Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@
hinter: Option<Box<dyn Hinter>>,
hide_hints: bool,

// Auto-show completion menu as you type (IDE/fish style)
Copy link
Member

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

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.

Copy link
Contributor

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.

The thing is that "IDE" in reedline context has sort of special meaning.

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 fish I don't think it relates to this internal meaning. Nevermind it's just a comment.

Copy link
Contributor

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 fish I don't think it relates to this internal meaning. Nevermind it's just a comment.

I'd prefer precise comments.

auto_complete_menu: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this name can confuse people. auto_complete_menu sounds like it's a menu for autocompletion, which all the menus are for. I'd prefer always_on_menu or something like that


// Use ansi coloring or not
use_ansi_coloring: bool,

Expand Down Expand Up @@ -233,6 +236,7 @@
visual_selection_style,
hinter,
hide_hints: false,
auto_complete_menu: false,
validator,
use_ansi_coloring: true,
cwd: None,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(' ') {
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

The 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 fuzzy/substring matcher.

Seems to me a pure reedline solution won't be "smart" enough in that sense.

Copy link
Member

Choose a reason for hiding this comment

The 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 foo that can only take one fruit as an argument: apple or banana. So if I type foo <TAB>, I want to see the options apple and banana, even if I haven't typed anything yet

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

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, default)

Diff in /home/runner/work/reedline/reedline/src/engine.rs

Check warning on line 1258 in src/engine.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, sqlite)

Diff in /home/runner/work/reedline/reedline/src/engine.rs

Check warning on line 1258 in src/engine.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, basqlite)

Diff in /home/runner/work/reedline/reedline/src/engine.rs

Check warning on line 1258 in src/engine.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, bashisms)

Diff in /home/runner/work/reedline/reedline/src/engine.rs

Check warning on line 1258 in src/engine.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, external_printer)

Diff in /home/runner/work/reedline/reedline/src/engine.rs
};

if let Some(menu) = self.menus.iter_mut().find(|m| m.name() == "completion_menu") {
Copy link
Member

Choose a reason for hiding this comment

The 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 auto_complete_menu be an Option<String> holding the name of the menu to activate automatically. That way, users can choose which menu they want to always be active.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick (pardon the indentation)

Suggested change
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(),
);
}
if !menu.is_active() {
// Only activate if not already active
menu.menu_event(MenuEvent::Activate(self.quick_completions));
} else {
menu.menu_event(MenuEvent::Edit(self.quick_completions));
}
menu.update_values(
&mut self.editor,
self.completer.as_mut(),
self.history.as_ref(),
);

} else if menu.is_active() {
// Deactivate when no longer typing a flag
menu.menu_event(MenuEvent::Deactivate);
}
}
}
Ok(EventStatus::Handled)
}
Expand Down
98 changes: 98 additions & 0 deletions src/hinter/completion.rs
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
}
}
2 changes: 2 additions & 0 deletions src/hinter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod completion;
mod cwd_aware;
mod default;
pub use completion::CompletionHinter;
pub use cwd_aware::CwdAwareHinter;
pub use default::DefaultHinter;

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ mod completion;
pub use completion::{Completer, DefaultCompleter, Span, Suggestion};

mod hinter;
pub use hinter::CompletionHinter;
pub use hinter::CwdAwareHinter;
pub use hinter::{DefaultHinter, Hinter};

Expand Down
Loading