summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/config.md7
-rw-r--r--src/locations.zig94
-rw-r--r--test/output.zig123
3 files changed, 199 insertions, 25 deletions
diff --git a/docs/config.md b/docs/config.md
index 2474c6b0..ffd1fb3f 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -159,8 +159,11 @@ LocationsConfig context:5 tscontext:on tslocations:off
`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. All visible locations start flush left; `n` and `N` still stop on matches.
+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`
+still stop on matches.
With `tscontext` enabled, asterisks after a match location show its declaration
depth (for example, `main.zig:42 ***`). Ordinary neighboring source lines retain
syntax colors; declaration headers do not.
diff --git a/src/locations.zig b/src/locations.zig
index ebc87a2e..43d99005 100644
--- a/src/locations.zig
+++ b/src/locations.zig
@@ -46,14 +46,31 @@ pub fn sameInput(input: []const u8, rendered: []const u8, rows: []const Row) boo
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');
- for (rows) |row| {
+ var width: usize = 0;
+ for (rows, 0..) |row, index| {
if (row.kind != .match) return false;
+ if (index % alignment_entries == 0) {
+ width = 0;
+ var chunk = raw_lines;
+ for (0..alignment_entries) |_| {
+ const line = chunk.next() orelse break;
+ const target = parseRaw(line);
+ width = @max(width, pardes.panes.File.displayWidth(line[0..target.end]));
+ }
+ }
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;
+ // 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;
+ const padding = shown[row.location_end..row.code_start];
+ 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;
}
if (raw_lines.next()) |tail| if (tail.len != 0) return false;
return raw_lines.next() == null;
@@ -136,7 +153,7 @@ fn sourceText(p: *pardes.Pardes, arena: std.mem.Allocator, dir: []const u8, path
}
/// Inputs are sorted results. Expand each source group, merge context with
-/// matches, then align once across the final rows. All temporary source data
+/// matches, then align groups of eight matches. All temporary source data
/// stays in the caller's scratch arena; returned text and rows are owned.
pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?usize, expand: bool) !Result {
const arena = p.scratch.allocator();
@@ -210,8 +227,6 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
pending.items.len = write;
first = end;
}
- var width: usize = 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;
@@ -223,32 +238,65 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us
rows.deinit(p.gpa);
}
var mapped = anchor;
- for (pending.items, 0..) |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);
+ 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 out.writer.writeAll(row.code);
+ 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;
}
- 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 (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;
}
const owned_rows = try rows.toOwnedSlice(p.gpa);
errdefer freeRows(p.gpa, owned_rows);
return .{ .content = try out.toOwnedSlice(), .rows = owned_rows, .anchor = mapped };
}
+const alignment_entries = 8;
+
+/// Keep neighboring context with the nearer match at a chunk boundary. Shared
+/// context stays deduplicated; ties belong to the preceding chunk.
+fn alignmentEnd(rows: []const Pending, first: usize) usize {
+ var matches: usize = 0;
+ var previous: usize = first;
+ for (rows[first..], first..) |row, index| {
+ if (row.original == null) continue;
+ if (matches == alignment_entries) {
+ var end = index;
+ while (end > previous + 1) {
+ const context = rows[end - 1];
+ if (!std.mem.eql(u8, context.path, row.path)) break;
+ if (std.mem.eql(u8, rows[previous].path, row.path) and
+ row.at.line -| context.at.line >= context.at.line -| rows[previous].at.line) break;
+ end -= 1;
+ }
+ return end;
+ }
+ matches += 1;
+ previous = index;
+ }
+ return rows.len;
+}
+
fn labelWidth(row: Pending) usize {
return pardes.panes.File.displayWidth(row.label) +
@as(usize, if (row.depth > 0) row.depth + 1 else 0);
diff --git a/test/output.zig b/test/output.zig
index f1d9d4e0..b0f964ee 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -1146,3 +1146,126 @@ test "Look ignores slash-only comment markers while retaining root and real path
try std.testing.expect(look.resolve(null, doubled, ".", &realbuf) == .dir);
try std.testing.expect(look.resolve(null, "https://example.com/path", ".", &realbuf) == .url);
}
+
+test "result alignment chunks keep seventeen matches independent and refresh stable" {
+ for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| {
+ for ([_]usize{ 7, 8 }) |long_at| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const source = try p.setTestFile("notes\n");
+ p.locations_config = .{ .context = 0, .tscontext = false };
+ var input: std.ArrayList(u8) = .empty;
+ defer input.deinit(p.gpa);
+ var widths: [17]usize = undefined;
+ for (0..17) |reverse| {
+ const index = 16 - reverse;
+ const label = try std.fmt.allocPrint(p.gpa, "/r{d:0>2}{s}.zig:3:1", .{ index, if (index == long_at) "-界-very-long-path-name" else "-é" });
+ defer p.gpa.free(label);
+ widths[index] = panes.File.displayWidth(label);
+ try input.appendSlice(p.gpa, label);
+ try input.appendSlice(p.gpa, " \t return 7;\n");
+ }
+ try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 0);
+ const result_id = source.search_pane.?;
+ const result = p.panes[result_id].?;
+ const file = &result.file.?;
+ try std.testing.expectEqual(@as(usize, 17), file.location_rows.len);
+ try std.testing.expectEqual(@as(usize, 16), source.search_row.?);
+ var starts: [17]usize = undefined;
+ for (file.location_rows, 0..) |row, index| {
+ const line = modal.lineSlice(file.content, index);
+ const first = index / 8 * 8;
+ var width: usize = 0;
+ for (widths[first..@min(first + 8, widths.len)]) |label_width| width = @max(width, label_width);
+ try std.testing.expectEqual(width, panes.File.displayWidth(line[0 .. row.code_start - 1]));
+ try std.testing.expectEqual(@as(u8, '\t'), line[row.code_start - 1]);
+ try std.testing.expectEqualStrings("\t return 7;", line[row.code_start..]);
+ try std.testing.expectEqualStrings(row.path, line[0 .. row.location_end - ":3:1".len]);
+ starts[index] = row.code_start;
+ }
+ const content = file.content.ptr;
+ const revision = file.revision;
+ const index = try panes.File.lineIndex(p.gpa, file);
+ result.cur_row = 8;
+ result.cur_col = @intCast(file.location_rows[8].code_start);
+ try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 8);
+ try std.testing.expectEqual(result_id, source.search_pane.?);
+ try std.testing.expectEqual(content, file.content.ptr);
+ try std.testing.expectEqual(revision, file.revision);
+ try std.testing.expectEqual(index.ptr, file.line_starts.ptr);
+ try std.testing.expectEqual(@as(usize, 8), source.search_row.?);
+ try std.testing.expectEqual(@as(i32, 8), result.cur_row);
+ for (file.location_rows, starts) |row, start| try std.testing.expectEqual(start, row.code_start);
+
+ // Simulate a restored view made by the previous global-padding
+ // formatter. Its metadata still describes the same result bytes.
+ const expected = try p.gpa.dupe(u8, file.content);
+ defer p.gpa.free(expected);
+ var global_width: usize = 0;
+ for (widths) |width| global_width = @max(global_width, width);
+ var legacy: std.ArrayList(u8) = .empty;
+ defer legacy.deinit(p.gpa);
+ for (file.location_rows, 0..) |*row, row_index| {
+ const line = modal.lineSlice(file.content, row_index);
+ const label = line[0..row.location_end];
+ const code = line[row.code_start..];
+ try legacy.appendSlice(p.gpa, label);
+ const padding = global_width - panes.File.displayWidth(label);
+ try legacy.appendNTimes(p.gpa, ' ', padding);
+ try legacy.append(p.gpa, '\t');
+ try legacy.appendSlice(p.gpa, code);
+ try legacy.append(p.gpa, '\n');
+ row.code_start = row.location_end + padding + 1;
+ }
+ const retained_rows = file.location_rows;
+ file.location_rows = &.{};
+ panes.File.setContent(p, file, try legacy.toOwnedSlice(p.gpa));
+ file.location_rows = retained_rows;
+ try std.testing.expect(!std.mem.eql(u8, expected, file.content));
+ try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 8);
+ try std.testing.expectEqual(result_id, source.search_pane.?);
+ try std.testing.expectEqualStrings(expected, file.content);
+ for (file.location_rows, starts) |row, start| try std.testing.expectEqual(start, row.code_start);
+ }
+ }
+}
+
+test "result alignment chunks assign boundary context to nearest match with previous ties" {
+ for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ 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 };
+ var input: std.ArrayList(u8) = .empty;
+ defer input.deinit(p.gpa);
+ for ([_]usize{ 1, 2, 3, 4, 5, 6, 7, 8, 12 }) |line| {
+ const entry = try std.fmt.allocPrint(p.gpa, "{s}:{d}:1 \t source();\n", .{ source.file.?.path, line });
+ defer p.gpa.free(entry);
+ try input.appendSlice(p.gpa, entry);
+ }
+ try panes.Output.fillResults(p, 0, "/", origin, "context chunks", try p.gpa.dupe(u8, input.items), 8);
+ const file = &p.panes[source.search_pane.?].?.file.?;
+ try std.testing.expectEqual(@as(usize, 14), file.location_rows.len);
+ try std.testing.expectEqual(@as(usize, 11), source.search_row.?);
+ const first_label = try std.fmt.allocPrint(p.gpa, "{s}:8:1", .{source.file.?.path});
+ defer p.gpa.free(first_label);
+ const next_label = try std.fmt.allocPrint(p.gpa, "{s}:12:1", .{source.file.?.path});
+ defer p.gpa.free(next_label);
+ for (file.location_rows, 0..) |row, index| {
+ const line = modal.lineSlice(file.content, index);
+ try std.testing.expectEqual(index + 1, row.at.line);
+ const expected = if (row.at.line <= 10) first_label else next_label;
+ try std.testing.expectEqual(panes.File.displayWidth(expected), panes.File.displayWidth(line[0 .. row.code_start - 1]));
+ try std.testing.expectEqualStrings("\t source();", line[row.code_start..]);
+ if (row.at.line >= 9 and row.at.line != 12) {
+ try std.testing.expectEqual(.context, row.kind);
+ try std.testing.expectEqual(@as(usize, 0), row.location_end);
+ } else try std.testing.expectEqual(.match, row.kind);
+ }
+ }
+}