返回 DeepSeek-TUI-2026
slash_menu.rs
根目录 / crates / tui / src / tui / slash_menu.rs
1 //! Slash-command autocomplete + popup-menu helpers.
2 //!
3 //! Extracted from `tui/ui.rs` (P1.2). The on-screen popup itself is rendered
4 //! by the composer widget; these helpers source the entries, apply a
5 //! selection, and handle Tab-completion when the popup isn't open.
6 //!
7 //! Intentionally separate from `tui::file_mention` even though both surface
8 //! a similar popup — the trigger characters, ranking, and post-selection
9 //! behaviour differ enough to keep them apart.
10
11 use crate::commands;
12
13 use super::app::App;
14 use super::widgets::SlashMenuEntry;
15 use super::widgets::slash_completion_hints;
16
17 /// Return the slash-menu entries the composer should display, honouring
18 /// `slash_menu_hidden` (set when the user dismisses the popup with Esc).
19 pub fn visible_slash_menu_entries(app: &App, limit: usize) -> Vec<SlashMenuEntry> {
20 if app.slash_menu_hidden {
21 return Vec::new();
22 }
23 slash_completion_hints(&app.input, limit, &app.cached_skills, app.ui_locale)
24 }
25
26 /// Apply the currently-selected slash menu entry to the composer input.
27 /// Optionally appends a trailing space when the command takes arguments
28 /// so the user can type the rest without an extra keystroke.
29 pub fn apply_slash_menu_selection(
30 app: &mut App,
31 entries: &[SlashMenuEntry],
32 append_space: bool,
33 ) -> bool {
34 if entries.is_empty() {
35 return false;
36 }
37
38 let selected_idx = app.slash_menu_selected.min(entries.len().saturating_sub(1));
39 let mut command = entries[selected_idx].name.clone();
40
41 if append_space
42 && !command.ends_with(' ')
43 && !command.contains(char::is_whitespace)
44 && let Some(info) = commands::get_command_info(command.trim_start_matches('/'))
45 && (info.usage.contains('<') || info.usage.contains('['))
46 {
47 command.push(' ');
48 }
49
50 app.input = command;
51 app.cursor_position = app.input.chars().count();
52 app.slash_menu_hidden = false;
53 app.status_message = Some(format!("Command selected: {}", app.input.trim_end()));
54 true
55 }
56
57 /// Tab-completion for a slash-command-like input. Extends the input to the
58 /// longest unambiguous prefix; if exactly one command matches, completes it
59 /// fully (with trailing space). On ambiguity, posts a status hint listing
60 /// up to five candidates. Also considers skill names as completion candidates.
61 pub fn try_autocomplete_slash_command(app: &mut App) -> bool {
62 if !app.input.starts_with('/') {
63 return false;
64 }
65
66 let candidates = slash_completion_hints(&app.input, 128, &app.cached_skills, app.ui_locale)
67 .into_iter()
68 .map(|entry| entry.name)
69 .collect::<Vec<_>>();
70
71 if candidates.is_empty() {
72 return false;
73 }
74
75 let prefix = app.input.trim_start_matches('/');
76 let refs: Vec<&str> = candidates
77 .iter()
78 .map(|name| name.trim_start_matches('/'))
79 .collect();
80 let shared = crate::tui::file_mention::longest_common_prefix(&refs);
81
82 if !shared.is_empty() && shared.len() > prefix.len() {
83 app.input = format!("/{shared}");
84 app.cursor_position = app.input.chars().count();
85 app.slash_menu_hidden = false;
86 app.status_message = Some(format!("Autocomplete: /{shared}"));
87 return true;
88 }
89
90 if candidates.len() == 1 {
91 let mut completed = candidates[0].clone();
92 if !completed.ends_with(' ') {
93 completed.push(' ');
94 }
95 app.input = completed.clone();
96 app.cursor_position = completed.chars().count();
97 app.slash_menu_hidden = false;
98 app.status_message = Some(format!("Command completed: {}", completed.trim_end()));
99 return true;
100 }
101
102 let preview = candidates
103 .iter()
104 .take(5)
105 .map(String::as_str)
106 .collect::<Vec<_>>()
107 .join(", ");
108 app.status_message = Some(format!("Suggestions: {preview}"));
109 true
110 }
111
111 lines RUST