Skip to content

Commit 392f09a

Browse files
committed
feat(main): replace glob with git ls-files for file completions
1 parent 788f578 commit 392f09a

2 files changed

Lines changed: 47 additions & 15 deletions

File tree

iclaw/main.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python3
2-
import glob
32
import json
43
import os
54
import re
5+
import subprocess
66
import sys
77
import time
88
from pathlib import Path
@@ -18,6 +18,23 @@
1818
from iclaw.commands.search_provider import handle_search_provider_command
1919
from iclaw.commands.utils import handle_copy_command
2020

21+
22+
def _get_git_files():
23+
"""Return files from git ls-files, natively respecting .gitignore."""
24+
try:
25+
result = subprocess.run(
26+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
27+
capture_output=True,
28+
text=True,
29+
timeout=5,
30+
)
31+
if result.returncode == 0:
32+
return result.stdout.splitlines()
33+
except (FileNotFoundError, subprocess.TimeoutExpired):
34+
pass
35+
return []
36+
37+
2138
COMMANDS = [
2239
"/model_provider",
2340
"/model",
@@ -39,15 +56,13 @@ def get_completions(self, document, complete_event):
3956
if at_pos != -1:
4057
prefix = text[at_pos + 1 :]
4158
if " " not in prefix:
42-
pattern = f"{prefix}*"
43-
matches = glob.glob(pattern) + glob.glob(
44-
os.path.join("**", pattern), recursive=True
45-
)
46-
seen = set()
47-
for path in sorted(matches)[:20]:
48-
if path in seen:
49-
continue
50-
seen.add(path)
59+
all_files = _get_git_files()
60+
matches = [f for f in all_files if prefix.lower() in f.lower()]
61+
count = 0
62+
for path in sorted(matches):
63+
if count >= 20:
64+
break
65+
count += 1
5166
meta = "dir" if os.path.isdir(path) else "file"
5267
yield Completion(
5368
path,

tests/test_at_mention.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,27 @@ def test_unreadable_file_skipped(self):
6262
self.assertEqual(result, text)
6363

6464

65+
TEST_FILES = ["alpha.py", "alpha_test.py", "beta.txt", "subdir/gamma.py"]
66+
67+
6568
class TestIclawCompleter(unittest.TestCase):
6669
def setUp(self):
6770
self.completer = IclawCompleter()
6871
self.tmpdir = tempfile.mkdtemp()
6972
self.orig_cwd = os.getcwd()
7073
os.chdir(self.tmpdir)
71-
# Create test files in the temp dir
74+
# Create actual files so os.path.isdir/isfile work in meta checks
7275
Path("alpha.py").write_text("")
7376
Path("alpha_test.py").write_text("")
7477
Path("beta.txt").write_text("")
7578
os.makedirs("subdir", exist_ok=True)
7679
Path("subdir/gamma.py").write_text("")
80+
# Patch _get_git_files so tests don't require a real git repo
81+
self.patcher = patch("iclaw.main._get_git_files", return_value=TEST_FILES)
82+
self.patcher.start()
7783

7884
def tearDown(self):
85+
self.patcher.stop()
7986
os.chdir(self.orig_cwd)
8087

8188
def _completions(self, text):
@@ -121,12 +128,22 @@ def test_at_with_space_after_at_returns_nothing(self):
121128
self.assertEqual(completions, [])
122129

123130
def test_at_limits_to_20_results(self):
124-
# Create 25 files
125-
for i in range(25):
126-
Path(f"zfile{i:02d}.py").write_text("")
127-
completions = self._completions("@zfile")
131+
many_files = [f"zfile{i:02d}.py" for i in range(25)]
132+
with patch("iclaw.main._get_git_files", return_value=many_files):
133+
completions = self._completions("@zfile")
128134
self.assertLessEqual(len(completions), 20)
129135

136+
def test_at_excludes_gitignored_files(self):
137+
# git ls-files naturally excludes ignored files; simulate this by
138+
# returning only non-ignored files from _get_git_files.
139+
clean_files = ["visible.py", "README.md"]
140+
with patch("iclaw.main._get_git_files", return_value=clean_files):
141+
completions = self._completions("@")
142+
paths = [c.text for c in completions]
143+
self.assertNotIn("visible.pyc", paths)
144+
self.assertNotIn("__pycache__", paths)
145+
self.assertIn("visible.py", paths)
146+
130147
# --- / command completion ---
131148

132149
def test_slash_alone_returns_all_commands(self):

0 commit comments

Comments
 (0)