fix(gateway): let @ completion find folders by name

The fuzzy branch of `complete.path` ranked basenames from
`_list_repo_files`, which lists files only, so a directory was only ever
reachable by typing a `/` — `@Desktop` returned nothing at all. Rank each
ancestor directory alongside the files, and break same-tier ties toward
the folder so `@docs` leads with `docs/` rather than `docs.md`.

Outside a git repo the fallback `os.walk` compounded this: it can spend
the whole `_FUZZY_CACHE_MAX_FILES` budget inside one deep subtree before
reaching a sibling, hiding top-level folders entirely. Seed the scan with
a `listdir` of the root so immediate children are always candidates.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 15:24:35 -05:00
parent 5646fed97e
commit b378cc0a72
2 changed files with 120 additions and 14 deletions

View file

@ -277,3 +277,79 @@ def test_fuzzy_paths_relative_to_cwd_inside_subdir(tmp_path, monkeypatch):
readme_texts = [t for t, _, _ in _items("@README")]
assert not any("README.md" in t for t in readme_texts), readme_texts
# ── Fuzzy DIRECTORY matching ─────────────────────────────────────────────
# `@Desktop` used to return nothing: the fuzzy scanner ranks basenames from
# `_list_repo_files`, which lists FILES only, so a directory whose name no
# file inside it happens to match was unreachable without typing a `/`.
def test_fuzzy_finds_directory_by_name(tmp_path, monkeypatch):
"""A folder is reachable by bare name, with no trailing slash typed."""
monkeypatch.chdir(tmp_path)
(tmp_path / "Desktop" / "nested").mkdir(parents=True)
# Deliberately named so NO file basename fuzzy-matches "Desktop".
(tmp_path / "Desktop" / "nested" / "zzz.txt").write_text("x")
entries = _items("@Desktop")
texts = [t for t, _, _ in entries]
assert "@folder:Desktop/" in texts, texts
row = next(r for r in entries if r[0] == "@folder:Desktop/")
assert row[1] == "Desktop/"
assert row[2] == "dir"
def test_fuzzy_directory_prefix_match(tmp_path, monkeypatch):
"""Partial folder names match too — `@Desk` finds `Desktop/`."""
monkeypatch.chdir(tmp_path)
(tmp_path / "Desktop").mkdir()
(tmp_path / "Desktop" / "zzz.txt").write_text("x")
assert "@folder:Desktop/" in [t for t, _, _ in _items("@Desk")]
def test_fuzzy_ranks_folder_above_file_at_same_tier(tmp_path, monkeypatch):
"""At an equal match tier the folder leads: `@docs` means the directory."""
monkeypatch.chdir(tmp_path)
(tmp_path / "docs").mkdir()
(tmp_path / "docs" / "intro.md").write_text("x")
(tmp_path / "docs.md").write_text("x")
texts = [t for t, _, _ in _items("@docs")]
assert texts[0] == "@folder:docs/", texts
assert "@file:docs.md" in texts
def test_fuzzy_hides_dot_directories_unless_asked(tmp_path, monkeypatch):
"""Dot-folders follow the same rule as dotfiles."""
monkeypatch.chdir(tmp_path)
(tmp_path / ".config").mkdir()
(tmp_path / ".config" / "zzz.txt").write_text("x")
assert not any(".config" in t for t, _, _ in _items("@config"))
assert any(t.startswith("@folder:.config") for t, _, _ in _items("@.config"))
def test_fuzzy_finds_top_level_entries_outside_a_git_repo(tmp_path, monkeypatch):
"""Outside a repo the fallback walk can exhaust its file budget on one
deep subtree before reaching a sibling, hiding top-level folders. The
root listdir seed guarantees immediate children are always candidates.
"""
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(server, "_FUZZY_CACHE_MAX_FILES", 5)
# A deep subtree that soaks up the entire (patched) file budget...
deep = tmp_path / "aaa_hog"
deep.mkdir()
for i in range(40):
(deep / f"f{i:03d}.txt").write_text("x")
# ...and the folder the user actually wants, sorted after it.
(tmp_path / "Desktop").mkdir()
(tmp_path / "Desktop" / "note.txt").write_text("x")
assert "@folder:Desktop/" in [t for t, _, _ in _items("@Desktop")]

View file

@ -16184,24 +16184,54 @@ def _(rid, params: dict) -> dict:
and "/" not in path_part
and prefix_tag != "folder"
):
ranked: list[tuple[tuple[int, int], str, str]] = []
for rel in _list_repo_files(root):
basename = os.path.basename(rel)
if basename.startswith(".") and not path_part.startswith("."):
continue
rank = _fuzzy_basename_rank(basename, path_part)
if rank is None:
continue
ranked.append((rank, rel, basename))
ranked: list[tuple[tuple[int, int], str, str, bool]] = []
walked_dirs: set[str] = set()
seen: set[str] = set()
want_hidden = path_part.startswith(".")
ranked.sort(key=lambda r: (r[0], len(r[1]), r[1]))
def _consider(rel: str, name: str, is_dir: bool) -> None:
if rel in seen or (name.startswith(".") and not want_hidden):
return
rank = _fuzzy_basename_rank(name, path_part)
if rank is not None:
seen.add(rel)
ranked.append((rank, rel, name, is_dir))
# Seed with root's immediate children. `_list_repo_files` is capped
# at _FUZZY_CACHE_MAX_FILES, and outside a git repo the fallback
# walk can burn that whole budget on one deep subtree before ever
# reaching a sibling — which is why `@Desk` in a non-repo $HOME
# found nothing. One listdir keeps the top level always reachable.
try:
for entry in os.listdir(root):
if entry not in _FUZZY_FALLBACK_EXCLUDES:
_consider(entry, entry, os.path.isdir(os.path.join(root, entry)))
except OSError:
pass
for rel in _list_repo_files(root):
_consider(rel, os.path.basename(rel), False)
# Directories are only implied by the file listing, so rank each
# ancestor too. Without this a bare `@Desktop` finds nothing —
# a folder with no name-matching file inside it is invisible to
# a file-only scan, which is the "can't @ a folder by name" bug.
parent = os.path.dirname(rel)
while parent and parent not in walked_dirs:
walked_dirs.add(parent)
_consider(parent, os.path.basename(parent), True)
parent = os.path.dirname(parent)
# Same rank tier: folders first, so `@Desktop` leads with the folder
# rather than a file that merely fuzzy-matches the same letters.
ranked.sort(key=lambda r: (r[0], not r[3], len(r[1]), r[1]))
tag = prefix_tag or "file"
for _, rel, basename in ranked[:30]:
for _, rel, basename, is_dir in ranked[:30]:
items.append(
{
"text": f"@{tag}:{rel}",
"display": basename,
"meta": os.path.dirname(rel),
"text": f"@{'folder' if is_dir else tag}:{rel}{'/' if is_dir else ''}",
"display": basename + ("/" if is_dir else ""),
"meta": "dir" if is_dir else os.path.dirname(rel),
}
)