From 505dedb2459e7b51cbdb0c066698c6e5596d19dd Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 18:33:12 -0300 Subject: Bound preceding result context by its outermost tree-sitter declaration --- docs/config.md | 3 ++- src/locations.zig | 16 ++++++++------ test/output.zig | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/docs/config.md b/docs/config.md index f369d798..5b5641ba 100644 --- a/docs/config.md +++ b/docs/config.md @@ -157,7 +157,8 @@ LocationsConfig context:5 tscontext:on tslocations:off `0`). Overlapping context is shown once. These neighboring lines omit their locations and keep their code aligned with the matching result. - `tscontext` includes enclosing tree-sitter declaration headers in source - order (default `off`). + order (default `off`). Preceding neighboring context stops at the outermost + enclosing declaration, keeping unrelated lines above it out of the result. - `tslocations` shows a location on the first line of each declaration header (default `on`); continuation lines keep the same alignment without repeating the location. Declaration headers use the same muted color as locations, with or diff --git a/src/locations.zig b/src/locations.zig index 34d1ad14..0f580e34 100644 --- a/src/locations.zig +++ b/src/locations.zig @@ -192,16 +192,11 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us for (pending.items[group_start..]) |*match| { const row = match.at.line -| 1; if (row >= count) continue; - const lo = row -| p.locations_config.context; - const hi = @min(count -| 1, row +| p.locations_config.context); - var context_row = lo; - while (context_row <= hi) : (context_row += 1) { - if (context_row == row) continue; - wanted[context_row] |= neighbor_context; - } + var enclosing_start: ?usize = null; for (declarations) |declaration| { if (declaration.start_line > row) break; if (declaration.end_line < row) continue; + enclosing_start = @min(enclosing_start orelse declaration.start_line, declaration.start_line); match.depth += 1; var header = declaration.start_line; while (header <= declaration.header_end_line and header < row) : (header += 1) { @@ -209,6 +204,13 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us if (header == declaration.start_line) wanted[header] |= declaration_start; } } + const lo = @max(row -| p.locations_config.context, enclosing_start orelse 0); + const hi = @min(count -| 1, row +| p.locations_config.context); + var context_row = lo; + while (context_row <= hi) : (context_row += 1) { + if (context_row == row) continue; + wanted[context_row] |= neighbor_context; + } } for (wanted, 0..) |kind, row| { if (kind > 0) { diff --git a/test/output.zig b/test/output.zig index ede8e7dd..c162c1d1 100644 --- a/test/output.zig +++ b/test/output.zig @@ -1381,3 +1381,67 @@ test "LSP occurrence tint follows the supplied source span through context" { try std.testing.expectEqualStrings(text, source.file.?.content); } } + +test "result context upper bound stops at earliest enclosing declaration" { + if (!pardes.syntax.enabled) return error.SkipZigTest; + const text = "// unrelated prelude\n" ++ + "const unrelated = 1;\n" ++ + "\n" ++ + "const Outer = struct {\n" ++ + " pub fn run(\n" ++ + " value: u32,\n" ++ + " ) void {\n" ++ + " _ = value;\n" ++ + " }\n" ++ + "};\n" ++ + "// trailing context remains available"; + for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { + for ([_]bool{ false, true }) |tscontext| { + for ([_]bool{ false, true }) |visible| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const source = try p.setTestFile(text); + p.gpa.free(source.file.?.path); + source.file.?.path = try p.gpa.dupe(u8, "/bounded-context.zig"); + p.locations_config = .{ .context = 100, .tscontext = tscontext, .tslocations = visible }; + const input = try std.fmt.allocPrint(p.gpa, "{s}:8:13 {s}\n", .{ source.file.?.path, modal.lineSlice(text, 7) }); + try panes.Output.fillResults(p, 0, "/", origin, "upper bound", input, 0); + const file = &p.panes[source.search_pane.?].?.file.?; + const first: usize = if (tscontext) 4 else 1; + try std.testing.expectEqual(first, file.location_rows[0].at.line); + try std.testing.expectEqual(@as(usize, 11), file.location_rows[file.location_rows.len - 1].at.line); + try std.testing.expectEqual(@as(usize, 8) - first, source.search_row.?); + var matches: usize = 0; + for (file.location_rows, 0..) |row, index| { + try std.testing.expectEqual(first + index, row.at.line); + try std.testing.expectEqualStrings(modal.lineSlice(text, row.at.line - 1), modal.lineSlice(file.content, index)[row.code_start..]); + if (row.kind == .match) { + try std.testing.expectEqual(@as(usize, 8), row.at.line); + matches += 1; + } + } + try std.testing.expectEqual(@as(usize, 1), matches); + try std.testing.expectEqualStrings(text, source.file.?.content); + } + } + } + + // A top-level comment has no enclosing declaration, and a plain-text + // file has no grammar. Neither should lose its preceding ordinary rows. + for ([_][]const u8{ "/no-ancestor.zig", "/no-grammar.txt" }) |path| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const plain = "// first\n// second\n// matched\n// trailing"; + const source = try p.setTestFile(plain); + p.gpa.free(source.file.?.path); + source.file.?.path = try p.gpa.dupe(u8, path); + p.locations_config = .{ .context = 100, .tscontext = true, .tslocations = false }; + const input = try std.fmt.allocPrint(p.gpa, "{s}:3:4 // matched\n", .{path}); + try panes.Output.fillResults(p, 0, "/", .search, "unbounded", input, 0); + const file = &p.panes[source.search_pane.?].?.file.?; + try std.testing.expectEqual(@as(usize, 4), file.location_rows.len); + try std.testing.expectEqual(@as(usize, 1), file.location_rows[0].at.line); + try std.testing.expectEqual(@as(usize, 4), file.location_rows[3].at.line); + try std.testing.expectEqual(@as(usize, 2), source.search_row.?); + } +} -- cgit v1.3