Merge pull request #47701 from kshitijk4poor/salvage/cli-completer-keystroke-latency
fix(cli): keep typing responsive by running completion off the UI event loop
This commit is contained in:
commit
9901141d64
9
cli.py
9
cli.py
@ -12024,6 +12024,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||||||
|
|
||||||
# Create the input area with multiline (Alt+Enter), autocomplete, and paste handling
|
# Create the input area with multiline (Alt+Enter), autocomplete, and paste handling
|
||||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
|
from prompt_toolkit.completion import ThreadedCompleter
|
||||||
|
|
||||||
|
|
||||||
_completer = SlashCommandCompleter(
|
_completer = SlashCommandCompleter(
|
||||||
@ -12039,7 +12040,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||||||
wrap_lines=True,
|
wrap_lines=True,
|
||||||
read_only=Condition(lambda: bool(cli_ref._command_running)),
|
read_only=Condition(lambda: bool(cli_ref._command_running)),
|
||||||
history=FileHistory(str(self._history_file)),
|
history=FileHistory(str(self._history_file)),
|
||||||
completer=_completer,
|
# complete_while_typing fires the completer on every keystroke. The
|
||||||
|
# completer does blocking work — fuzzy @-file indexing shells out to
|
||||||
|
# rg/fd (up to a 2s timeout) and path completion hits os.listdir/stat
|
||||||
|
# — so running it inline would stall the render loop on each key (very
|
||||||
|
# noticeable on WSL2/slow filesystems). ThreadedCompleter moves it off
|
||||||
|
# the UI event loop, keeping typing responsive.
|
||||||
|
completer=ThreadedCompleter(_completer),
|
||||||
complete_while_typing=True,
|
complete_while_typing=True,
|
||||||
auto_suggest=SlashCommandAutoSuggest(
|
auto_suggest=SlashCommandAutoSuggest(
|
||||||
history_suggest=AutoSuggestFromHistory(),
|
history_suggest=AutoSuggestFromHistory(),
|
||||||
|
|||||||
@ -1280,6 +1280,10 @@ class SlashCommandCompleter(Completer):
|
|||||||
current word doesn't look like a path. A word is path-like when
|
current word doesn't look like a path. A word is path-like when
|
||||||
it starts with ``./``, ``../``, ``~/``, ``/``, or contains a
|
it starts with ``./``, ``../``, ``~/``, ``/``, or contains a
|
||||||
``/`` separator (e.g. ``src/main.py``).
|
``/`` separator (e.g. ``src/main.py``).
|
||||||
|
|
||||||
|
Tokens containing a ``://`` scheme separator (e.g. URLs like
|
||||||
|
``https://example.com/x``) are excluded even though they contain a
|
||||||
|
``/`` — they are never useful local-path completions.
|
||||||
"""
|
"""
|
||||||
if not text:
|
if not text:
|
||||||
return None
|
return None
|
||||||
@ -1291,6 +1295,12 @@ class SlashCommandCompleter(Completer):
|
|||||||
word = text[i + 1:]
|
word = text[i + 1:]
|
||||||
if not word:
|
if not word:
|
||||||
return None
|
return None
|
||||||
|
# URLs contain "/" but are not local paths. Treating them as paths fires
|
||||||
|
# os.listdir on every keystroke while typing/pasting a link (e.g. an
|
||||||
|
# https:// URL becomes a listdir of "https:") — pure latency, never a
|
||||||
|
# useful completion. Skip any token with a scheme separator.
|
||||||
|
if "://" in word:
|
||||||
|
return None
|
||||||
# Only trigger path completion for path-like tokens
|
# Only trigger path completion for path-like tokens
|
||||||
if word.startswith(("./", "../", "~/", "/")) or "/" in word:
|
if word.startswith(("./", "../", "~/", "/")) or "/" in word:
|
||||||
return word
|
return word
|
||||||
|
|||||||
@ -59,6 +59,32 @@ class TestExtractPathWord:
|
|||||||
def test_just_tilde_slash(self):
|
def test_just_tilde_slash(self):
|
||||||
assert SlashCommandCompleter._extract_path_word("~/") == "~/"
|
assert SlashCommandCompleter._extract_path_word("~/") == "~/"
|
||||||
|
|
||||||
|
def test_url_is_not_treated_as_path(self):
|
||||||
|
# A URL contains "/" so the bare slash heuristic would otherwise return
|
||||||
|
# it as a path word, firing os.listdir("https:") on every keystroke.
|
||||||
|
assert SlashCommandCompleter._extract_path_word("see https://paste.rs/abc") is None
|
||||||
|
|
||||||
|
def test_http_url_is_not_treated_as_path(self):
|
||||||
|
assert SlashCommandCompleter._extract_path_word("ref http://example.com/x") is None
|
||||||
|
|
||||||
|
def test_scheme_alone_is_enough_to_reject(self):
|
||||||
|
# The "://" scheme separator is the signal, even before any path part
|
||||||
|
# has been typed.
|
||||||
|
assert SlashCommandCompleter._extract_path_word("ssh://host") is None
|
||||||
|
|
||||||
|
def test_path_word_with_colon_but_no_scheme_still_resolves(self):
|
||||||
|
# Only the "://" scheme separator should reject; a bare colon inside a
|
||||||
|
# real path token must not regress path detection.
|
||||||
|
assert (
|
||||||
|
SlashCommandCompleter._extract_path_word("open ./a:b/c.py") == "./a:b/c.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ordinary_path_unaffected_by_url_guard(self):
|
||||||
|
assert (
|
||||||
|
SlashCommandCompleter._extract_path_word("edit src/pkg/mod.py")
|
||||||
|
== "src/pkg/mod.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestPathCompletions:
|
class TestPathCompletions:
|
||||||
def test_lists_current_directory(self, tmp_path):
|
def test_lists_current_directory(self, tmp_path):
|
||||||
@ -155,6 +181,23 @@ class TestIntegration:
|
|||||||
completions = list(completer.get_completions(doc, event))
|
completions = list(completer.get_completions(doc, event))
|
||||||
assert completions == []
|
assert completions == []
|
||||||
|
|
||||||
|
def test_url_does_not_touch_filesystem(self, completer, monkeypatch):
|
||||||
|
# Regression for laggy typing: a URL token contains "/", so before the
|
||||||
|
# scheme guard it reached _path_completions and called os.listdir on
|
||||||
|
# every keystroke. Assert no completions AND that the filesystem is
|
||||||
|
# never touched while a URL is under the cursor.
|
||||||
|
import hermes_cli.commands as commands_mod
|
||||||
|
|
||||||
|
def _fail(*_args, **_kwargs):
|
||||||
|
raise AssertionError("os.listdir must not run for a URL token")
|
||||||
|
|
||||||
|
monkeypatch.setattr(commands_mod.os, "listdir", _fail)
|
||||||
|
|
||||||
|
text = "open https://paste.rs/abc"
|
||||||
|
doc = Document(text, cursor_position=len(text))
|
||||||
|
event = MagicMock()
|
||||||
|
assert list(completer.get_completions(doc, event)) == []
|
||||||
|
|
||||||
def test_absolute_path_triggers_completion(self, completer):
|
def test_absolute_path_triggers_completion(self, completer):
|
||||||
doc = Document("check /etc/hos", cursor_position=14)
|
doc = Document("check /etc/hos", cursor_position=14)
|
||||||
event = MagicMock()
|
event = MagicMock()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user