summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 15:11:14 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12 (patch)
treeb3102012a030560431a9360840e80a5e7316cb1e
parent40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (diff)
downloadpardes-7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12.tar.gz
pardes-7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12.zip
Mute result declaration context and show nesting after locations
-rw-r--r--docs/config.md8
-rw-r--r--src/dump.zig1
-rw-r--r--src/locations.zig56
-rw-r--r--src/panes.zig20
-rw-r--r--src/syntax.zig41
-rw-r--r--test/panes.zig86
6 files changed, 188 insertions, 24 deletions
diff --git a/docs/config.md b/docs/config.md
index 8a8f1d23..53c76087 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -150,10 +150,14 @@ LocationsConfig context:5 tscontext:on tslocations:off
- `tscontext` includes enclosing tree-sitter declaration headers in source
order (default `off`).
- `tslocations` shows locations beside those declaration headers (default
- `on`). Turning it off keeps the headers' syntax colors.
+ `on`). Declaration headers use the same muted color as locations, with or
+ without their locations visible.
Result locations are padded so the source text aligns, preserving the source's
-indentation. Context rows begin with `| `; `n` and `N` still stop on matches.
+indentation. Context rows begin with two spaces; `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.
Open file buffers supply context from their current edits. Missing files still
leave the original result available. These settings apply to subsequent result
generation and survive `Dump`/`Restore`. Invalid fields reject the entire
diff --git a/src/dump.zig b/src/dump.zig
index e82a8ab3..0c538a1a 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -76,6 +76,7 @@ pub const Terminal = struct {
pub const LocationRow = struct {
kind: enum { match, context },
+ declaration: bool = false,
path: []const u8,
line: usize,
col: usize = 0,
diff --git a/src/locations.zig b/src/locations.zig
index 8f8546f6..61bbbd1a 100644
--- a/src/locations.zig
+++ b/src/locations.zig
@@ -5,10 +5,15 @@ const syntax = @import("syntax.zig");
const filesystem = @import("fs.zig");
pub const Config = @import("locations_config.zig").Config;
-pub const context_prefix = "| ";
+pub const context_prefix = " ";
+
+pub fn isContextLine(line: []const u8) bool {
+ return std.mem.startsWith(u8, line, context_prefix) or std.mem.startsWith(u8, line, "| ");
+}
pub const Row = struct {
kind: enum { match, context },
+ declaration: bool = false,
path: []u8,
at: look.Spot,
location_end: usize,
@@ -27,7 +32,7 @@ pub fn freeRows(gpa: std.mem.Allocator, rows: []Row) void {
pub fn equalRows(a: []const Row, b: []const Row) bool {
if (a.len != b.len) return false;
for (a, b) |left, right| {
- if (left.kind != right.kind or !std.meta.eql(left.at, right.at) or
+ if (left.kind != right.kind or left.declaration != right.declaration or !std.meta.eql(left.at, right.at) or
left.location_end != right.location_end or left.code_start != right.code_start or
!std.mem.eql(u8, left.path, right.path) or !std.mem.eql(u8, left.colors, right.colors)) return false;
}
@@ -65,9 +70,16 @@ pub const Parsed = struct {
pub fn parse(line: []const u8) Parsed {
if (std.mem.indexOfScalar(u8, line, '\t')) |tab| {
const prefix = std.mem.trimEnd(u8, line[0..tab], " ");
- const target = look.parsePathLine(prefix);
- if (target.at.line > 0 and target.end == prefix.len)
- return .{ .path = target.path, .at = target.at, .end = prefix.len, .code_start = tab + 1 };
+ const target = parseRaw(prefix);
+ if (target.at.line > 0) {
+ const suffix = std.mem.trim(u8, prefix[target.end..], " ");
+ var stars = true;
+ for (suffix) |byte| if (byte != '*') {
+ stars = false;
+ break;
+ };
+ if (stars) return .{ .path = target.path, .at = target.at, .end = target.end, .code_start = tab + 1 };
+ }
}
return parseRaw(line);
}
@@ -100,6 +112,8 @@ const Pending = struct {
code: []const u8,
original: ?usize,
hidden: bool = false,
+ declaration: bool = false,
+ depth: usize = 0,
colors: []const u8 = &.{},
fn lessThan(_: void, a: Pending, b: Pending) bool {
@@ -155,7 +169,7 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
syntax.contextDeclarations(arena, path, source) catch &.{}
else
&.{};
- for (matches.items[first..end]) |match| {
+ for (pending.items[group_start..]) |*match| {
const row = match.at.line -| 1;
if (row >= count) continue;
const lo = row -| p.locations_config.context;
@@ -163,22 +177,24 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
var context_row = lo;
while (context_row <= hi) : (context_row += 1) {
if (context_row == row) continue;
- wanted[context_row] = @max(wanted[context_row], 1);
+ wanted[context_row] |= 1;
}
for (declarations) |declaration| {
if (declaration.start_line > row) break;
if (declaration.end_line < row) continue;
+ match.depth += 1;
var header = declaration.start_line;
while (header <= declaration.header_end_line and header < row) : (header += 1)
- wanted[header] = if (p.locations_config.tslocations) @max(wanted[header], 1) else 2;
+ wanted[header] |= 2;
}
}
for (wanted, 0..) |kind, row| {
if (kind > 0) {
const line = source_lines.items[row];
const offset = @intFromPtr(line.ptr) - @intFromPtr(source.ptr);
- const colors = if (offset + line.len <= source_colors.len) source_colors[offset..][0..line.len] else &.{};
- try appendContext(arena, &pending, path, line, row, kind == 2, colors);
+ const declaration = kind & 2 != 0;
+ const colors = if (!declaration and offset + line.len <= source_colors.len) source_colors[offset..][0..line.len] else &.{};
+ try appendContext(arena, &pending, path, line, row, declaration, declaration and !p.locations_config.tslocations, colors);
}
}
}
@@ -195,7 +211,7 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
first = end;
}
var width: usize = 0;
- for (pending.items) |row| width = @max(width, pardes.panes.File.displayWidth(row.label) + @as(usize, if (row.original == null) 2 else 0));
+ for (pending.items) |row| width = @max(width, labelWidth(row));
var out: std.Io.Writer.Allocating = .init(p.gpa);
errdefer out.deinit();
var rows: std.ArrayList(Row) = .empty;
@@ -213,8 +229,11 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
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;
- const used = pardes.panes.File.displayWidth(row.label) + @as(usize, if (context) 2 else 0);
- try out.writer.splatByteAll(' ', width - used);
+ if (row.depth > 0) {
+ try out.writer.writeByte(' ');
+ try out.writer.splatByteAll('*', row.depth);
+ }
+ try out.writer.splatByteAll(' ', width - labelWidth(row));
try out.writer.writeByte('\t');
const code_start = out.written().len - before;
try out.writer.writeAll(row.code);
@@ -223,7 +242,7 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
errdefer p.gpa.free(owned_path);
const owned_colors = try p.gpa.dupe(u8, row.colors);
errdefer p.gpa.free(owned_colors);
- try rows.append(p.gpa, .{ .kind = if (context) .context else .match, .path = owned_path, .at = row.at, .location_end = location_end, .code_start = code_start, .colors = owned_colors });
+ try rows.append(p.gpa, .{ .kind = if (context) .context else .match, .declaration = row.declaration, .path = owned_path, .at = row.at, .location_end = location_end, .code_start = code_start, .colors = owned_colors });
if (anchor != null and row.original == anchor) mapped = index;
}
const owned_rows = try rows.toOwnedSlice(p.gpa);
@@ -231,7 +250,13 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
return .{ .content = try out.toOwnedSlice(), .rows = owned_rows, .anchor = mapped };
}
-fn appendContext(arena: std.mem.Allocator, rows: *std.ArrayList(Pending), path: []const u8, source: []const u8, row: usize, hidden: bool, colors: []const u8) !void {
+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);
+}
+
+fn appendContext(arena: std.mem.Allocator, rows: *std.ArrayList(Pending), path: []const u8, source: []const u8, row: usize, declaration: bool, hidden: bool, colors: []const u8) !void {
try rows.append(arena, .{
.path = path,
.at = .{ .line = row + 1 },
@@ -239,6 +264,7 @@ fn appendContext(arena: std.mem.Allocator, rows: *std.ArrayList(Pending), path:
.code = source,
.original = null,
.hidden = hidden,
+ .declaration = declaration,
.colors = colors,
});
}
diff --git a/src/panes.zig b/src/panes.zig
index f07e63c3..e21b1dc4 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -1064,6 +1064,7 @@ pub const File = struct {
const saved = try arena.alloc(dump.LocationRow, rows.len);
for (rows, saved) |row, *out| out.* = .{
.kind = @enumFromInt(@intFromEnum(row.kind)),
+ .declaration = row.declaration,
.path = row.path,
.line = row.at.line,
.col = row.at.col,
@@ -1094,7 +1095,8 @@ 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) != std.mem.startsWith(u8, line, locations.context_prefix) or
+ (row.kind == .context) != locations.isContextLine(line) 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))
return error.InvalidLocationRows;
@@ -1107,6 +1109,7 @@ pub const File = struct {
const path = try gpa.dupe(u8, row.path);
out.* = .{
.kind = @enumFromInt(@intFromEnum(row.kind)),
+ .declaration = row.declaration,
.path = path,
.at = .{ .line = row.line, .col = row.col, .end_line = row.end_line, .end_col = row.end_col },
.location_end = row.location_end,
@@ -2151,6 +2154,8 @@ pub const Output = struct {
const RowDecoration = struct {
location_end: usize = 0,
+ prefix_end: usize = 0,
+ declaration: bool = false,
mark_start: usize = 0,
mark_end: usize = 0,
severity: enum { none, err, warning, info, hint } = .none,
@@ -2159,7 +2164,8 @@ pub const Output = struct {
// Body paper changes immediately, so its ink must change with it
// even while the surrounding tags are still fading.
const chrome = pardes.ChromeTheme.fromTheme(p.theme());
- if (byte < row.location_end) return .{ .fg = .{ .rgb = chrome.lineno } };
+ if (row.declaration or byte < @max(row.location_end, row.prefix_end))
+ return .{ .fg = .{ .rgb = chrome.lineno } };
if (byte < row.mark_start or byte >= row.mark_end) return null;
if (row.severity == .none) return .{
.fg = .{ .rgb = chrome.search_fg },
@@ -2184,7 +2190,11 @@ pub const Output = struct {
const target = locations.parse(line);
const spot = if (metadata) |m| m.at else target.at;
if (spot.line == 0) return .{};
- var row: RowDecoration = .{ .location_end = if (metadata) |m| m.location_end else target.end };
+ var row: RowDecoration = .{
+ .location_end = if (metadata) |m| m.location_end else target.end,
+ .prefix_end = if (metadata) |m| m.code_start else target.end,
+ .declaration = if (metadata) |m| m.declaration else false,
+ };
const source_start = if (metadata) |m| m.code_start else target.code_start;
if (source_start >= line.len) return row;
if (metadata) |m| if (m.kind == .context) return row;
@@ -2341,7 +2351,7 @@ pub const Output = struct {
pub fn isResultRow(file: *const File.State, row: usize) bool {
if (row < file.location_rows.len) return file.location_rows[row].kind == .match;
- return !std.mem.startsWith(u8, modal.lineSlice(file.content, row), locations.context_prefix);
+ return !locations.isContextLine(modal.lineSlice(file.content, row));
}
fn sortResults(arena: std.mem.Allocator, from: Origin, content: []u8, anchor: ?usize) !?usize {
@@ -2438,7 +2448,7 @@ pub const Output = struct {
var first: ?usize = null;
var row: usize = 0;
while (lines.next()) |line| : (row += 1) {
- if (std.mem.startsWith(u8, line, locations.context_prefix)) continue;
+ if (locations.isContextLine(line)) continue;
const target = location(line);
if (target.at.line == 0) continue;
if (first == null) first = row;
diff --git a/src/syntax.zig b/src/syntax.zig
index 8209dc2c..bd6a64a0 100644
--- a/src/syntax.zig
+++ b/src/syntax.zig
@@ -690,6 +690,21 @@ pub fn highlightLocationRows(gpa: std.mem.Allocator, content: []const u8, rows:
offset = @min(end + 1, content.len);
}
}
+ // Declaration context is intentionally muted by the output painter. Keep
+ // its source in parsing groups, then remove every syntax/style flag after
+ // complete-source snapshots have been applied.
+ if (comptime @hasField(@TypeOf(rows[0]), "declaration")) {
+ offset = 0;
+ for (rows) |row| {
+ if (offset >= content.len) break;
+ const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len;
+ if (row.declaration) {
+ const source_start = offset + @min(row.code_start, end - offset);
+ @memset(styles[source_start..end], 0);
+ }
+ offset = @min(end + 1, content.len);
+ }
+ }
if (!painted) {
gpa.free(styles);
return &.{};
@@ -1203,3 +1218,29 @@ test "syntax location context snapshots retain omitted multiline comment scope"
try std.testing.expectEqual(@intFromEnum(Syn.comment), style);
try std.testing.expectEqual(@intFromEnum(Syn.none), styles[0]);
}
+
+test "syntax declaration locations clear snapshots without muting ordinary context" {
+ if (!enabled) return;
+ const gpa = std.testing.allocator;
+ start(gpa);
+ defer stop();
+ const header = "pub fn run() void {";
+ const content = "| \t" ++ header ++ "\n| \t const value = 42;\nfile.zig:3\t}\n";
+ const snapshots = [_]u8{0xff} ** header.len;
+ const Row = struct {
+ path: []const u8 = "file.zig",
+ code_start: usize,
+ at: look.Spot,
+ colors: []const u8 = &.{},
+ declaration: bool = false,
+ };
+ const rows = [_]Row{
+ .{ .code_start = 3, .at = .{ .line = 1 }, .colors = &snapshots, .declaration = true },
+ .{ .code_start = 3, .at = .{ .line = 2 } },
+ .{ .code_start = "file.zig:3\t".len, .at = .{ .line = 3 } },
+ };
+ const styles = try highlightLocationRows(gpa, content, &rows);
+ defer gpa.free(styles);
+ for (styles[3..][0..header.len]) |style| try std.testing.expectEqual(@as(u8, 0), style);
+ try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "42").?]);
+}
diff --git a/test/panes.zig b/test/panes.zig
index 16e40ee3..4ed8c1e1 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -2000,10 +2000,11 @@ const OutputTests = struct {
matches[count] = i;
count += 1;
} else {
- try std.testing.expect(std.mem.startsWith(u8, line, "| "));
+ try std.testing.expect(std.mem.startsWith(u8, line, " "));
try std.testing.expect(!panes.Output.isResultRow(file, i));
try std.testing.expectEqualStrings(modal.lineSlice(source, expected_line - 1), line[row.code_start..]);
}
+ try std.testing.expectEqual(expected_line <= 2, row.declaration);
if (expected_line <= 2) try std.testing.expectEqual(@as(usize, 0), row.location_end);
}
try std.testing.expectEqual(@as(usize, 2), count);
@@ -2028,7 +2029,13 @@ const OutputTests = struct {
const styles = file.highlights[keyword..][0.."struct".len];
var colored = false;
for (styles) |style| colored = colored or style != 0;
- try std.testing.expect(colored);
+ try std.testing.expect(!colored);
+ const neighbor_row = file.location_rows[2];
+ const neighbor_offset = @intFromPtr(modal.lineSlice(file.content, 2).ptr) - @intFromPtr(file.content.ptr);
+ var neighbor_colored = false;
+ for (file.highlights[neighbor_offset + neighbor_row.code_start .. neighbor_offset + modal.lineSlice(file.content, 2).len]) |style|
+ neighbor_colored = neighbor_colored or style != 0;
+ try std.testing.expect(neighbor_colored);
try p.dumpState();
const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
@@ -2041,6 +2048,7 @@ const OutputTests = struct {
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.expectEqualStrings(before.path, after.path);
try std.testing.expectEqual(before.at, after.at);
try std.testing.expectEqual(before.location_end, after.location_end);
@@ -2071,6 +2079,80 @@ const OutputTests = struct {
try std.testing.expectEqual(matches[1], pane.search_row.?);
}
+ test "Locations visible declarations are muted and stars count scopes rather than signature lines" {
+ if (!syntax.enabled) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 });
+ defer p.deinit();
+ const source = "const Outer = struct {\n" ++
+ " const Inner = struct {\n" ++
+ " pub fn run(\n" ++
+ " value: usize,\n" ++
+ " ) void {\n" ++
+ " // nearby\n" ++
+ "\t\tcall();\n" ++
+ " }\n };\n};\n";
+ const pane = try TreeContextTests.setZigFile(p, source);
+ p.locations_config = .{ .context = 1, .tscontext = true, .tslocations = true };
+ const path = pane.file.?.path;
+ try fillResults(p, 0, "/", .search, "nesting", try std.fmt.allocPrint(p.gpa, "{s}:7:1 \t\tcall();\n{s}:2:5 const Inner = struct {{\n", .{ path, path }), 0);
+ const result = p.panes[pane.search_pane.?].?;
+ const file = &result.file.?;
+ var matches: usize = 0;
+ var alignment: ?usize = null;
+ for (file.location_rows, 0..) |row, index| {
+ const line = modal.lineSlice(file.content, index);
+ const width = panes.File.displayWidth(line[0..row.code_start]);
+ if (alignment) |previous| try std.testing.expectEqual(previous, width);
+ alignment = width;
+ try std.testing.expect(!std.mem.startsWith(u8, line, "| "));
+ if (row.kind == .match) {
+ matches += 1;
+ try std.testing.expect(!row.declaration);
+ const location = panes.Output.location(line);
+ try std.testing.expectEqualStrings(path, location.path);
+ try std.testing.expectEqual(row.at, location.at);
+ try std.testing.expectEqual(row.location_end, location.end);
+ if (row.at.line == 7) {
+ try std.testing.expect(std.mem.startsWith(u8, line[row.location_end..], " ***"));
+ try std.testing.expectEqualStrings("\t\tcall();", line[row.code_start..]);
+ }
+ } 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.expectEqual(@as(usize, 2), matches);
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ result.scrollBy(-1000);
+ layout.compute(p);
+ const surface = try p.render(frame.allocator());
+ const rect = p.rects[pane.search_pane.?];
+ const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ for (file.location_rows, 0..) |row, index| {
+ if (!row.declaration) continue;
+ const line = modal.lineSlice(file.content, index);
+ const start = @intFromPtr(line.ptr) - @intFromPtr(file.content.ptr);
+ for (file.highlights[start + row.code_start .. start + line.len]) |style|
+ try std.testing.expectEqual(@as(u8, 0), style);
+ const local = std.mem.indexOfNone(u8, line[row.code_start..], " \t") orelse continue;
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(result) + @as(u16, @intCast(panes.File.displayWidth(line[0 .. row.code_start + local])));
+ const y = body_y + @as(u16, @intCast(index));
+ const cell = surface.at(x, y);
+ try std.testing.expect(!cell.style.bold);
+ try std.testing.expectEqual(pardes.Color{ .rgb = pardes.ChromeTheme.fromTheme(p.theme()).lineno }, cell.style.fg);
+ }
+ // Old dumps and edited outputs still identify both context prefixes.
+ panes.File.setContent(p, file, try p.gpa.dupe(u8, "| old.zig:1 old\n new.zig:2 new\nmain.zig:42 ***\t\tmatch\n"));
+ try std.testing.expect(!panes.Output.isResultRow(file, 0));
+ try std.testing.expect(!panes.Output.isResultRow(file, 1));
+ try std.testing.expect(panes.Output.isResultRow(file, 2));
+ const parsed = panes.Output.location(modal.lineSlice(file.content, 2));
+ try std.testing.expectEqualStrings("main.zig", parsed.path);
+ try std.testing.expectEqual(@as(usize, 42), parsed.at.line);
+ }
+
test "replacing generated Help clears stale selection before executing a new row" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 });
defer p.deinit();