Skip to main content

codetether_agent/tui/app/state/
slash_suggest.rs

1//! Slash autocomplete suggestion methods.
2//!
3//! Refreshing, navigating, and applying slash command completions.
4
5use crate::tui::models::InputMode;
6
7impl super::AppState {
8    pub fn refresh_slash_suggestions(&mut self) {
9        if !self.input.starts_with('/') {
10            self.slash_suggestions.clear();
11            self.selected_slash_suggestion = 0;
12            return;
13        }
14        let raw_query = self.input.split_whitespace().next().unwrap_or("");
15        let query = crate::tui::app::text::normalize_slash_command(raw_query).to_lowercase();
16        let mut matches: Vec<&str> = super::slash_commands::SLASH_COMMANDS
17            .iter()
18            .filter(|cmd| cmd.starts_with(&query))
19            .copied()
20            .collect();
21        if matches.is_empty() && query.len() > 1 {
22            let needle = &query[1..];
23            matches = super::slash_commands::SLASH_COMMANDS
24                .iter()
25                .filter(|cmd| cmd.contains(needle))
26                .copied()
27                .collect();
28        }
29        if matches.is_empty() && query == "/" {
30            matches = super::slash_commands::SLASH_COMMANDS
31                .iter()
32                .copied()
33                .collect();
34        }
35        self.slash_suggestions = matches.into_iter().map(String::from).collect();
36        if self.selected_slash_suggestion >= self.slash_suggestions.len() {
37            self.selected_slash_suggestion = self.slash_suggestions.len().saturating_sub(1);
38        }
39    }
40
41    pub fn slash_suggestions_visible(&self) -> bool {
42        // Stay visible past the first space so the suggestions panel can
43        // swap in a usage hint for the typed command — see
44        // [`AppState::current_slash_hint`].
45        self.view_mode == crate::tui::models::ViewMode::Chat
46            && !self.processing
47            && self.input.starts_with('/')
48    }
49
50    /// True only when there is a non-empty prefix-match list the user can
51    /// navigate with Tab / Up / Down / PageUp-Down. Distinct from
52    /// [`AppState::slash_suggestions_visible`], which also returns true
53    /// when the panel is showing a usage hint with no list to navigate.
54    pub fn slash_suggestions_navigable(&self) -> bool {
55        self.slash_suggestions_visible() && !self.slash_suggestions.is_empty()
56    }
57
58    /// Usage hint for the currently-typed slash command, if any.
59    ///
60    /// Looks up the first whitespace-separated token of `input` in
61    /// [`super::slash_hints`]. Used by the chat-view suggestions panel to
62    /// render `<args>` documentation once the prefix-match list is empty.
63    pub fn current_slash_hint(&self) -> Option<&'static str> {
64        let cmd = self.input.split_whitespace().next()?;
65        super::slash_hints::usage_hint(cmd)
66    }
67
68    pub fn select_prev_slash_suggestion(&mut self) {
69        if !self.slash_suggestions.is_empty() {
70            self.selected_slash_suggestion = self.selected_slash_suggestion.saturating_sub(1);
71        }
72    }
73
74    pub fn select_next_slash_suggestion(&mut self) {
75        if !self.slash_suggestions.is_empty() {
76            self.selected_slash_suggestion =
77                (self.selected_slash_suggestion + 1).min(self.slash_suggestions.len() - 1);
78        }
79    }
80
81    pub fn selected_slash_suggestion(&self) -> Option<&str> {
82        self.slash_suggestions
83            .get(self.selected_slash_suggestion)
84            .map(String::as_str)
85    }
86
87    pub fn apply_selected_slash_suggestion(&mut self) -> bool {
88        if let Some(selected) = self.selected_slash_suggestion() {
89            self.input = selected.to_string();
90            self.input_cursor = self.input_char_count();
91            self.input_mode = InputMode::Command;
92            self.refresh_slash_suggestions();
93            return true;
94        }
95        false
96    }
97}