feat(tui): wire up @-mention popup end-to-end (P2.1)
The audit doc claimed the wiring was "in place" but only the App state
fields existed (`mention_menu_selected`, `mention_menu_hidden`) — no
helpers, no widget rendering, no key handling. Building it out fully so
the popup actually shows when the user types `@` in the composer and
Up/Down/Enter/Tab/Esc behave the way the slash menu does.
What's new:
1. `file_mention::visible_mention_menu_entries(app, limit)` — the entries
source. Returns `Vec<String>` from the workspace walk, gated on the
`mention_menu_hidden` flag and on the cursor being inside an `@token`.
2. `file_mention::apply_mention_menu_selection(app, entries)` — splices
the selected entry into the input via the existing `replace_file_mention`,
resets `mention_menu_hidden`, surfaces a status confirmation.
3. `ComposerWidget::new(app, max_height, slash_entries, mention_entries)`
— second menu slot. The widget renders whichever slice is non-empty,
addressed by the matching selected index. Mention entries get an `@`
prefix so the popup row reads like the actual mention being composed.
Mention takes precedence (positional check is stricter than slash's
"starts-with-/").
4. ui.rs key handler:
- Up/Down navigate `mention_menu_selected` when the popup is open.
- Enter applies `apply_mention_menu_selection` instead of submitting.
- Tab applies the selection (then falls through to the existing slash /
command-completion / file-mention chain).
- Esc hides the popup until the next input edit (`insert_str` already
resets `mention_menu_hidden`, so typing re-opens it).
6 new tests in `ui/tests.rs`:
- mention_popup_is_empty_when_cursor_is_not_in_a_mention
- mention_popup_lists_workspace_matches_for_cursor_partial
- mention_popup_respects_hidden_flag
- apply_mention_menu_selection_splices_selected_entry
- apply_mention_menu_selection_is_noop_outside_a_mention
- apply_mention_menu_selection_with_no_entries_is_noop
Also fixes a stray duplicate `#[cfg(...)]` and an unused-doc-comment
warning that landed when the parse-counter went thread-local — back to
baseline 7 clippy warnings.
948 → 954 tests, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,60 @@ pub fn find_file_mention_completions(workspace: &Path, partial: &str, limit: usi
|
||||
prefix_hits
|
||||
}
|
||||
|
||||
/// Resolve the `@`-mention completion popup contents for the current
|
||||
/// composer state. Returns an empty `Vec` when:
|
||||
///
|
||||
/// - The popup is suppressed (`app.mention_menu_hidden`).
|
||||
/// - The cursor is not inside an `@<partial>` token.
|
||||
/// - The workspace walk produced no candidates.
|
||||
///
|
||||
/// Mirrors `visible_slash_menu_entries` so the composer widget can treat
|
||||
/// both menus identically (one `Vec<String>` of entries, one selected index).
|
||||
///
|
||||
/// Once the composer widget is extended to render this as a popup, it will
|
||||
/// pair with `apply_mention_menu_selection` for the Up/Down/Enter flow.
|
||||
#[must_use]
|
||||
pub fn visible_mention_menu_entries(app: &App, limit: usize) -> Vec<String> {
|
||||
if app.mention_menu_hidden {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some((_byte_start, partial)) =
|
||||
partial_file_mention_at_cursor(&app.input, app.cursor_position)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
find_file_mention_completions(&app.workspace, &partial, limit)
|
||||
}
|
||||
|
||||
/// Apply the currently selected `@`-mention popup entry to the composer
|
||||
/// input, splicing it in place of the `@<partial>` token at the cursor.
|
||||
/// Returns `true` if a substitution occurred.
|
||||
///
|
||||
/// Designed to be invoked by the same keybinding that drives
|
||||
/// `apply_slash_menu_selection` (Enter / Tab); the caller is responsible
|
||||
/// for choosing which menu is "active" based on cursor context.
|
||||
pub fn apply_mention_menu_selection(app: &mut App, entries: &[String]) -> bool {
|
||||
if entries.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let Some((byte_start, partial)) =
|
||||
partial_file_mention_at_cursor(&app.input, app.cursor_position)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let selected_idx = app
|
||||
.mention_menu_selected
|
||||
.min(entries.len().saturating_sub(1));
|
||||
let replacement = &entries[selected_idx];
|
||||
replace_file_mention(app, byte_start, &partial, replacement);
|
||||
app.mention_menu_hidden = false;
|
||||
app.status_message = Some(format!("Attached @{replacement}"));
|
||||
true
|
||||
}
|
||||
|
||||
/// Tab-completion handler for `@file` mentions. Mirrors the slash-command
|
||||
/// flow: a single match is applied directly; multiple matches with a longer
|
||||
/// shared prefix extend the partial; otherwise the first few candidates are
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(test, feature = "perf-counters"))]
|
||||
#[cfg(any(test, feature = "perf-counters"))]
|
||||
use std::cell::Cell;
|
||||
|
||||
@@ -36,14 +35,11 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::palette;
|
||||
|
||||
/// Per-process counter incremented every time [`parse`] runs. Used by tests to
|
||||
/// prove that width-only changes hit the cached-AST path and skip parsing.
|
||||
///
|
||||
/// Available in test builds and behind the `perf-counters` feature flag so
|
||||
/// release builds pay no cost.
|
||||
// Thread-local instead of a global atomic so concurrent tests that call
|
||||
// `parse()` don't pollute each other's counters. Each test thread sees only
|
||||
// its own invocations.
|
||||
// Thread-local counter incremented every time `parse` runs. Used by tests to
|
||||
// prove that width-only changes hit the cached-AST path and skip parsing.
|
||||
// Available in test builds and behind the `perf-counters` feature flag so
|
||||
// release builds pay no cost. Thread-local (not global atomic) so concurrent
|
||||
// tests calling `parse()` can't pollute each other's counters.
|
||||
#[cfg(any(test, feature = "perf-counters"))]
|
||||
thread_local! {
|
||||
static PARSE_INVOCATIONS: Cell<u64> = const { Cell::new(0) };
|
||||
|
||||
@@ -90,6 +90,7 @@ use super::widgets::{
|
||||
// === Constants ===
|
||||
|
||||
const SLASH_MENU_LIMIT: usize = 6;
|
||||
const MENTION_MENU_LIMIT: usize = 6;
|
||||
const MIN_CHAT_HEIGHT: u16 = 3;
|
||||
const MIN_COMPOSER_HEIGHT: u16 = 2;
|
||||
const CONTEXT_WARNING_THRESHOLD_PERCENT: f64 = 85.0;
|
||||
@@ -1142,6 +1143,12 @@ async fn run_event_loop(
|
||||
if slash_menu_open && app.slash_menu_selected >= slash_menu_entries.len() {
|
||||
app.slash_menu_selected = slash_menu_entries.len().saturating_sub(1);
|
||||
}
|
||||
let mention_menu_entries =
|
||||
crate::tui::file_mention::visible_mention_menu_entries(app, MENTION_MENU_LIMIT);
|
||||
let mention_menu_open = !mention_menu_entries.is_empty();
|
||||
if mention_menu_open && app.mention_menu_selected >= mention_menu_entries.len() {
|
||||
app.mention_menu_selected = mention_menu_entries.len().saturating_sub(1);
|
||||
}
|
||||
|
||||
// Global keybindings
|
||||
match key.code {
|
||||
@@ -1263,6 +1270,10 @@ async fn run_event_loop(
|
||||
let _ = engine_handle.send(Op::Shutdown).await;
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Esc if mention_menu_open => {
|
||||
app.mention_menu_hidden = true;
|
||||
app.mention_menu_selected = 0;
|
||||
}
|
||||
KeyCode::Esc => match next_escape_action(app, slash_menu_open) {
|
||||
EscapeAction::CloseSlashMenu => app.close_slash_menu(),
|
||||
EscapeAction::CancelRequest => {
|
||||
@@ -1281,6 +1292,13 @@ async fn run_event_loop(
|
||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
app.scroll_up(3);
|
||||
}
|
||||
KeyCode::Up
|
||||
if key.modifiers.is_empty()
|
||||
&& mention_menu_open
|
||||
&& app.mention_menu_selected > 0 =>
|
||||
{
|
||||
app.mention_menu_selected = app.mention_menu_selected.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Up
|
||||
if key.modifiers.is_empty()
|
||||
&& slash_menu_open
|
||||
@@ -1291,6 +1309,10 @@ async fn run_event_loop(
|
||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
app.scroll_down(3);
|
||||
}
|
||||
KeyCode::Down if key.modifiers.is_empty() && mention_menu_open => {
|
||||
app.mention_menu_selected = (app.mention_menu_selected + 1)
|
||||
.min(mention_menu_entries.len().saturating_sub(1));
|
||||
}
|
||||
KeyCode::Down if key.modifiers.is_empty() && slash_menu_open => {
|
||||
app.slash_menu_selected = (app.slash_menu_selected + 1)
|
||||
.min(slash_menu_entries.len().saturating_sub(1));
|
||||
@@ -1304,6 +1326,14 @@ async fn run_event_loop(
|
||||
app.scroll_down(page);
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
if mention_menu_open
|
||||
&& crate::tui::file_mention::apply_mention_menu_selection(
|
||||
app,
|
||||
&mention_menu_entries,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if slash_menu_open && apply_slash_menu_selection(app, &slash_menu_entries, true)
|
||||
{
|
||||
continue;
|
||||
@@ -1366,6 +1396,15 @@ async fn run_event_loop(
|
||||
KeyCode::Enter if key.modifiers.contains(KeyModifiers::ALT) => {
|
||||
app.insert_char('\n');
|
||||
}
|
||||
KeyCode::Enter
|
||||
if mention_menu_open
|
||||
&& crate::tui::file_mention::apply_mention_menu_selection(
|
||||
app,
|
||||
&mention_menu_entries,
|
||||
) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Some(input) = app.submit_input() {
|
||||
if handle_plan_choice(app, &engine_handle, &input).await? {
|
||||
@@ -2580,12 +2619,24 @@ fn render(f: &mut Frame, app: &mut App) {
|
||||
let footer_height = 1;
|
||||
let body_height = size.height.saturating_sub(header_height + footer_height);
|
||||
let slash_menu_entries = visible_slash_menu_entries(app, SLASH_MENU_LIMIT);
|
||||
let mention_menu_entries =
|
||||
crate::tui::file_mention::visible_mention_menu_entries(app, MENTION_MENU_LIMIT);
|
||||
if !mention_menu_entries.is_empty()
|
||||
&& app.mention_menu_selected >= mention_menu_entries.len()
|
||||
{
|
||||
app.mention_menu_selected = mention_menu_entries.len().saturating_sub(1);
|
||||
}
|
||||
let context_usage = context_usage_snapshot(app);
|
||||
let composer_max_height = body_height
|
||||
.saturating_sub(MIN_CHAT_HEIGHT)
|
||||
.max(MIN_COMPOSER_HEIGHT);
|
||||
let composer_height = {
|
||||
let composer_widget = ComposerWidget::new(app, composer_max_height, &slash_menu_entries);
|
||||
let composer_widget = ComposerWidget::new(
|
||||
app,
|
||||
composer_max_height,
|
||||
&slash_menu_entries,
|
||||
&mention_menu_entries,
|
||||
);
|
||||
composer_widget.desired_height(size.width)
|
||||
};
|
||||
|
||||
@@ -2672,7 +2723,12 @@ fn render(f: &mut Frame, app: &mut App) {
|
||||
|
||||
// Render composer
|
||||
let cursor_pos = {
|
||||
let composer_widget = ComposerWidget::new(app, composer_max_height, &slash_menu_entries);
|
||||
let composer_widget = ComposerWidget::new(
|
||||
app,
|
||||
composer_max_height,
|
||||
&slash_menu_entries,
|
||||
&mention_menu_entries,
|
||||
);
|
||||
let buf = f.buffer_mut();
|
||||
composer_widget.render(chunks[2], buf);
|
||||
composer_widget.cursor_pos(chunks[2])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::tui::file_mention::{
|
||||
find_file_mention_completions, partial_file_mention_at_cursor, try_autocomplete_file_mention,
|
||||
user_request_with_file_mentions,
|
||||
apply_mention_menu_selection, find_file_mention_completions, partial_file_mention_at_cursor,
|
||||
try_autocomplete_file_mention, user_request_with_file_mentions, visible_mention_menu_entries,
|
||||
};
|
||||
use crate::tui::history::{GenericToolCell, HistoryCell, ToolCell, ToolStatus};
|
||||
use crate::tui::views::{ModalView, ViewAction};
|
||||
@@ -1057,6 +1057,105 @@ fn try_autocomplete_file_mention_returns_false_outside_mention() {
|
||||
assert!(!try_autocomplete_file_mention(&mut app));
|
||||
}
|
||||
|
||||
// ---- P2.1: @-mention popup helpers ----
|
||||
//
|
||||
// `visible_mention_menu_entries` is the entries source the composer widget
|
||||
// renders; `apply_mention_menu_selection` is what Tab/Enter invoke when the
|
||||
// popup is open. The popup widget itself piggybacks the slash-menu render
|
||||
// path (see `ComposerWidget::active_menu_entries`).
|
||||
|
||||
#[test]
|
||||
fn mention_popup_is_empty_when_cursor_is_not_in_a_mention() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "no mention here".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
assert!(visible_mention_menu_entries(&app, 6).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mention_popup_lists_workspace_matches_for_cursor_partial() {
|
||||
let tmpdir = TempDir::new().expect("tempdir");
|
||||
std::fs::create_dir_all(tmpdir.path().join("docs")).unwrap();
|
||||
std::fs::write(tmpdir.path().join("docs/deepseek_v4.pdf"), b"%PDF-").unwrap();
|
||||
std::fs::write(tmpdir.path().join("docs/MCP.md"), "x").unwrap();
|
||||
std::fs::write(tmpdir.path().join("README.md"), "x").unwrap();
|
||||
|
||||
let mut app = create_test_app();
|
||||
app.workspace = tmpdir.path().to_path_buf();
|
||||
app.input = "look at @docs/".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
|
||||
let entries = visible_mention_menu_entries(&app, 6);
|
||||
assert!(!entries.is_empty(), "popup should surface docs/ entries");
|
||||
assert!(entries.iter().any(|e| e.starts_with("docs/")));
|
||||
// README.md doesn't match `docs/` — confirm we didn't dump every file.
|
||||
assert!(!entries.iter().any(|e| e == "README.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mention_popup_respects_hidden_flag() {
|
||||
let tmpdir = TempDir::new().expect("tempdir");
|
||||
std::fs::write(tmpdir.path().join("README.md"), "x").unwrap();
|
||||
|
||||
let mut app = create_test_app();
|
||||
app.workspace = tmpdir.path().to_path_buf();
|
||||
app.input = "@READ".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
app.mention_menu_hidden = true;
|
||||
|
||||
assert!(
|
||||
visible_mention_menu_entries(&app, 6).is_empty(),
|
||||
"Esc-hidden popup must not surface entries until next input edit",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_mention_menu_selection_splices_selected_entry() {
|
||||
let tmpdir = TempDir::new().expect("tempdir");
|
||||
std::fs::create_dir_all(tmpdir.path().join("crates/tui")).unwrap();
|
||||
std::fs::write(tmpdir.path().join("crates/tui/lib.rs"), "//").unwrap();
|
||||
std::fs::write(tmpdir.path().join("crates/tui/main.rs"), "//").unwrap();
|
||||
|
||||
let mut app = create_test_app();
|
||||
app.workspace = tmpdir.path().to_path_buf();
|
||||
app.input = "open @crates/tui/m".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
|
||||
let entries = visible_mention_menu_entries(&app, 6);
|
||||
assert!(!entries.is_empty(), "expected entries for @crates/tui/m");
|
||||
// Pick whichever entry appears at index 0; it's deterministic given the
|
||||
// workspace setup. Apply it.
|
||||
app.mention_menu_selected = 0;
|
||||
let applied = apply_mention_menu_selection(&mut app, &entries);
|
||||
assert!(applied, "apply_mention_menu_selection should report success");
|
||||
assert!(
|
||||
app.input.starts_with("open @"),
|
||||
"input should still start with `open @`, got: {input}",
|
||||
input = app.input,
|
||||
);
|
||||
// Cursor should land at the end of the spliced token.
|
||||
assert_eq!(app.cursor_position, app.input.chars().count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_mention_menu_selection_is_noop_outside_a_mention() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "no @ here".to_string();
|
||||
app.cursor_position = 1; // before the @ token
|
||||
let applied = apply_mention_menu_selection(&mut app, &["whatever".to_string()]);
|
||||
assert!(!applied);
|
||||
assert_eq!(app.input, "no @ here");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_mention_menu_selection_with_no_entries_is_noop() {
|
||||
let mut app = create_test_app();
|
||||
app.input = "@partial".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
let applied = apply_mention_menu_selection(&mut app, &[]);
|
||||
assert!(!applied);
|
||||
}
|
||||
|
||||
// === CX#7 — single active cell mutated in place for parallel tool calls ===
|
||||
|
||||
/// Build a minimal successful ToolResult with the given content.
|
||||
|
||||
@@ -203,14 +203,42 @@ pub struct ComposerWidget<'a> {
|
||||
app: &'a App,
|
||||
max_height: u16,
|
||||
slash_menu_entries: &'a [String],
|
||||
mention_menu_entries: &'a [String],
|
||||
}
|
||||
|
||||
impl<'a> ComposerWidget<'a> {
|
||||
pub fn new(app: &'a App, max_height: u16, slash_menu_entries: &'a [String]) -> Self {
|
||||
pub fn new(
|
||||
app: &'a App,
|
||||
max_height: u16,
|
||||
slash_menu_entries: &'a [String],
|
||||
mention_menu_entries: &'a [String],
|
||||
) -> Self {
|
||||
Self {
|
||||
app,
|
||||
max_height,
|
||||
slash_menu_entries,
|
||||
mention_menu_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of popup rows below the input. Mention and slash menus are
|
||||
/// mutually exclusive — the cursor can only sit inside an `@token` OR
|
||||
/// a `/cmd` token, not both at once. Mention takes precedence because
|
||||
/// the partial-mention check is positional and stricter than slash's
|
||||
/// "starts-with-/" check.
|
||||
fn active_menu_entries(&self) -> &'a [String] {
|
||||
if !self.mention_menu_entries.is_empty() {
|
||||
self.mention_menu_entries
|
||||
} else {
|
||||
self.slash_menu_entries
|
||||
}
|
||||
}
|
||||
|
||||
fn active_menu_selected(&self) -> usize {
|
||||
if !self.mention_menu_entries.is_empty() {
|
||||
self.app.mention_menu_selected
|
||||
} else {
|
||||
self.app.slash_menu_selected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +272,8 @@ impl Renderable for ComposerWidget<'_> {
|
||||
let background = Style::default().bg(self.app.ui_theme.composer_bg);
|
||||
let has_panel = self.has_panel(area);
|
||||
let inner_area = self.inner_area(area);
|
||||
let menu_lines = self.slash_menu_entries.len();
|
||||
let menu_entries = self.active_menu_entries();
|
||||
let menu_lines = menu_entries.len();
|
||||
let input_rows_budget = composer_input_rows_budget(inner_area.height, menu_lines);
|
||||
let content_width = usize::from(inner_area.width.max(1));
|
||||
let (visible_lines, _cursor_row, _cursor_col) = layout_input(
|
||||
@@ -317,12 +346,18 @@ impl Renderable for ComposerWidget<'_> {
|
||||
}
|
||||
lines.extend(input_lines);
|
||||
|
||||
if !self.slash_menu_entries.is_empty() {
|
||||
if !menu_entries.is_empty() {
|
||||
let selected = self
|
||||
.app
|
||||
.slash_menu_selected
|
||||
.min(self.slash_menu_entries.len().saturating_sub(1));
|
||||
for (idx, entry) in self.slash_menu_entries.iter().enumerate() {
|
||||
.active_menu_selected()
|
||||
.min(menu_entries.len().saturating_sub(1));
|
||||
// `@`-mention entries get an "@" prefix so the popup line reads
|
||||
// like the actual mention the user is composing.
|
||||
let prefix = if !self.mention_menu_entries.is_empty() {
|
||||
"@"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
for (idx, entry) in menu_entries.iter().enumerate() {
|
||||
let is_selected = idx == selected;
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
@@ -336,7 +371,7 @@ impl Renderable for ComposerWidget<'_> {
|
||||
Span::styled(" ", Style::default()),
|
||||
Span::styled(marker, style),
|
||||
Span::styled(" ", style),
|
||||
Span::styled(entry.clone(), style),
|
||||
Span::styled(format!("{prefix}{entry}"), style),
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -352,7 +387,7 @@ impl Renderable for ComposerWidget<'_> {
|
||||
&self.app.input,
|
||||
width,
|
||||
self.max_height.min(self.max_height_cap()),
|
||||
self.slash_menu_entries.len(),
|
||||
self.active_menu_entries().len(),
|
||||
self.app.composer_density,
|
||||
self.app.composer_border,
|
||||
)
|
||||
@@ -362,7 +397,7 @@ impl Renderable for ComposerWidget<'_> {
|
||||
let inner_area = self.inner_area(area);
|
||||
let content_width = usize::from(inner_area.width.max(1));
|
||||
let input_rows_budget =
|
||||
composer_input_rows_budget(inner_area.height, self.slash_menu_entries.len());
|
||||
composer_input_rows_budget(inner_area.height, self.active_menu_entries().len());
|
||||
|
||||
let (visible_lines, cursor_row, cursor_col) = layout_input(
|
||||
&self.app.input,
|
||||
@@ -1394,7 +1429,8 @@ mod tests {
|
||||
// Pin density so the test is independent of any loaded user settings.
|
||||
app.composer_density = ComposerDensity::Comfortable;
|
||||
let slash_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 5, &slash_menu_entries);
|
||||
let mention_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 5, &slash_menu_entries, &mention_menu_entries);
|
||||
|
||||
// Use a wide area so the placeholder fits on one line (no wrapping).
|
||||
let area = Rect {
|
||||
@@ -1418,7 +1454,8 @@ mod tests {
|
||||
let mut app = create_test_app();
|
||||
app.composer_density = ComposerDensity::Comfortable;
|
||||
let slash_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 5, &slash_menu_entries);
|
||||
let mention_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 5, &slash_menu_entries, &mention_menu_entries);
|
||||
|
||||
// Narrow area forces the placeholder to wrap.
|
||||
let area = Rect {
|
||||
@@ -1444,7 +1481,8 @@ mod tests {
|
||||
app.composer_density = ComposerDensity::Comfortable;
|
||||
app.composer_border = false;
|
||||
let slash_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 3, &slash_menu_entries);
|
||||
let mention_menu_entries = Vec::<String>::new();
|
||||
let widget = ComposerWidget::new(&app, 3, &slash_menu_entries, &mention_menu_entries);
|
||||
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
|
||||
Reference in New Issue
Block a user