-
Notifications
You must be signed in to change notification settings - Fork 195
feat: add auto_complete_menu option for IDE-style completions #993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -143,6 +143,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hinter: Option<Box<dyn Hinter>>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hide_hints: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Auto-show completion menu as you type (IDE/fish style) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto_complete_menu: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this name can confuse people. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use ansi coloring or not | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use_ansi_coloring: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -233,6 +236,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visual_selection_style, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hinter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hide_hints: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto_complete_menu: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validator, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use_ansi_coloring: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cwd: None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -326,6 +330,16 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Enable auto-showing completion menu as you type (IDE/fish style) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// When enabled, the completion menu named "completion_menu" will automatically | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// appear as you type, without needing to press Tab. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[must_use] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub fn with_auto_complete_menu(mut self, enable: bool) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.auto_complete_menu = enable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// A builder to configure the tab completion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// # Example | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// ```rust | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1209,11 +1223,65 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.editor.line_buffer().get_buffer().is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if we should keep the menu active | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let buffer = self.editor.line_buffer().get_buffer(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let should_stay_active = if self.auto_complete_menu { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use same logic as auto-activate: need 1+ char after last space | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(last_word_start) = buffer.rfind(' ') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !buffer[last_word_start + 1..].is_empty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First word - keep menu if user activated it manually | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !buffer.is_empty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No auto-complete mode, keep active unless empty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !buffer.is_empty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !should_stay_active { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.menu_event(MenuEvent::Deactivate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.menu_event(MenuEvent::Edit(self.quick_completions)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if self.auto_complete_menu { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Auto-activate completion menu (IDE style) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Flags: show immediately on "-" or "--" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Files/args: show after 1+ chars typed (not just space) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Commands (first word): don't auto-complete | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let buffer = self.editor.line_buffer().get_buffer(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let should_show = if let Some(last_word_start) = buffer.rfind(' ') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to make it so that the menu is always active, even if it's after a space. Let's leave it up to the menu and completer to decide whether to show anything. I know this means users will see "NO RECORDS" all the time, but I'm sure we can work around that (or just let it be). @blindFS thoughts?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit tricky. On one side, there should be a configurable min-triggering-characters to lower the frequency. However spaces are sometimes meaningful characters for Seems to me a pure reedline solution won't be "smart" enough in that sense.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not just for the fuzzy/substring matchers, I'm thinking about a scenario where someone has a command And yeah, it's out of scope for Reedline to guess how a completer works. The only time that disabling the menu is probably safe is when the user hasn't typed anything at all yet (empty buffer) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let last_word = &buffer[last_word_start + 1..]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Show if: flags (starts with -) OR has 1+ char typed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !last_word.is_empty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First word - don't auto-complete commands | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 1258 in src/engine.rs
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(menu) = self.menus.iter_mut().find(|m| m.name() == "completion_menu") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fan of the menu name being hardcoded here. I'd rather |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if should_show { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !menu.is_active() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only activate if not already active | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.menu_event(MenuEvent::Activate(self.quick_completions)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.update_values( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &mut self.editor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.completer.as_mut(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.history.as_ref(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Menu already active, just send edit event to update | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.menu_event(MenuEvent::Edit(self.quick_completions)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.update_values( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| &mut self.editor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.completer.as_mut(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.history.as_ref(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1263
to
+1279
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick (pardon the indentation)
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if menu.is_active() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Deactivate when no longer typing a flag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| menu.menu_event(MenuEvent::Deactivate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(EventStatus::Handled) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| use crate::{hinter::get_first_token, Completer, Hinter, History}; | ||
| use nu_ansi_term::{Color, Style}; | ||
| use std::sync::{Arc, Mutex}; | ||
|
|
||
| /// A hinter that uses completions (not history) to show inline suggestions | ||
| /// | ||
| /// This provides fish-style autosuggestions based on what the completer returns, | ||
| /// showing the first completion result as gray text that can be accepted with → | ||
| pub struct CompletionHinter { | ||
| completer: Arc<Mutex<dyn Completer + Send>>, | ||
| style: Style, | ||
| current_hint: String, | ||
| min_chars: usize, | ||
| } | ||
|
|
||
| impl Hinter for CompletionHinter { | ||
| fn handle( | ||
| &mut self, | ||
| line: &str, | ||
| pos: usize, | ||
| _history: &dyn History, | ||
| use_ansi_coloring: bool, | ||
| _cwd: &str, | ||
| ) -> String { | ||
| self.current_hint = if line.chars().count() >= self.min_chars && pos == line.len() { | ||
| // Only show hints when cursor is at end of line | ||
| if let Ok(mut completer) = self.completer.lock() { | ||
| let suggestions = completer.complete(line, pos); | ||
| if let Some(first) = suggestions.first() { | ||
| // The suggestion replaces line[span.start..span.end] with `value` | ||
| // We want to show what extends beyond what the user typed | ||
| let span_end = first.span.end.min(line.len()); | ||
| let span_start = first.span.start.min(span_end); | ||
| let typed_portion = &line[span_start..span_end]; | ||
|
|
||
| // If the completion value starts with what's being replaced, | ||
| // show the suffix (the new part) | ||
| if let Some(suffix) = first.value.strip_prefix(typed_portion) { | ||
| suffix.to_string() | ||
| } else { | ||
| // Fuzzy match - just show if value is longer than typed | ||
| if first.value.len() > typed_portion.len() { | ||
| first.value.clone() | ||
| } else { | ||
| String::new() | ||
| } | ||
| } | ||
| } else { | ||
| String::new() | ||
| } | ||
| } else { | ||
| String::new() | ||
| } | ||
| } else { | ||
| String::new() | ||
| }; | ||
|
|
||
| if use_ansi_coloring && !self.current_hint.is_empty() { | ||
| self.style.paint(&self.current_hint).to_string() | ||
| } else { | ||
| self.current_hint.clone() | ||
| } | ||
| } | ||
|
|
||
| fn complete_hint(&self) -> String { | ||
| self.current_hint.clone() | ||
| } | ||
|
|
||
| fn next_hint_token(&self) -> String { | ||
| get_first_token(&self.current_hint) | ||
| } | ||
| } | ||
|
|
||
| impl CompletionHinter { | ||
| /// Create a new CompletionHinter with the given completer | ||
| pub fn new(completer: Arc<Mutex<dyn Completer + Send>>) -> Self { | ||
| CompletionHinter { | ||
| completer, | ||
| style: Style::new().fg(Color::DarkGray), | ||
| current_hint: String::new(), | ||
| min_chars: 1, | ||
| } | ||
| } | ||
|
|
||
| /// A builder that sets the style applied to the hint | ||
| #[must_use] | ||
| pub fn with_style(mut self, style: Style) -> Self { | ||
| self.style = style; | ||
| self | ||
| } | ||
|
|
||
| /// A builder that sets minimum characters before showing hints | ||
| #[must_use] | ||
| pub fn with_min_chars(mut self, min_chars: usize) -> Self { | ||
| self.min_chars = min_chars; | ||
| self | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You say "IDE" here, but looking at your code, it won't necessarily pick the
IdeMenu, it'll pick whichever menu is namedcompletion_menu. I think you just happened to see the IDE menu because that's what you have set in your Nushell config.But also, let's not restrict this feature to the IDE menu. I prefer the columnar menu myself and would like to use that. So you can just remove the "(IDE/fish style)" bit from this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment just explain what the author means by "as you type", since in IDE like vscode completion is shown as you type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is that "IDE" in reedline context has sort of special meaning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surely, but given that it appears next to
fishI don't think it relates to this internal meaning. Nevermind it's just a comment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer precise comments.