Skip to content

feat(menu): let a menu close at the end of the word it was opened for - #1209

Open
shreeve wants to merge 1 commit into
nushell:mainfrom
shreeve:menu-closes-at-word-boundary
Open

feat(menu): let a menu close at the end of the word it was opened for#1209
shreeve wants to merge 1 commit into
nushell:mainfrom
shreeve:menu-closes-at-word-boundary

Conversation

@shreeve

@shreeve shreeve commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1176.

A menu opened over a word refilters against everything typed after it, so it stays
active for the rest of the line and keeps its claim on Enter. The user finishes the
statement, presses Enter, and the menu answers instead of the line: the highlighted
suggestion lands at the cursor, or, once the filter has emptied, nothing happens at all.

#1175 fixed the empty half. The other half it structurally cannot reach, because a
grammar-driven completer is never empty: type show tab, Tab to open the
menu, then les; and Enter. After the ; a SQL completer offers
next-statement keywords, so the menu still has values, so Enter still goes to the
menu — and appends table to a finished statement. That is the report behind #1176.

MenuBuilder::with_word_chars bounds a menu's life to the word it was opened for, the
way fish and zsh dismiss their pagers as a word ends.

Why it is a menu setting, and why the caller picks the characters

Not every menu is filtering on a word. A history menu filters on whole command lines,
where a space is ordinary input and ending on it would break multi-word search — so
the setting lives on the menu, and the one that has no word to end simply never sets
it. examples/demo.rs pairs exactly those two menus; there is a test for it.

What ends a word is grammar-specific too. / ends a SQL identifier and sits in the
middle of a path; DefaultCompleter splits on spaces alone. So the caller says: an
alphanumeric character always extends a word, and the setting lists the punctuation
that also does.

// SQL identifiers
ColumnarMenu::default().with_word_chars(Some("_.".into()))

// paths
ColumnarMenu::default().with_word_chars(Some("_-./".into()))

None is the default, and is today's behavior exactly.

Where the close runs

Ahead of the rest of the Edit handling, which matters twice:

  • with quick completions on, the completer is not asked to refilter a word the user
    has already left, only for the answer to be thrown away with the menu
  • an abbreviation expanding on the same space returns out of the edit before the menu
    is looked at again, so a close placed after it would be skipped on the exact
    keystroke the feature is about

Notes

  • Every character in the batch is read, not just the first: a burst of typing arrives
    as one Edit with several InsertChars, and the word can end anywhere in it.
  • Text inserted whole does not end a word, which is how a bracketed paste arrives.
    Without use_bracketed_paste a terminal cannot tell the engine a paste from fast
    typing, and the word ends either way — the doc comment says so.
  • with_persistent_menus still wins.
  • Independent of Don't let a menu with no suggestions swallow Enter #1175: this takes the menu's claim on Enter away before the key is
    pressed, Don't let a menu with no suggestions swallow Enter #1175 guards the key once a menu is open. Either is useful without the other.

Tests

  • the boundary table: space, ; and ) end the word, a letter, _ and . do not
  • the same table over path characters, showing the boundary moves where the caller puts it
  • a menu with no word characters still outlives the word — the default is unchanged
  • a history menu beside a word-bounded completion menu is left alone
  • an abbreviation expanded on the space still ends the word
  • the completer is not asked about a word that has ended
  • a persistent menu is unaffected
  • InsertString is unaffected
  • the word ending inside one batched Edit, which is how the rest of a statement
    arrives when typed at speed
  • the Completion menu stays active for the rest of the line once opened #1176 repro end to end, against a completer that always suggests: show tables;
    runs with no stray word appended

cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings are
clean; the suite passes with default and with all features.

@shreeve
shreeve force-pushed the menu-closes-at-word-boundary branch 3 times, most recently from a0c4213 to fc1d31a Compare September 9, 2026 12:53
A menu opened over a word refilters against everything typed after it, so
it stays active for the rest of the line and keeps its claim on `Enter`.
The user finishes the statement, presses `Enter`, and the menu answers
instead of the line: the highlighted suggestion lands at the cursor, or,
once the filter has emptied, nothing happens at all.

`with_word_chars` bounds a menu's life to the word instead, the way fish
and zsh dismiss their pagers as a word ends. What counts as a word is
grammar-specific: alphanumerics always extend one, and the setting lists
the punctuation that also does. `Some("_.")` suits SQL identifiers,
`Some("_-./")` paths.

It belongs to the menu rather than the engine because a menu filtering on
whole command lines has no word to end. Left unset, as a history menu
would leave it, a menu behaves exactly as it always has.

The close runs ahead of the rest of the edit handling, so a completer is
never asked to refilter a word the user has already left, and an
abbreviation expanding on the same space cannot carry the menu past it.

Only typed characters end a word; text that arrives whole does not.
Comment thread src/engine.rs
return;
};
let word_ended = commands.iter().any(|command| {
matches!(command, EditCommand::InsertChar(c) if menu.settings().word_ends_at(*c))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

InsertNewline does not end the word: only InsertChar is checked here.
The default Alt+Enter and Shift+Enter bindings emit EditCommand::InsertNewline, so a menu opened over th stays active after it and still holds Enter on the next line. In a multi-line statement that is #1176 again.
InsertChar(' ') and InsertChar('\n') both close it.
Count InsertNewline as ending the word.

Comment thread src/menu/mod.rs
/// Only characters typed into the line end a word; text inserted whole,
/// such as a bracketed paste, does not.
#[must_use]
fn with_word_chars(mut self, word_chars: Option<String>) -> Self {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The sibling builders take the bare value: with_input_mode(mode: InputMode), with_output_mode(mode: OutputMode).
Take impl Into<String> and store Some, here and in MenuSettings::with_word_chars at line 305.

@kronberger-droid

Copy link
Copy Markdown
Collaborator

Plus a rebase. Then we are set i think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Completion menu stays active for the rest of the line once opened

2 participants