| 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 |