From f6c05e02d7effb258a6e876a0034a4da69b32392 Mon Sep 17 00:00:00 2001 From: hikmetba-bit Date: Wed, 16 Sep 2026 20:54:30 +0300 Subject: [PATCH] Accept auto-suggestions with 'l' in Vi navigation mode Fixes #2075. The right arrow key (and c-f/c-e) already accept an available fish-style suggestion when the cursor is at the end of the buffer. This adds the same behavior for 'l' in Vi navigation mode, matching what Fish itself does. Vi navigation mode needed its own "at the end" condition: `Document.is_cursor_at_the_end` requires cursor_position == len(text), but navigation mode never lets the cursor move past the last character, so that condition can never be true there for a non-empty buffer. The equivalent state in navigation mode is the cursor sitting on the last character. That also means the accept handler can no longer assume the cursor is already at the true end before inserting: in navigation mode it's one position short (on the last character, not after it), so inserting there would splice the suggestion in before that character instead of appending it. The handler now moves the cursor to the end first (a no-op for the pre-existing emacs/insert-mode case, where it's already there). Co-Authored-By: Claude Sonnet 5 --- .../key_binding/bindings/auto_suggest.py | 23 +++++++++- tests/test_cli.py | 45 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/prompt_toolkit/key_binding/bindings/auto_suggest.py b/src/prompt_toolkit/key_binding/bindings/auto_suggest.py index b487f14ff..6ee6716c4 100644 --- a/src/prompt_toolkit/key_binding/bindings/auto_suggest.py +++ b/src/prompt_toolkit/key_binding/bindings/auto_suggest.py @@ -7,7 +7,7 @@ import re from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import Condition, emacs_mode +from prompt_toolkit.filters import Condition, emacs_mode, vi_navigation_mode from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.key_binding.key_processor import KeyPressEvent @@ -38,9 +38,25 @@ def suggestion_available() -> bool: and app.current_buffer.document.is_cursor_at_the_end ) + @Condition + def suggestion_available_vi_navigation() -> bool: + # In Vi navigation mode, the cursor can never move past the last + # character (unlike insert mode), so `is_cursor_at_the_end` can + # never be true for a non-empty buffer. Being on the last character + # is the navigation-mode equivalent of "at the end" here. + app = get_app() + buffer = app.current_buffer + document = buffer.document + return ( + buffer.suggestion is not None + and len(buffer.suggestion.text) > 0 + and document.cursor_position >= len(document.text) - 1 + ) + @handle("c-f", filter=suggestion_available) @handle("c-e", filter=suggestion_available) @handle("right", filter=suggestion_available) + @handle("l", filter=suggestion_available_vi_navigation & vi_navigation_mode) def _accept(event: E) -> None: """ Accept suggestion. @@ -49,6 +65,11 @@ def _accept(event: E) -> None: suggestion = b.suggestion if suggestion: + # In Vi navigation mode the cursor sits *on* the last character + # rather than past it, so move to the true end before inserting, + # otherwise the suggestion would be spliced in before that + # character instead of appended. + b.cursor_position = len(b.text) b.insert_text(suggestion.text) @handle("escape", "f", filter=suggestion_available & emacs_mode) diff --git a/tests/test_cli.py b/tests/test_cli.py index a876f2993..0ea48bf33 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,10 +5,12 @@ from __future__ import annotations +import asyncio from functools import partial import pytest +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.clipboard import ClipboardData, InMemoryClipboard from prompt_toolkit.enums import EditingMode from prompt_toolkit.filters import ViInsertMode @@ -37,6 +39,7 @@ def _feed_cli_with_input( multiline=False, check_line_ending=True, key_bindings=None, + auto_suggest=None, ): """ Create a Prompt, feed it with the given user input and return the CLI @@ -58,6 +61,7 @@ def _feed_cli_with_input( multiline=multiline, clipboard=clipboard, key_bindings=key_bindings, + auto_suggest=auto_suggest, ) _ = session.prompt() @@ -607,6 +611,47 @@ def test_vi_cursor_movements(): assert result.text == "heXlo" +def test_vi_accept_suggestion_with_l(): + """ + In Vi navigation mode, 'l' should accept an available auto-suggestion, + the same way the right arrow key already does (fish-shell style). + + This can't use `_feed_cli_with_input`: auto-suggestions are computed by + a background asyncio task scheduled on text changes, so the key(s) that + accept the suggestion have to be sent as a separate write, after + yielding to the event loop, or the suggestion never gets computed in + time. + """ + + async def run(after_escape): + history = InMemoryHistory() + history.append_string("hello world") + + with create_pipe_input() as inp: + session = PromptSession( + input=inp, + output=DummyOutput(), + editing_mode=EditingMode.VI, + history=history, + auto_suggest=AutoSuggestFromHistory(), + ) + task = asyncio.ensure_future(session.app.run_async()) + inp.send_text("hello") + await asyncio.sleep(0.05) # Let the suggestion get computed. + inp.send_text("\x1b" + after_escape + "\r") + return await asyncio.wait_for(task, timeout=2) + + # In navigation mode, the cursor sits *on* the last character (unlike + # insert mode), so a single "l" both counts as "at the end" and accepts + # the suggestion. + assert asyncio.run(run("l")) == "hello world" + + # Away from the last character (here, moved to the start with "0"), + # "l" keeps its normal meaning of moving the cursor one position right, + # even though a suggestion is available. + assert asyncio.run(run("0l")) == "hello" + + def test_vi_operators(): feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI)