Rust · 5314 bytes Raw Blame History
1 //! Custom hinter that combines history and completion suggestions
2 //!
3 //! Unlike DefaultHinter which only uses history, this hinter also provides
4 //! ghost text suggestions from completions when there's a single match.
5
6 use crate::completer::RushCompleter;
7 use nu_ansi_term::{Color, Style};
8 use reedline::{Completer, Hinter, History};
9
10 /// A hinter that provides suggestions from both history and completions
11 pub struct RushHinter {
12 style: Style,
13 current_hint: String,
14 min_chars: usize,
15 }
16
17 impl RushHinter {
18 pub fn new() -> Self {
19 Self {
20 style: Style::new().fg(Color::DarkGray),
21 current_hint: String::new(),
22 min_chars: 1,
23 }
24 }
25
26 /// Set the style for the hint text
27 pub fn with_style(mut self, style: Style) -> Self {
28 self.style = style;
29 self
30 }
31
32 /// Set minimum characters before showing hints
33 pub fn with_min_chars(mut self, min_chars: usize) -> Self {
34 self.min_chars = min_chars;
35 self
36 }
37
38 /// Get hint from history (matching prefix)
39 fn get_history_hint(&self, line: &str, history: &dyn History) -> Option<String> {
40 if line.is_empty() {
41 return None;
42 }
43
44 // Search history for commands starting with current line
45 let search = history
46 .search(reedline::SearchQuery::last_with_prefix(
47 line.to_string(),
48 None, // No session filter
49 ))
50 .ok()?;
51
52 // Return the first match's suffix (part after current input)
53 search.first().and_then(|entry| {
54 let cmd = &entry.command_line;
55 if cmd.starts_with(line) && cmd.len() > line.len() {
56 Some(cmd[line.len()..].to_string())
57 } else {
58 None
59 }
60 })
61 }
62
63 /// Get hint from completions
64 /// Shows hint for single match, or common prefix for multiple matches
65 fn get_completion_hint(&self, line: &str, pos: usize) -> Option<String> {
66 let mut completer = RushCompleter::new();
67 let suggestions = completer.complete(line, pos);
68
69 if suggestions.is_empty() {
70 return None;
71 }
72
73 // Get the current word being completed
74 let current_word_start = suggestions[0].span.start;
75 let current_word = &line[current_word_start..pos];
76
77 if suggestions.len() == 1 {
78 // Single match - show full completion as hint
79 let suggestion = &suggestions[0];
80 if suggestion.value.starts_with(current_word) {
81 let hint = &suggestion.value[current_word.len()..];
82 if !hint.is_empty() {
83 return Some(hint.to_string());
84 }
85 }
86 } else {
87 // Multiple matches - find common prefix beyond current input
88 let first = &suggestions[0].value;
89 let common_prefix = suggestions.iter().skip(1).fold(first.clone(), |acc, s| {
90 acc.chars()
91 .zip(s.value.chars())
92 .take_while(|(a, b)| a == b)
93 .map(|(a, _)| a)
94 .collect()
95 });
96
97 // Only show hint if common prefix extends beyond what user typed
98 if common_prefix.len() > current_word.len() && common_prefix.starts_with(current_word) {
99 let hint = &common_prefix[current_word.len()..];
100 if !hint.is_empty() {
101 return Some(hint.to_string());
102 }
103 }
104 }
105
106 None
107 }
108 }
109
110 impl Default for RushHinter {
111 fn default() -> Self {
112 Self::new()
113 }
114 }
115
116 impl Hinter for RushHinter {
117 fn handle(
118 &mut self,
119 line: &str,
120 pos: usize,
121 history: &dyn History,
122 use_ansi_coloring: bool,
123 _cwd: &str,
124 ) -> String {
125 self.current_hint.clear();
126
127 // Don't show hints for very short input
128 if line.len() < self.min_chars {
129 return String::new();
130 }
131
132 // Only provide hints when cursor is at end of line
133 if pos != line.len() {
134 return String::new();
135 }
136
137 // First try history hints (higher priority)
138 if let Some(hint) = self.get_history_hint(line, history) {
139 self.current_hint = hint.clone();
140 return if use_ansi_coloring {
141 self.style.paint(&hint).to_string()
142 } else {
143 hint
144 };
145 }
146
147 // Fall back to completion hints
148 if let Some(hint) = self.get_completion_hint(line, pos) {
149 self.current_hint = hint.clone();
150 return if use_ansi_coloring {
151 self.style.paint(&hint).to_string()
152 } else {
153 hint
154 };
155 }
156
157 String::new()
158 }
159
160 fn complete_hint(&self) -> String {
161 self.current_hint.clone()
162 }
163
164 fn next_hint_token(&self) -> String {
165 // Return the first word/token of the hint for incremental completion
166 let hint = &self.current_hint;
167
168 // Find first whitespace or end of string
169 if let Some(space_pos) = hint.find(char::is_whitespace) {
170 hint[..space_pos].to_string()
171 } else {
172 hint.clone()
173 }
174 }
175 }
176