From 95f41f0eccd27d2823ffd49bc6cd9bf01fadcc4e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 15:25:11 -0300 Subject: Align search and LSP context locations flush left --- docs/config.md | 2 +- src/locations.zig | 8 +++----- src/panes.zig | 8 +++++--- src/pardes.zig | 2 +- test/output.zig | 6 +++--- test/panes.zig | 32 +++++++++++++++++++++++++------- 6 files changed, 38 insertions(+), 20 deletions(-) diff --git a/docs/config.md b/docs/config.md index 5a7a358f..3a6d00d2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -154,7 +154,7 @@ LocationsConfig context:5 tscontext:on tslocations:off without their locations visible. Result locations are padded so the source text aligns, preserving the source's -indentation. Context rows begin with two spaces; `n` and `N` still stop on matches. +indentation. All visible locations start flush left; `n` and `N` still stop on matches. With `tscontext` enabled, asterisks after a match location show its declaration depth (for example, `main.zig:42 ***`). Ordinary neighboring source lines retain syntax colors; declaration headers do not. diff --git a/src/locations.zig b/src/locations.zig index 61bbbd1a..5e588dbe 100644 --- a/src/locations.zig +++ b/src/locations.zig @@ -5,10 +5,10 @@ const syntax = @import("syntax.zig"); const filesystem = @import("fs.zig"); pub const Config = @import("locations_config.zig").Config; -pub const context_prefix = " "; - +/// Legacy marked rows and hidden locations remain recognizable without metadata. +/// Visible context locations are flush left and use Row.kind instead. pub fn isContextLine(line: []const u8) bool { - return std.mem.startsWith(u8, line, context_prefix) or std.mem.startsWith(u8, line, "| "); + return std.mem.startsWith(u8, line, " ") or std.mem.startsWith(u8, line, "| "); } pub const Row = struct { @@ -226,7 +226,6 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us for (pending.items, 0..) |row, index| { const before = out.written().len; const context = row.original == null; - if (context) try out.writer.writeAll(context_prefix); try out.writer.writeAll(row.label); const location_end = if (row.hidden) 0 else out.written().len - before; if (row.depth > 0) { @@ -252,7 +251,6 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us fn labelWidth(row: Pending) usize { return pardes.panes.File.displayWidth(row.label) + - @as(usize, if (row.original == null) context_prefix.len else 0) + @as(usize, if (row.depth > 0) row.depth + 1 else 0); } diff --git a/src/panes.zig b/src/panes.zig index e21b1dc4..09225068 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -1095,7 +1095,7 @@ pub const File = struct { if (row.path.len == 0 or row.path.len >= 4096 or row.location_end > row.code_start or row.code_start == 0 or row.code_start > line.len or line[row.code_start - 1] != '\t' or - (row.kind == .context) != locations.isContextLine(line) or + (row.kind == .match and row.location_end == 0) or (row.declaration and row.kind != .context) or row.line > std.math.maxInt(i32) or row.col > std.math.maxInt(i32) or row.end_line > std.math.maxInt(i32) or row.end_col > std.math.maxInt(i32)) @@ -2442,13 +2442,15 @@ pub const Output = struct { return mapped; } - pub fn nextResult(content: []const u8, path: []const u8, at: look.Spot) usize { + pub fn nextResult(content: []const u8, rows: []const locations.Row, path: []const u8, at: look.Spot) usize { const current: Location = .{ .path = path, .at = at, .end = 0 }; var lines = std.mem.splitScalar(u8, content, '\n'); var first: ?usize = null; var row: usize = 0; while (lines.next()) |line| : (row += 1) { - if (locations.isContextLine(line)) continue; + if (row < rows.len) { + if (rows[row].kind != .match) continue; + } else if (locations.isContextLine(line)) continue; const target = location(line); if (target.at.line == 0) continue; if (first == null) first = row; diff --git a/src/pardes.zig b/src/pardes.zig index 51c9c3db..40312770 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -9551,7 +9551,7 @@ pub const Pardes = struct { if (panes.Output.traits(from).jumps) { const result = p.panes[pane.search_pane orelse return] orelse return; const file = pane.file orelse return; - const row = panes.Output.nextResult(result.file.?.content, lsp.rel(dir, file.path), .{ + const row = panes.Output.nextResult(result.file.?.content, result.file.?.location_rows, lsp.rel(dir, file.path), .{ .line = @as(usize, @intCast(@max(0, w.row))) + 1, .col = @as(usize, @intCast(@max(0, w.col))) + 1, }); diff --git a/test/output.zig b/test/output.zig index 42b1ce7c..e835a474 100644 --- a/test/output.zig +++ b/test/output.zig @@ -287,9 +287,9 @@ test "Find sorts whole filenames and next location uses numeric columns" { try std.testing.expectEqualStrings("x:10\nx:2\n", result.file.?.content); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); const rows = "with space.zig:2:2-3 first\nwith space.zig:2:10-12 next\nwith space.zig:10:1 later"; - try std.testing.expectEqual(@as(usize, 1), panes.Output.nextResult(rows, "with space.zig", .{ .line = 2, .col = 3 })); - try std.testing.expectEqual(@as(usize, 2), panes.Output.nextResult(rows, "with space.zig", .{ .line = 2, .col = 10 })); - try std.testing.expectEqual(@as(usize, 0), panes.Output.nextResult(rows, "with space.zig", .{ .line = 10, .col = 1 })); + try std.testing.expectEqual(@as(usize, 1), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 2, .col = 3 })); + try std.testing.expectEqual(@as(usize, 2), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 2, .col = 10 })); + try std.testing.expectEqual(@as(usize, 0), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 10, .col = 1 })); } test "result stepping reaches an unterminated last row" { diff --git a/test/panes.zig b/test/panes.zig index 72d83ce9..9972df84 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -2001,7 +2001,7 @@ const OutputTests = struct { matches[count] = i; count += 1; } else { - try std.testing.expect(std.mem.startsWith(u8, line, " ")); + if (row.location_end > 0) try std.testing.expect(std.mem.startsWith(u8, line, path)); try std.testing.expect(!panes.Output.isResultRow(file, i)); try std.testing.expectEqualStrings(modal.lineSlice(source, expected_line - 1), line[row.code_start..]); } @@ -2069,15 +2069,15 @@ const OutputTests = struct { try std.testing.expect(pardes.test_api.searchStep(restored, 0, -1)); try std.testing.expectEqual(matches[0], restored_source.search_row.?); - // Once an edit invalidates source metadata, the explicit marker still - // prevents context declarations from becoming search destinations. + // Editing drops source metadata. Hidden declarations remain skipped, + // while flush-left visible context becomes an ordinary location. panes.File.setContent(p, file, try p.gpa.dupe(u8, file.content)); try std.testing.expectEqual(@as(usize, 0), file.location_rows.len); pane.search_row = null; try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); - try std.testing.expectEqual(matches[0], pane.search_row.?); + try std.testing.expectEqual(@as(usize, 2), pane.search_row.?); try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); - try std.testing.expectEqual(matches[1], pane.search_row.?); + try std.testing.expectEqual(matches[0], pane.search_row.?); } test "Locations visible declarations are muted and stars count scopes rather than signature lines" { @@ -2119,11 +2119,29 @@ const OutputTests = struct { } } else if (row.at.line <= 5) { try std.testing.expect(row.declaration); - try std.testing.expect(row.location_end > 2); - try std.testing.expect(std.mem.startsWith(u8, line, " ")); + try std.testing.expect(row.location_end > 0); + try std.testing.expect(std.mem.startsWith(u8, line, path)); + try std.testing.expectEqualStrings(path, panes.Output.location(line).path); } } try std.testing.expectEqual(@as(usize, 2), matches); + const next = panes.Output.nextResult(file.content, file.location_rows, path, .{ .line = 2, .col = 5 }); + try std.testing.expectEqual(@as(usize, 7), file.location_rows[next].at.line); + try std.testing.expectEqual(.match, file.location_rows[next].kind); + const wrapped = panes.Output.nextResult(file.content, file.location_rows, path, .{ .line = 99 }); + try std.testing.expectEqual(@as(usize, 2), file.location_rows[wrapped].at.line); + try p.dumpState(); + const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?); + defer restored.deinit(); + const restored_file = &restored.panes[pane.search_pane.?].?.file.?; + try std.testing.expectEqualStrings(file.content, restored_file.content); + try std.testing.expectEqual(file.location_rows.len, restored_file.location_rows.len); + for (file.location_rows, restored_file.location_rows) |before, after| { + try std.testing.expectEqual(before.kind, after.kind); + try std.testing.expectEqual(before.declaration, after.declaration); + try std.testing.expectEqual(before.location_end, after.location_end); + } + try std.testing.expectEqual(next, panes.Output.nextResult(restored_file.content, restored_file.location_rows, path, .{ .line = 2, .col = 5 })); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); result.scrollBy(-1000); -- cgit v1.3