Skip to content

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

Merged
kronberger-droid merged 1 commit into
nushell:mainfrom
shreeve:menu-closes-at-word-boundary
Sep 22, 2026
Merged

kronberger-droid merged 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 4 times, most recently from fc1d31a to 1134947 Compare September 12, 2026 02:27
Comment thread src/engine.rs Outdated
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. InsertNewline now ends the word too, in the same match as InsertChar, and there's a test for it: a menu opened over th is closed by Edit(vec![InsertNewline]).

Comment thread src/menu/mod.rs Outdated
/// 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Both MenuBuilder::with_word_chars and MenuSettings::with_word_chars take impl Into<String> and store Some, matching with_input_mode and the others. The four call sites and the docs are updated.

@kronberger-droid

Copy link
Copy Markdown
Collaborator

Plus a rebase. Then we are set i think.

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. `"_."` suits SQL identifiers, `"_-./"`
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. A
newline ends it whether it comes as `InsertChar('\n')` or as
`InsertNewline`, the command the default `Alt+Enter` and `Shift+Enter`
bindings emit, so a menu cannot outlive the first line of a multi-line
statement and hold `Enter` on the next.
@shreeve
shreeve force-pushed the menu-closes-at-word-boundary branch from 1134947 to ad4a916 Compare September 22, 2026 00:47
@shreeve

shreeve commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and both points addressed. CI is green.

@kronberger-droid
kronberger-droid merged commit c3931be into nushell:main Sep 22, 2026
7 checks passed
@shreeve

shreeve commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Awesome! Thanks Martin

@kronberger-droid

Copy link
Copy Markdown
Collaborator

@shreeve Sure thing.
Thanks for your work!

fdncred pushed a commit to nushell/nushell that referenced this pull request Sep 23, 2026
## Description

Bumps reedline to `db34d84`, seven commits past the previous pin
`c9e7035`:

- nushell/reedline#1214 `feat(engine)!: switch between edit modes with
one SwitchMode event`
- nushell/reedline#1226 `fix(engine): Up and Down report whether they
moved anything`
- nushell/reedline#1225 `refactor(editor): give paste-after the same
shape as paste-before`
- nushell/reedline#1223 `chore(deps): drop fd-lock, itertools, thiserror
and unicase`
- nushell/reedline#1209 `feat(menu): let a menu close at the end of the
word it was opened for`
- nushell/reedline#1206 `fix(painter): trust the measured cursor row
over a short terminal size`
- nushell/reedline#1222 `fix(ci): change typos to v1`

#1214 replaces `ViChangeMode` and `HelixChangeMode` with one
`SwitchMode` event and gives vi visual mode its own keybinding table and
cursor slot.
Both old names still parse and mean the same, so existing configs keep
working.

## User-facing changes (Release notes)

- Added the `SwitchMode` keybinding event, `{ send: SwitchMode, mode:
vi_normal }`, taking the same mode names `mode` takes on a keybinding. A
switch into the mode already active reports itself inapplicable, so an
`until` list falls through to its next event where it used to stop;
`ViChangeMode` and `HelixChangeMode` share that.
- Added `vi_visual` as a keybinding mode and
`$env.config.cursor_shape.vi_visual`, which follows `vi_normal` while
left on `inherit`. Bindings for `vi_normal` no longer apply in visual
mode.
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