summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 18:41:15 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit02ea4db25f9802e76112f3b4e69aa75e5a9464dc (patch)
treedaa17ec75135e055a60eff982d97057dcd109c25
parent505dedb2459e7b51cbdb0c066698c6e5596d19dd (diff)
downloadpardes-02ea4db25f9802e76112f3b4e69aa75e5a9464dc.tar.gz
pardes-02ea4db25f9802e76112f3b4e69aa75e5a9464dc.zip
Default location results to stacked addresses and source previews
-rw-r--r--docs/config.md7
-rw-r--r--src/dump.zig2
-rw-r--r--src/locations.zig121
-rw-r--r--src/locations_config.zig8
-rw-r--r--src/panes.zig69
-rw-r--r--src/pardes.zig2
-rw-r--r--src/syntax.zig111
-rw-r--r--test/output.zig154
-rw-r--r--test/panes.zig12
-rw-r--r--test/pdf.zig17
10 files changed, 406 insertions, 97 deletions
diff --git a/docs/config.md b/docs/config.md
index 5b5641ba..6f3273b9 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -150,7 +150,7 @@ results in an output pane. Execute the printed line to apply it again, or
supply just the fields to change:
```text
-LocationsConfig context:5 tscontext:on tslocations:off
+LocationsConfig context:5 tscontext:on tslocations:off layout:stacked
```
- `context` is the number of source lines above and below each match (default
@@ -163,8 +163,11 @@ LocationsConfig context:5 tscontext:on tslocations:off
(default `on`); continuation lines keep the same alignment without repeating
the location. Declaration headers use the same muted color as locations, with or
without their locations visible.
+- `layout:stacked` (default) puts each location on its own line, followed by its
+ source preview. Hidden context locations do not add an empty address line.
+ Use `layout:inline` to put locations beside the source instead.
-Result locations share padding in groups of eight matches, so a long path only
+In inline layout, result locations share padding in groups of eight matches, so a long path only
widens its own group. Context does not count toward the eight; at group boundaries,
it aligns with the nearer match (ties stay with the preceding group). Source
indentation is preserved. All visible locations start flush left; `n` and `N`
diff --git a/src/dump.zig b/src/dump.zig
index 0c538a1a..89abd995 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -75,7 +75,7 @@ pub const Terminal = struct {
};
pub const LocationRow = struct {
- kind: enum { match, context },
+ kind: enum { match, context, preview },
declaration: bool = false,
path: []const u8,
line: usize,
diff --git a/src/locations.zig b/src/locations.zig
index 0f580e34..97caf1d9 100644
--- a/src/locations.zig
+++ b/src/locations.zig
@@ -15,7 +15,7 @@ pub fn isContextLine(line: []const u8) bool {
}
pub const Row = struct {
- kind: enum { match, context },
+ kind: enum { match, context, preview },
declaration: bool = false,
path: []u8,
at: look.Spot,
@@ -44,15 +44,16 @@ pub fn equalRows(a: []const Row, b: []const Row) bool {
/// Compare native producer rows with an already formatted, context-free view.
/// Identical refreshes retain allocations, syntax and selection state.
-pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) bool {
+pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row, layout: Config.Layout) bool {
if (rows.len == 0) return std.mem.eql(u8, input, rendered);
- if (std.mem.endsWith(u8, input, "\n") != std.mem.endsWith(u8, rendered, "\n")) return false;
var raw_lines = std.mem.splitScalar(u8, input, '\n');
var shown_lines = std.mem.splitScalar(u8, rendered, '\n');
var width: usize = 0;
- for (rows, 0..) |row, index| {
+ var index: usize = 0;
+ while (index < rows.len) {
+ const row = rows[index];
if (row.kind != .match) return false;
- if (index % alignment_entries == 0) {
+ if (layout == .@"inline" and index % alignment_entries == 0) {
width = 0;
var chunk = raw_lines;
for (0..alignment_entries) |_| {
@@ -64,9 +65,19 @@ pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) boo
const raw = raw_lines.next() orelse return false;
const shown = shown_lines.next() orelse return false;
const target = parseRaw(raw);
- if (target.end != row.location_end or row.code_start > shown.len or
- !std.mem.eql(u8, raw[0..target.end], shown[0..row.location_end]) or
- !std.mem.eql(u8, raw[target.code_start..], shown[row.code_start..])) return false;
+ if (target.end != row.location_end or row.location_end > shown.len or row.code_start > shown.len or
+ !std.mem.eql(u8, raw[0..target.end], shown[0..row.location_end])) return false;
+ if (layout == .stacked) {
+ if (shown.len != row.location_end or row.code_start != shown.len or index + 1 >= rows.len) return false;
+ const preview = rows[index + 1];
+ if (preview.kind != .preview or preview.location_end != 0 or preview.code_start != 0 or
+ !std.meta.eql(row.at, preview.at) or !std.mem.eql(u8, row.path, preview.path)) return false;
+ const code = shown_lines.next() orelse return false;
+ if (!std.mem.eql(u8, raw[target.code_start..], code)) return false;
+ index += 2;
+ continue;
+ }
+ if (!std.mem.eql(u8, raw[target.code_start..], shown[row.code_start..])) return false;
// A restored result may still have the old pane-wide padding. Do not
// let the identical-input shortcut retain that layout when rerun.
if (row.code_start <= row.location_end) return false;
@@ -74,9 +85,14 @@ pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) boo
if (padding.len != width - pardes.panes.File.displayWidth(raw[0..target.end]) + 1 or
padding[padding.len - 1] != '\t') return false;
for (padding[0 .. padding.len - 1]) |byte| if (byte != ' ') return false;
+ index += 1;
}
- if (raw_lines.next()) |tail| if (tail.len != 0) return false;
- return raw_lines.next() == null;
+ const raw_tail = raw_lines.next();
+ const shown_tail = shown_lines.next();
+ if ((raw_tail == null) != (shown_tail == null)) return false;
+ if (raw_tail) |tail| if (tail.len != 0) return false;
+ if (shown_tail) |tail| if (tail.len != 0) return false;
+ return raw_lines.next() == null and shown_lines.next() == null;
}
pub const Parsed = struct {
@@ -246,44 +262,73 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
rows.deinit(p.gpa);
}
var mapped = anchor;
- first = 0;
- while (first < pending.items.len) {
- const end = alignmentEnd(pending.items, first);
- var width: usize = 0;
- for (pending.items[first..end]) |row| width = @max(width, labelWidth(row));
- for (pending.items[first..end], first..) |row, index| {
- const before = out.written().len;
+ if (p.locations_config.layout == .stacked) {
+ for (pending.items, 0..) |row, index| {
const context = row.original == null;
- try out.writer.writeAll(row.label);
- const location_end = if (row.hidden) 0 else out.written().len - before;
- 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);
- if (row.declaration and index + 1 < pending.items.len) {
- const next = pending.items[index + 1];
- if (std.mem.eql(u8, row.path, next.path) and next.at.line -| row.at.line > 1)
- try out.writer.writeAll(" ...");
+ if (anchor != null and row.original == anchor) mapped = rows.items.len;
+ if (!row.hidden) {
+ const before = out.written().len;
+ try out.writer.writeAll(row.label);
+ if (row.depth > 0) {
+ try out.writer.writeByte(' ');
+ try out.writer.splatByteAll('*', row.depth);
+ }
+ try appendMetadata(p.gpa, &rows, row, if (context) .context else .match, row.label.len, out.written().len - before, false);
+ try out.writer.writeByte('\n');
}
+ try writeCode(&out.writer, pending.items, index);
+ try appendMetadata(p.gpa, &rows, row, if (context) .context else .preview, 0, 0, true);
if (index + 1 < pending.items.len or std.mem.endsWith(u8, input, "\n")) try out.writer.writeByte('\n');
- const owned_path = try p.gpa.dupe(u8, row.path);
- 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, .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;
}
- first = end;
+ } else {
+ first = 0;
+ while (first < pending.items.len) {
+ const end = alignmentEnd(pending.items, first);
+ var width: usize = 0;
+ for (pending.items[first..end]) |row| width = @max(width, labelWidth(row));
+ for (pending.items[first..end], first..) |row, index| {
+ const before = out.written().len;
+ const context = row.original == null;
+ try out.writer.writeAll(row.label);
+ const location_end = if (row.hidden) 0 else out.written().len - before;
+ 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 writeCode(&out.writer, pending.items, index);
+ if (index + 1 < pending.items.len or std.mem.endsWith(u8, input, "\n")) try out.writer.writeByte('\n');
+ try appendMetadata(p.gpa, &rows, row, if (context) .context else .match, location_end, code_start, true);
+ if (anchor != null and row.original == anchor) mapped = index;
+ }
+ first = end;
+ }
}
const owned_rows = try rows.toOwnedSlice(p.gpa);
errdefer freeRows(p.gpa, owned_rows);
return .{ .content = try out.toOwnedSlice(), .rows = owned_rows, .anchor = mapped };
}
+fn writeCode(writer: *std.Io.Writer, rows: []const Pending, index: usize) !void {
+ const row = rows[index];
+ try writer.writeAll(row.code);
+ if (row.declaration and index + 1 < rows.len) {
+ const next = rows[index + 1];
+ if (std.mem.eql(u8, row.path, next.path) and next.at.line -| row.at.line > 1)
+ try writer.writeAll(" ...");
+ }
+}
+
+fn appendMetadata(gpa: std.mem.Allocator, rows: *std.ArrayList(Row), pending: Pending, kind: @FieldType(Row, "kind"), location_end: usize, code_start: usize, source: bool) !void {
+ const path = try gpa.dupe(u8, pending.path);
+ errdefer gpa.free(path);
+ const colors = try gpa.dupe(u8, if (source) pending.colors else &.{});
+ errdefer gpa.free(colors);
+ try rows.append(gpa, .{ .kind = kind, .declaration = pending.declaration, .path = path, .at = pending.at, .location_end = location_end, .code_start = code_start, .colors = colors });
+}
+
const alignment_entries = 8;
/// Keep neighboring context with the nearer match at a chunk boundary. Shared
diff --git a/src/locations_config.zig b/src/locations_config.zig
index ed67f990..6b23575a 100644
--- a/src/locations_config.zig
+++ b/src/locations_config.zig
@@ -1,9 +1,11 @@
const std = @import("std");
pub const Config = struct {
+ pub const Layout = enum { @"inline", stacked };
context: u16 = 0,
tscontext: bool = false,
tslocations: bool = true,
+ layout: Layout = .stacked,
/// Apply only supplied fields. A malformed token rejects the whole update.
/// Repeating a field uses its last supplied value.
@@ -20,6 +22,7 @@ pub const Config = struct {
@field(result, field.name) = switch (@typeInfo(field.type)) {
.bool => if (std.mem.eql(u8, value, "on")) true else if (std.mem.eql(u8, value, "off")) false else return error.ExpectedOnOrOff,
.int => std.fmt.parseInt(field.type, value, 10) catch return error.InvalidContext,
+ .@"enum" => std.meta.stringToEnum(field.type, value) orelse return error.InvalidLayout,
else => @compileError("unsupported location setting type"),
};
found = true;
@@ -38,6 +41,7 @@ pub const Config = struct {
switch (@typeInfo(field.type)) {
.bool => try writer.writeAll(if (value) "on" else "off"),
.int => try writer.print("{d}", .{value}),
+ .@"enum" => try writer.writeAll(@tagName(value)),
else => @compileError("unsupported location setting type"),
}
}
@@ -52,10 +56,11 @@ test "LocationsConfig reflection round trip and partial updates" {
var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out.deinit();
try config.write(&out.writer);
- try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off", out.written());
+ try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off layout:stacked", out.written());
try std.testing.expectEqual(config, try (Config{}).parse(out.written()));
try std.testing.expectEqual(config, try config.parse(" \n\t"));
try std.testing.expectEqual(@as(u16, 2), (try config.parse("context:1 context:2")).context);
+ try std.testing.expectEqual(Config.Layout.@"inline", (try config.parse("layout:inline")).layout);
}
test "LocationsConfig rejects invalid updates atomically" {
@@ -63,6 +68,7 @@ test "LocationsConfig rejects invalid updates atomically" {
try std.testing.expectError(error.UnknownLocationSetting, config.parse("context:5 unknown:on"));
try std.testing.expectError(error.ExpectedOnOrOff, config.parse("context:5 tscontext:true"));
try std.testing.expectError(error.ExpectedKeyValue, config.parse("context"));
+ try std.testing.expectError(error.InvalidLayout, config.parse("context:5 layout:sideways"));
for ([_][]const u8{ "context:", "context:-1", "context:65536", "context:five" }) |input|
try std.testing.expectError(error.InvalidContext, config.parse(input));
try std.testing.expectEqual(@as(u16, 3), config.context);
diff --git a/src/panes.zig b/src/panes.zig
index 7855866d..59993148 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -1082,6 +1082,11 @@ pub const File = struct {
return saved;
}
+ fn sameSavedLocation(a: dump.LocationRow, b: dump.LocationRow) bool {
+ return std.mem.eql(u8, a.path, b.path) and a.line == b.line and a.col == b.col and
+ a.end_line == b.end_line and a.end_col == b.end_col;
+ }
+
fn restoreLocationRows(gpa: std.mem.Allocator, content: []const u8, saved: []const dump.LocationRow) ![]locations.Row {
if (saved.len == 0) return &.{};
if (saved.len > std.mem.count(u8, content, "\n") + 1) return error.InvalidLocationRows;
@@ -1095,16 +1100,33 @@ pub const File = struct {
gpa.free(rows);
}
var lines = std.mem.splitScalar(u8, content, '\n');
- for (saved, rows) |row, *out| {
+ var previous_line: []const u8 = "";
+ for (saved, rows, 0..) |row, *out, index| {
const line = lines.next() orelse return error.InvalidLocationRows;
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.location_end > row.code_start or row.code_start > line.len 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))
return error.InvalidLocationRows;
+ const inline_row = row.code_start > row.location_end and row.code_start > 0 and line[row.code_start - 1] == '\t';
+ const address_row = !inline_row and row.location_end > 0 and row.code_start == line.len;
+ const source_row = row.location_end == 0 and row.code_start == 0;
+ if (!inline_row and !address_row and !source_row) return error.InvalidLocationRows;
+ if (row.kind == .preview) {
+ if (!source_row or index == 0) return error.InvalidLocationRows;
+ const previous = saved[index - 1];
+ if (previous.kind != .match or previous.location_end == 0 or previous.code_start != previous_line.len or previous_line[previous_line.len - 1] == '\t' or
+ !sameSavedLocation(previous, row)) return error.InvalidLocationRows;
+ }
+ if (address_row) {
+ if (index + 1 >= saved.len) return error.InvalidLocationRows;
+ const following = saved[index + 1];
+ const expected: @TypeOf(row.kind) = if (row.kind == .match) .preview else .context;
+ if (following.kind != expected or following.code_start != 0 or following.location_end != 0 or
+ following.declaration != row.declaration or !sameSavedLocation(row, following)) return error.InvalidLocationRows;
+ }
const code_len = line.len - row.code_start;
if (row.colors_b64.len > std.base64.standard.Encoder.calcSize(code_len)) return error.InvalidLocationRows;
const colors = try dump.decodeBytes(gpa, row.colors_b64);
@@ -1122,6 +1144,7 @@ pub const File = struct {
.colors = colors,
};
initialized += 1;
+ previous_line = line;
}
if (lines.next()) |remaining| if (remaining.len != 0 or lines.next() != null) return error.InvalidLocationRows;
return rows;
@@ -1149,6 +1172,32 @@ pub const File = struct {
try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "| \tx\n", &.{row}));
}
+ test "stacked location metadata requires matching adjacent preview ownership" {
+ const gpa = std.testing.allocator;
+ const header = "x.c:2:1-3";
+ const content = header ++ "\nfoo\n";
+ const match: dump.LocationRow = .{ .kind = .match, .path = "x.c", .line = 2, .col = 1, .end_line = 2, .end_col = 3, .location_end = header.len, .code_start = header.len };
+ const preview: dump.LocationRow = .{ .kind = .preview, .path = "x.c", .line = 2, .col = 1, .end_line = 2, .end_col = 3, .location_end = 0, .code_start = 0 };
+ const restored = try restoreLocationRows(gpa, content, &.{ match, preview });
+ defer locations.freeRows(gpa, restored);
+ try std.testing.expectEqual(.preview, restored[1].kind);
+ try std.testing.expectEqual(restored[0].at, restored[1].at);
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "foo\n", &.{preview}));
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, header ++ "\n", &.{match}));
+ var inline_match = match;
+ inline_match.code_start += 1;
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, header ++ "\t\nfoo\n", &.{ inline_match, preview }));
+ var wrong = preview;
+ wrong.path = "other.c";
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong }));
+ wrong = preview;
+ wrong.line = 3;
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong }));
+ wrong = preview;
+ wrong.code_start = 1;
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, content, &.{ match, wrong }));
+ }
+
pub fn graphemeDisplayWidth(grapheme: []const u8) usize {
if (std.mem.eql(u8, grapheme, "\t")) return config.tab_width;
if (grapheme.len == 1 and grapheme[0] >= 0x20 and grapheme[0] < 0x7f) return 1;
@@ -2258,6 +2307,18 @@ pub const Output = struct {
}
}
+ test "stacked empty previews distinguish separators from trailing newlines" {
+ var path = [_]u8{ 'x', '.', 'c' };
+ const rows = [_]locations.Row{
+ .{ .kind = .match, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 7, .code_start = 7 },
+ .{ .kind = .preview, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 0, .code_start = 0 },
+ };
+ try std.testing.expect(locations.sameInput("x.c:1:1 ", "x.c:1:1\n", &rows, .stacked));
+ try std.testing.expect(locations.sameInput("x.c:1:1 \n", "x.c:1:1\n\n", &rows, .stacked));
+ try std.testing.expect(!locations.sameInput("x.c:1:1 ", "x.c:1:1\n\n", &rows, .stacked));
+ try std.testing.expect(!locations.sameInput("x.c:1:1 \n", "x.c:1:1\n", &rows, .stacked));
+ }
+
test "plain output does not opt into location decoration" {
const report = rowDecoration(.{ .from = .{ .cmd = .LocationsConfig } }, "LocationsConfig context:5 tscontext:on");
try std.testing.expectEqualDeep(RowDecoration{}, report);
@@ -2527,7 +2588,7 @@ pub const Output = struct {
if (!std.meta.eql(output.from, from)) continue;
if (by_arg and !std.mem.eql(u8, output.arg(), arg)) continue;
if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue;
- if (!locations.sameInput(content, rf.content, rf.location_rows)) continue;
+ if (!locations.sameInput(content, rf.content, rf.location_rows, p.locations_config.layout)) continue;
try setArg(output, arg);
p.gpa.free(content);
p.active = id;
diff --git a/src/pardes.zig b/src/pardes.zig
index d115bf2d..2729ef93 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1121,7 +1121,7 @@ test "LocationsConfig command reports partial updates and survives restore" {
const out = file.output orelse continue;
if (!std.meta.eql(out.from, panes.Output.Origin{ .cmd = .LocationsConfig })) continue;
reports += 1;
- try std.testing.expectEqualStrings("LocationsConfig context:5 tscontext:on tslocations:off\n", file.content);
+ try std.testing.expectEqualStrings("LocationsConfig context:5 tscontext:on tslocations:off layout:stacked\n", file.content);
}
try std.testing.expectEqual(@as(usize, 1), reports);
try p.dumpState();
diff --git a/src/syntax.zig b/src/syntax.zig
index bd6a64a0..9e31669f 100644
--- a/src/syntax.zig
+++ b/src/syntax.zig
@@ -630,48 +630,58 @@ pub fn highlightLocationRows(gpa: std.mem.Allocator, content: []const u8, rows:
defer gpa.free(code);
const colors = try gpa.alloc(u8, content.len);
defer gpa.free(colors);
- var painted = false;
- var row_index: usize = 0;
+ const SourceRow = struct { index: usize, start: usize, len: usize };
+ const sources = try gpa.alloc(SourceRow, rows.len);
+ defer gpa.free(sources);
+ var source_count: usize = 0;
var offset: usize = 0;
- while (row_index < rows.len and offset < content.len) {
- const first = row_index;
- const group_start = offset;
- const selected = (forExt(std.fs.path.extension(rows[first].path)) catch null);
- var code_len: usize = 0;
- while (row_index < rows.len and offset < content.len) {
- const row = rows[row_index];
- if (row_index > first and (!std.mem.eql(u8, row.path, rows[first].path) or
- row.at.line != rows[row_index - 1].at.line +| 1)) break;
- const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len;
- const source_start = offset + @min(row.code_start, end - offset);
- if (row_index > first) {
- code[code_len] = '\n';
- code_len += 1;
- }
- @memcpy(code[code_len..][0 .. end - source_start], content[source_start..end]);
- code_len += end - source_start;
- offset = @min(end + 1, content.len);
- row_index += 1;
+ for (rows, 0..) |row, index| {
+ if (offset >= content.len) break;
+ const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len;
+ const source_start = offset + @min(row.code_start, end - offset);
+ var address_only = false;
+ if (comptime @hasField(@TypeOf(row), "location_end")) {
+ address_only = row.location_end > 0 and source_start == end;
}
- const language = selected orelse continue;
- if (offset > 0 and content[offset - 1] == '\n') {
- code[code_len] = '\n';
- code_len += 1;
+ if (!address_only) {
+ sources[source_count] = .{ .index = index, .start = source_start, .len = end - source_start };
+ source_count += 1;
}
- if (code_len == 0) continue;
- @memset(colors[0..code_len], 0);
- paint(colors[0..code_len], code[0..code_len], language);
- painted = true;
- var target = group_start;
- var source_offset: usize = 0;
- for (rows[first..row_index]) |row| {
- const end = std.mem.indexOfScalarPos(u8, content, target, '\n') orelse content.len;
- const source_start = target + @min(row.code_start, end - target);
- const len = end - source_start;
- @memcpy(styles[source_start..end], colors[source_offset..][0..len]);
- source_offset += len + 1;
- target = @min(end + 1, content.len);
+ offset = @min(end + 1, content.len);
+ }
+ var painted = false;
+ var first: usize = 0;
+ while (first < source_count) {
+ const first_row = rows[sources[first].index];
+ var end = first + 1;
+ while (end < source_count) : (end += 1) {
+ const row = rows[sources[end].index];
+ const previous = rows[sources[end - 1].index];
+ if (!std.mem.eql(u8, row.path, first_row.path) or row.at.line != previous.at.line +| 1) break;
}
+ const language = (forExt(std.fs.path.extension(first_row.path)) catch null);
+ if (language) |selected| {
+ var code_len: usize = 0;
+ for (sources[first..end]) |source| {
+ @memcpy(code[code_len..][0..source.len], content[source.start..][0..source.len]);
+ code_len += source.len;
+ if (code_len < code.len) {
+ code[code_len] = '\n';
+ code_len += 1;
+ }
+ }
+ if (code_len > 0) {
+ @memset(colors[0..code_len], 0);
+ paint(colors[0..code_len], code[0..code_len], selected);
+ painted = true;
+ var at: usize = 0;
+ for (sources[first..end]) |source| {
+ @memcpy(styles[source.start..][0..source.len], colors[at..][0..source.len]);
+ at += source.len + 1;
+ }
+ }
+ }
+ first = end;
}
// A shown context range can begin inside a comment/string whose opener was
// omitted. Formatter snapshots from the complete source take precedence,
@@ -1244,3 +1254,28 @@ test "syntax declaration locations clear snapshots without muting ordinary conte
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").?]);
}
+
+test "syntax stacked addresses do not split multiline source groups" {
+ if (!enabled or (!minimal_grammars and !full_grammars)) return;
+ const gpa = std.testing.allocator;
+ start(gpa);
+ defer stop();
+ const Row = struct { path: []const u8 = "a.c", code_start: usize, location_end: usize, at: look.Spot };
+ const content = "a.c:1:1\n/* open\na.c:2:1\nint value = 42;\na.c:3:1\n*/ int other = 7;\n";
+ const rows = [_]Row{
+ .{ .code_start = 7, .location_end = 7, .at = .{ .line = 1 } },
+ .{ .code_start = 0, .location_end = 0, .at = .{ .line = 1 } },
+ .{ .code_start = 7, .location_end = 7, .at = .{ .line = 2 } },
+ .{ .code_start = 0, .location_end = 0, .at = .{ .line = 2 } },
+ .{ .code_start = 7, .location_end = 7, .at = .{ .line = 3 } },
+ .{ .code_start = 0, .location_end = 0, .at = .{ .line = 3 } },
+ };
+ const styles = try highlightLocationRows(gpa, content, &rows);
+ defer gpa.free(styles);
+ try std.testing.expectEqual(@intFromEnum(Syn.comment), styles[std.mem.indexOf(u8, content, "42").?]);
+ try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "7;").?]);
+ for ([_][]const u8{ "a.c:1:1", "a.c:2:1", "a.c:3:1" }) |label| {
+ const at = std.mem.indexOf(u8, content, label).?;
+ for (styles[at..][0..label.len]) |style| try std.testing.expectEqual(@intFromEnum(Syn.none), style);
+ }
+}
diff --git a/test/output.zig b/test/output.zig
index c162c1d1..c738a661 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -92,6 +92,7 @@ test "output callers opt into location highlighting instead of matching prose co
for ([_]panes.Output.Origin{ .{ .cmd = .LocationsConfig }, .{ .cmd = .Config }, .{ .query = .hover } }) |origin| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 140, .rows = 30 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("ordinary\n");
p.presentation.enabled = false;
if (origin == .cmd and origin.cmd == .LocationsConfig) {
@@ -122,6 +123,7 @@ test "location producers retain rendered location match and diagnostic styles" {
for ([_]panes.Output.Origin{ .search, .{ .cmd = .Grep }, .{ .query = .references }, .{ .query = .diagnostics } }) |origin| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 140, .rows = 30 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("ordinary\n");
p.presentation.enabled = false;
const diagnostic = origin == .query and origin.query == .diagnostics;
@@ -157,6 +159,7 @@ test "location results sort filenames and numeric positions while remapping thei
for ([_]panes.Output.Origin{ .search, .{ .cmd = .Grep }, .{ .query = .references }, .{ .query = .diagnostics } }) |origin| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
try panes.Output.fillResults(p, 0, "/tmp", origin, "", try p.gpa.dupe(u8, before), 2);
const results = p.panes[source.search_pane.?].?;
@@ -170,6 +173,7 @@ test "ranked choices and prose retain their supplied order" {
for ([_]pardes.lsp.Kind{ .completion, .hover, .code_action }) |kind| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("notes\n");
try panes.Output.fillResults(p, 0, "/tmp", .{ .query = kind }, "", try p.gpa.dupe(u8, text), null);
const result = for (p.panes) |slot| {
@@ -192,6 +196,7 @@ test "reference Look selects the next sorted row from the captured request and k
const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/m.zig", .{tmp.sub_path});
const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = p.panes[0].?;
const response = "m.zig:5:5-8 later\nz.zig:1:1 last-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\na.zig:1:1 first-file\nm.zig:4:5-8 next\n";
const sorted = "a.zig:1:1 first-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\nm.zig:4:5-8 next\nm.zig:5:5-8 later\nz.zig:1:1 last-file\n";
@@ -254,6 +259,7 @@ test "location sorting leaves mixed prose alone and preserves newline shape" {
for (cases) |case| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
try panes.Output.fillResults(p, 0, "/tmp", .search, "", try p.gpa.dupe(u8, case.before), 1);
try expectResultRows(case.after, &p.panes[source.search_pane.?].?.file.?);
@@ -301,6 +307,7 @@ test "result locations stop before preview text and retain full addresses" {
test "reordered identical results preserve the selected row and warm line index" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
const sorted = "a.zig:2:1 first\na.zig:10:1 second\n";
try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "", try p.gpa.dupe(u8, sorted), null);
@@ -328,6 +335,7 @@ test "reordered identical results preserve the selected row and warm line index"
test "result sorting allocation failures preserve the previous answer and navigation" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "before", try p.gpa.dupe(u8, "a.zig:1:1 keep\n"), 0);
const id = source.search_pane.?;
@@ -360,6 +368,7 @@ test "result sorting allocation failures preserve the previous answer and naviga
test "Find sorts whole filenames and next location uses numeric columns" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("");
try panes.Output.fillResults(p, 0, "/tmp", .{ .cmd = .Find }, "", try p.gpa.dupe(u8, "x:2\nx:10\n"), 0);
const result = p.panes[source.search_pane.?].?;
@@ -374,6 +383,7 @@ test "Find sorts whole filenames and next location uses numeric columns" {
test "result stepping reaches an unterminated last row" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("one\ntwo\n");
const rows = try std.fmt.allocPrint(p.gpa, "{s}:1:1 first\n{s}:2:1 last", .{ source.file.?.path, source.file.?.path });
try panes.Output.fillResults(p, 0, "/tmp", .search, "", rows, 0);
@@ -604,6 +614,7 @@ test "EffectCode lists readable virtual sources used by this backend" {
test "every enabled setting builtin mutates the State Config reports" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
try std.testing.expectEqual(config.gui_tagline_font_percent, p.settings.font.tagline_percent);
var font_arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
@@ -730,6 +741,7 @@ test "n/N skips formatted context and selects comment match locations" {
for ([_]bool{ false, true }) |tslocations| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const text = "const Box = struct {\n" ++
" pub fn run() void {\n" ++
" // /tmp neighboring path\n" ++
@@ -742,7 +754,7 @@ test "n/N skips formatted context and selects comment match locations" {
p.gpa.free(source.file.?.path);
source.file.?.path = try p.gpa.dupe(u8, "/look-walk-context.zig");
p.presentation.enabled = false;
- p.locations_config = .{ .context = 1, .tscontext = true, .tslocations = tslocations };
+ p.locations_config = .{ .layout = .@"inline", .context = 1, .tscontext = true, .tslocations = tslocations };
const path = source.file.?.path;
const input = try std.fmt.allocPrint(p.gpa, "{s}:4:9 {s}\n{s}:6:9 {s}\n", .{
path, modal.lineSlice(text, 3), path, modal.lineSlice(text, 5),
@@ -798,6 +810,7 @@ test "n/N selects one output location per row and opens nothing" {
const paths = try projectPaths(&cwd_buf, &path_buf);
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
p.update(.{ .resize = .{ .cols = 100, .rows = 40 } });
const rid = try walkFixture(p, p.active, paths.cwd, "one");
const results = p.panes[rid].?;
@@ -837,6 +850,7 @@ test "n/N resumes the result output whose Look moved focus away" {
const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/look-owner.txt", .{tmp.sub_path});
const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
try std.testing.expect(p.executeBuiltinLine(0, "Look target"));
const rid = p.panes[0].?.search_pane orelse return error.MissingResults;
@@ -889,6 +903,7 @@ test "N exactly reverses n across output panes and the ring seam" {
const paths = try projectPaths(&cwd_buf, &path_buf);
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
p.update(.{ .resize = .{ .cols = 100, .rows = 40 } });
const first = try walkFixture(p, p.active, paths.cwd, "one");
p.update(.{ .key = .{ .cp = 'n', .alt = true } });
@@ -995,6 +1010,7 @@ test "identical result refresh keeps warm content caches without allocation" {
var allocator: std.testing.FailingAllocator = .init(std.testing.allocator, .{});
const p = try Pardes.init(allocator.allocator(), .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
p.settings.colors = true;
const text = "a.zig:1:1 pub fn first() void {}\na.zig:2:1 if (true) return;\n";
@@ -1088,6 +1104,7 @@ test "rejected result refresh frees incoming text and leaves the existing output
var allocator: std.testing.FailingAllocator = .init(std.testing.allocator, .{});
const p = try Pardes.init(allocator.allocator(), .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "before", try p.gpa.dupe(u8, "keep\n"), 1);
const id = source.search_pane.?;
@@ -1152,8 +1169,9 @@ test "result alignment chunks keep seventeen matches independent and refresh sta
for ([_]usize{ 7, 8 }) |long_at| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile("notes\n");
- p.locations_config = .{ .context = 0, .tscontext = false };
+ p.locations_config = .{ .layout = .@"inline", .context = 0, .tscontext = false };
var input: std.ArrayList(u8) = .empty;
defer input.deinit(p.gpa);
var widths: [17]usize = undefined;
@@ -1234,13 +1252,14 @@ test "result alignment chunks assign boundary context to nearest match with prev
for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
var source_text: std.ArrayList(u8) = .empty;
defer source_text.deinit(p.gpa);
for (0..14) |_| try source_text.appendSlice(p.gpa, "\t source();\n");
const source = try p.setTestFile(source_text.items);
p.gpa.free(source.file.?.path);
source.file.?.path = try p.gpa.dupe(u8, "/chunk-boundary-界.txt");
- p.locations_config = .{ .context = 2, .tscontext = false };
+ p.locations_config = .{ .layout = .@"inline", .context = 2, .tscontext = false };
var input: std.ArrayList(u8) = .empty;
defer input.deinit(p.gpa);
for ([_]usize{ 1, 2, 3, 4, 5, 6, 7, 8, 12 }) |line| {
@@ -1295,10 +1314,11 @@ test "declaration context ellipses mark only final header lines before omitted s
for ([_]u8{ 0, 1 }) |neighbors| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile(text);
p.gpa.free(source.file.?.path);
source.file.?.path = try p.gpa.dupe(u8, "/ellipsis-context.zig");
- p.locations_config = .{ .context = neighbors, .tscontext = true, .tslocations = visible };
+ p.locations_config = .{ .layout = .@"inline", .context = neighbors, .tscontext = true, .tslocations = visible };
const input = try std.fmt.allocPrint(p.gpa, "{s}:12:17 {s}\n{s}:16:9 {s}\n", .{
source.file.?.path, modal.lineSlice(text, 11), source.file.?.path, modal.lineSlice(text, 15),
});
@@ -1344,10 +1364,11 @@ test "LSP occurrence tint follows the supplied source span through context" {
for ([_]pardes.lsp.Kind{ .references, .definition }) |kind| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = try p.setTestFile(text);
p.gpa.free(source.file.?.path);
source.file.?.path = try p.gpa.dupe(u8, "/occurrence.zig");
- p.locations_config = .{ .context = 1, .tscontext = true };
+ p.locations_config = .{ .layout = .@"inline", .context = 1, .tscontext = true };
p.presentation.enabled = false;
var input: std.Io.Writer.Allocating = .init(p.gpa);
defer input.deinit();
@@ -1400,10 +1421,11 @@ test "result context upper bound stops at earliest enclosing declaration" {
for ([_]bool{ false, true }) |visible| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
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 };
+ p.locations_config = .{ .layout = .@"inline", .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.?;
@@ -1431,11 +1453,12 @@ test "result context upper bound stops at earliest enclosing declaration" {
for ([_][]const u8{ "/no-ancestor.zig", "/no-grammar.txt" }) |path| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
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 };
+ p.locations_config = .{ .layout = .@"inline", .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.?;
@@ -1445,3 +1468,120 @@ test "result context upper bound stops at earliest enclosing declaration" {
try std.testing.expectEqual(@as(usize, 2), source.search_row.?);
}
}
+
+test "stacked search and LSP results retain source tint navigation and dump ownership" {
+ if (!pardes.syntax.enabled) return error.SkipZigTest;
+ const first = "\tfoo + 界 + foo;";
+ const second = " return foo;";
+ const source_text = "const Box = struct {\n fn run() void {\n" ++ first ++ "\n const neighbor = 42;\n" ++ second ++ "\n }\n};\n";
+ for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| {
+ for ([_]bool{ false, true }) |visible| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 50 });
+ defer p.deinit();
+ try std.testing.expectEqual(.stacked, p.locations_config.layout);
+ p.locations_config = .{ .context = 1, .tscontext = true, .tslocations = visible };
+ p.presentation.enabled = false;
+ const source = try p.setTestFile(source_text);
+ p.gpa.free(source.file.?.path);
+ source.file.?.path = try p.gpa.dupe(u8, "/stacked-界.zig");
+ var input: std.Io.Writer.Allocating = .init(p.gpa);
+ defer input.deinit();
+ const first_col = std.mem.lastIndexOf(u8, first, "foo").?;
+ const second_col = std.mem.lastIndexOf(u8, second, "foo").?;
+ try pardes.lsp.sourceSpanRow(&input.writer, source.file.?.path, 2, first_col, 2, first_col + 3, first);
+ try pardes.lsp.sourceSpanRow(&input.writer, source.file.?.path, 4, second_col, 4, second_col + 3, second);
+ try panes.Output.fillResults(p, 0, "/", origin, "stacked", try p.gpa.dupe(u8, input.written()), 1);
+ const id = source.search_pane.?;
+ const pane = p.panes[id].?;
+ const file = &pane.file.?;
+ var matches: [2]usize = undefined;
+ var count: usize = 0;
+ var previews: usize = 0;
+ var context_addresses: usize = 0;
+ for (file.location_rows, 0..) |row, index| {
+ const line = modal.lineSlice(file.content, index);
+ switch (row.kind) {
+ .match => {
+ try std.testing.expectEqual(line.len, row.code_start);
+ try std.testing.expect(row.location_end > 0);
+ try std.testing.expect(!std.mem.containsAtLeast(u8, line, 1, "foo"));
+ matches[count] = index;
+ count += 1;
+ },
+ .preview => {
+ try std.testing.expect(index > 0);
+ try std.testing.expectEqual(.match, file.location_rows[index - 1].kind);
+ try std.testing.expectEqual(file.location_rows[index - 1].at, row.at);
+ try std.testing.expectEqualStrings(file.location_rows[index - 1].path, row.path);
+ try std.testing.expectEqual(@as(usize, 0), row.location_end);
+ try std.testing.expectEqual(@as(usize, 0), row.code_start);
+ try std.testing.expectEqualStrings(if (previews == 0) first else second, line);
+ previews += 1;
+ },
+ .context => {
+ if (row.location_end > 0) {
+ try std.testing.expect(visible and row.declaration);
+ try std.testing.expectEqual(line.len, row.code_start);
+ context_addresses += 1;
+ } else {
+ try std.testing.expectEqual(@as(usize, 0), row.code_start);
+ try std.testing.expectEqualStrings(modal.lineSlice(source_text, row.at.line - 1), line);
+ }
+ },
+ }
+ }
+ try std.testing.expectEqual(@as(usize, 2), count);
+ try std.testing.expectEqual(count, previews);
+ try std.testing.expectEqual(@as(usize, if (visible) 2 else 0), context_addresses);
+ try std.testing.expectEqual(matches[1], source.search_row.?);
+ const original_content = try p.gpa.dupe(u8, file.content);
+ defer p.gpa.free(original_content);
+ try panes.Output.fillResults(p, 0, "/", origin, "stacked", try p.gpa.dupe(u8, input.written()), 0);
+ try std.testing.expectEqual(id, source.search_pane.?);
+ try std.testing.expectEqualStrings(original_content, file.content);
+ pardes.layout.compute(p);
+ var arena = std.heap.ArenaAllocator.init(p.gpa);
+ defer arena.deinit();
+ const surface = try p.render(arena.allocator());
+ const chrome = pardes.ChromeTheme.fromTheme(p.theme());
+ var marked: usize = 0;
+ for (surface.cells) |cell| {
+ if (!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = chrome.search_bg })) continue;
+ try std.testing.expect(cell.text[0] == 'f' or cell.text[0] == 'o');
+ marked += 1;
+ }
+ try std.testing.expectEqual(@as(usize, 6), marked);
+ const number = std.mem.indexOf(u8, file.content, "42").?;
+ try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.number), file.highlights[number]);
+ p.active = id;
+ pane.mode = .normal;
+ p.armLookWalk(id);
+ for ([_]struct { key: u21, match: usize }{ .{ .key = 'n', .match = 0 }, .{ .key = 'n', .match = 1 }, .{ .key = 'N', .match = 0 } }) |step| {
+ p.update(.{ .key = .{ .cp = step.key } });
+ try std.testing.expectEqual(@as(i32, @intCast(matches[step.match])), pane.cur_row);
+ const row = file.location_rows[matches[step.match]];
+ const line = modal.lineSlice(file.content, matches[step.match]);
+ try std.testing.expectEqualStrings(line[0..row.location_end], selectedOutputText(pane) orelse "");
+ }
+ try p.dumpState();
+ const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
+ defer restored.deinit();
+ const restored_file = &restored.panes[id].?.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.at, after.at);
+ try std.testing.expectEqualStrings(before.path, after.path);
+ try std.testing.expectEqual(before.code_start, after.code_start);
+ try std.testing.expectEqual(before.location_end, after.location_end);
+ try std.testing.expectEqualSlices(u8, before.colors, after.colors);
+ }
+ restored.active = id;
+ restored.armLookWalk(id);
+ restored.update(.{ .key = .{ .cp = 'N' } });
+ try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.cur_row);
+ try std.testing.expectEqualStrings(source_text, source.file.?.content);
+ }
+ }
+}
diff --git a/test/panes.zig b/test/panes.zig
index d844f26e..28af8141 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -975,6 +975,7 @@ const TagNameTintTests = struct {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const pane = try p.setTestFile("body\n");
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/project/界file.zig");
@@ -2129,6 +2130,7 @@ const OutputTests = struct {
test "Locations align Unicode paths by display width and preserve source indentation" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("notes\n");
try fillResults(p, 0, "/", .search, "alignment", try p.gpa.dupe(u8, "/界.zig:2:1 first\n/longer-name.zig:12:3 \tsecond\n"), null);
const result = p.panes[p.panes[0].?.search_pane.?].?;
@@ -2150,9 +2152,10 @@ const OutputTests = struct {
for ([_]bool{ false, true }) |tslocations| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = "\tbefore\n\tmatch\n after\n";
const pane = try TreeContextTests.setZigFile(p, source);
- p.locations_config = .{ .context = 1, .tscontext = false, .tslocations = tslocations };
+ p.locations_config = .{ .layout = .@"inline", .context = 1, .tscontext = false, .tslocations = tslocations };
const path = pane.file.?.path;
try fillResults(p, 0, "/", .search, "neighbors", try std.fmt.allocPrint(p.gpa, "{s}:2:1 \tmatch\n", .{path}), 0);
const file = &p.panes[pane.search_pane.?].?.file.?;
@@ -2183,6 +2186,7 @@ const OutputTests = struct {
if (!syntax.enabled) return error.SkipZigTest;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = "const Box = struct {\n" ++
" pub fn run() void {\n" ++
" // unsaved before\n" ++
@@ -2297,6 +2301,7 @@ const OutputTests = struct {
if (!syntax.enabled) return error.SkipZigTest;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
const source = "const Outer = struct {\n" ++
" const Inner = struct {\n" ++
" pub fn run(\n" ++
@@ -2306,7 +2311,7 @@ const OutputTests = struct {
"\t\tcall();\n" ++
" }\n };\n};\n";
const pane = try TreeContextTests.setZigFile(p, source);
- p.locations_config = .{ .context = 1, .tscontext = true, .tslocations = true };
+ p.locations_config = .{ .layout = .@"inline", .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.?].?;
@@ -2431,6 +2436,7 @@ const OutputTests = struct {
test "generated result refills preserve unchanged selections and reset changed text" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("notes\n");
const content = "first\nsecond\nthird\n";
try fillResults(p, 0, "/", .{ .query = .hover }, "", try p.gpa.dupe(u8, content), null);
@@ -2474,6 +2480,7 @@ const OutputTests = struct {
test "generated result replacement cancels an in-progress body selection" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
+ p.locations_config.layout = .@"inline";
_ = try p.setTestFile("notes\n");
p.presentation.enabled = false;
try fillResults(p, 0, "/", .{ .query = .hover }, "", try p.gpa.dupe(u8, "first\nsecond\nthird\n"), null);
@@ -2513,6 +2520,7 @@ const OutputTests = struct {
.rows = 24,
});
defer p.deinit();
+ p.locations_config.layout = .@"inline";
var first: [max_arg]u8 = @splat('a');
var second = first;
diff --git a/test/pdf.zig b/test/pdf.zig
index 04b2e8b9..113ed8b9 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -1576,12 +1576,20 @@ test "PDF n/N addresses and reveals distinct MuPDF hits on one page" {
const results = p.panes[results_id].?.file.?;
var found: [3]bool = @splat(false);
for (results.location_rows, 0..) |row, index| {
- // PDF locations address page/hit pairs, not binary file lines.
+ // PDF locations address page/hit pairs, not binary file lines. Each
+ // address owns one snippet, which must not add a navigation stop.
+ if (index % 2 == 1) {
+ try std.testing.expectEqual(.preview, row.kind);
+ try std.testing.expectEqual(results.location_rows[index - 1].at, row.at);
+ try std.testing.expectEqual(@as(usize, 0), row.code_start);
+ try std.testing.expectEqualStrings("Pardes", pardes.modal.lineSlice(results.content, index));
+ continue;
+ }
try std.testing.expectEqual(.match, row.kind);
+ const line = pardes.modal.lineSlice(results.content, index);
+ try std.testing.expectEqual(line.len, row.code_start);
if (!std.mem.endsWith(u8, row.path, "design.pdf") or row.at.line != 1) continue;
if (row.at.col < 1 or row.at.col > found.len) continue;
- const line = pardes.modal.lineSlice(results.content, index);
- try std.testing.expectEqualStrings("Pardes", line[row.code_start..]);
found[row.at.col - 1] = true;
}
for (found) |present| try std.testing.expect(present);
@@ -1599,6 +1607,7 @@ test "PDF n/N addresses and reveals distinct MuPDF hits on one page" {
defer frame.deinit();
try std.testing.expect(pardes.test_api.searchStep(p, 0, 1));
try std.testing.expectEqual(@as(usize, 0), pv.search_hit);
+ try std.testing.expectEqual(@as(usize, 0), pane.search_row.?);
const first = try p.render(frame.allocator());
const revision = first.images[0].?.native.revision;
const raster = panes.Pdf.rasterForPage(pv, pv.page).?;
@@ -1617,12 +1626,14 @@ test "PDF n/N addresses and reveals distinct MuPDF hits on one page" {
try std.testing.expect(pardes.test_api.searchStep(p, 0, 1));
try std.testing.expectEqual(@as(usize, 1), pv.search_hit);
+ try std.testing.expectEqual(@as(usize, 2), pane.search_row.?);
_ = frame.reset(.retain_capacity);
const second = try p.render(frame.allocator());
try std.testing.expectEqual(revision, second.images[0].?.native.revision);
try std.testing.expect(pardes.test_api.searchStep(p, 0, 1));
try std.testing.expectEqual(@as(usize, 2), pv.search_hit);
+ try std.testing.expectEqual(@as(usize, 4), pane.search_row.?);
_ = frame.reset(.retain_capacity);
const third = try p.render(frame.allocator());
try std.testing.expectEqual(revision, third.images[0].?.native.revision);