Add project text search panel

This commit is contained in:
slhx agent
2026-06-21 03:06:12 +02:00
parent 233a5245f6
commit fc3b6a7d08
3 changed files with 239 additions and 1 deletions
+86
View File
@@ -125,6 +125,9 @@ pub const Client = struct {
if (std.mem.eql(u8, line, "file_tree")) return self.openRepoList("tree", true, false);
if (std.mem.eql(u8, line, "file_tree all")) return self.openRepoList("tree", true, true);
if (std.mem.eql(u8, line, "file_open_selected")) return self.openSelectedRepoFile();
if (std.mem.startsWith(u8, line, "search_text all ")) return self.openSearchList(line[16..], true);
if (std.mem.startsWith(u8, line, "search_text ")) return self.openSearchList(line[12..], false);
if (std.mem.eql(u8, line, "search_open_selected")) return self.openSelectedSearchResult();
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
@@ -307,6 +310,25 @@ pub const Client = struct {
self.message = null;
}
fn openSearchList(self: *Client, query: []const u8, include_ignored: bool) !void {
const rows = self.repo.searchRowsAlloc(self.allocator, query, include_ignored) catch return Error.ProtocolRejected;
defer {
for (rows) |row| self.allocator.free(row);
self.allocator.free(rows);
}
try self.session.openListPanel("search", rows);
self.message = null;
}
fn openSelectedSearchResult(self: *Client) !void {
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
const match = self.repo.searchOffsetFromRow(selected) catch return Error.ProtocolRejected;
const content = self.repo.content(match.path) catch return Error.ProtocolRejected;
try self.session.openFixtureAt(content, match.offset);
try self.session.closePanel();
self.message = null;
}
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
if (std.mem.eql(u8, key_name, "space")) return self.handleInput(" ");
if (std.mem.eql(u8, key_name, "enter")) return self.handleInput("\r");
@@ -1032,3 +1054,67 @@ test "adversarial: invalid repo paths and missing selections fail without corrup
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "safe") != null);
}
test "regular: project text search lists matches filters results and jumps to match" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
defer client.deinit();
try client.handleTraceLine("repo_file src/main.zig=abc_needle");
try client.handleTraceLine("repo_file src/lib.zig=needle_lib");
try client.handleTraceLine("search_text needle");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "src/main.zig:1:5") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "src/lib.zig:1:1") != null);
}
try client.handleTraceLine("list_filter lib");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "src/lib.zig:1:1") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "src/main.zig") == null);
}
try client.handleTraceLine("search_open_selected");
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("needle_lib", snap.bytes);
try std.testing.expectEqual(@as(usize, 0), snap.cursor_byte);
}
test "regular: project text search respects ignored files unless include ignored is explicit" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 5 });
defer client.deinit();
try client.handleTraceLine("repo_gitignore zig-out/");
try client.handleTraceLine("repo_file src/main.zig=needle");
try client.handleTraceLine("repo_file zig-out/log.txt=needle");
try client.handleTraceLine("search_text needle");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "src/main.zig") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "zig-out") == null);
}
try client.handleTraceLine("panel_close");
try client.handleTraceLine("search_text all needle");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "zig-out/log.txt") != null);
}
}
test "adversarial: project text search failures do not corrupt current buffer" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 5 });
defer client.deinit();
try client.handleTraceLine("open safe");
try client.handleTraceLine("repo_file src/main.zig=needle");
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("search_text two words"));
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("search_open_selected"));
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("safe", snap.bytes);
}