add workspace grep engine + search.grep/cancel, editor.open --line

The pure-Dart content-search engine behind find-in-files (D-79): walks
the ignore-pruned workspace, fans files across worker isolates
(Isolate.run) for parallelism, matches each line with a literal
indexOf fast-path or a RegExp, and streams match batches with
cooperative cancellation. No ripgrep dependency; the search.grep IPC
contract is engine-agnostic so an rg accelerator can slot in later.

search.grep returns a searchId and streams search.match / search.done
(or search.error) events, mirroring files.watch; search.cancel stops
an in-flight search. The service reuses the files service's resolved
ignore set so both honour the same ignore_files: layering.

editor.open gains an optional 1-based line argument: it converts the
line to a byte offset and sets the initial selection, enabling
click-to-line from search results.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 20:31:18 +02:00
co-authored by Claude Opus 4.8
parent 0c7a6e86d5
commit 399a4d3a3f
10 changed files with 824 additions and 0 deletions
+22
View File
@@ -47,6 +47,28 @@ void main() {
expect(act['path'], 'doc.md');
});
test('editor.open with a 1-based line jumps the initial selection (T-52)', () async {
await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n');
final r = await call('editor.open', {'path': 'multi.txt', 'line': 3});
expect(r.ok, isTrue);
final sel = r.data['selection'] as Map;
// Line 3 starts after 'one\n' + 'two\n' = 8 characters.
expect(sel['start'], 8);
expect(sel['end'], 8);
});
test('editor.open without a line opens at the top', () async {
await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\n');
final r = await call('editor.open', {'path': 'multi.txt'});
expect((r.data['selection'] as Map)['start'], 0);
});
test('editor.open with an out-of-range line clamps to the content end', () async {
await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\n'); // 8 chars
final r = await call('editor.open', {'path': 'multi.txt', 'line': 999});
expect((r.data['selection'] as Map)['start'], 8);
});
test('editor.insert without id targets the active buffer', () async {
await call('editor.open', {'path': 'doc.md'});
final r = await call('editor.insert', {'text': 'X '});