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
23 changes: 22 additions & 1 deletion src/prompt_toolkit/key_binding/bindings/auto_suggest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -58,6 +61,7 @@ def _feed_cli_with_input(
multiline=multiline,
clipboard=clipboard,
key_bindings=key_bindings,
auto_suggest=auto_suggest,
)

_ = session.prompt()
Expand Down Expand Up @@ -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)

Expand Down
Loading