diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 15:11:14 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12 (patch) | |
| tree | b3102012a030560431a9360840e80a5e7316cb1e | |
| parent | 40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (diff) | |
| download | pardes-7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12.tar.gz pardes-7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12.zip | |
Mute result declaration context and show nesting after locations
| -rw-r--r-- | docs/config.md | 8 | ||||
| -rw-r--r-- | src/dump.zig | 1 | ||||
| -rw-r--r-- | src/locations.zig | 56 | ||||
| -rw-r--r-- | src/panes.zig | 20 | ||||
| -rw-r--r-- | src/syntax.zig | 41 | ||||
| -rw-r--r-- | test/panes.zig | 86 |
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(); |
