1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
|
const std = @import("std");
const pardes = @import("pardes.zig");
const look = @import("look.zig");
const syntax = @import("syntax.zig");
const filesystem = @import("fs.zig");
pub const Config = @import("locations_config.zig").Config;
/// Legacy marked rows and hidden locations remain recognizable without metadata.
/// Visible context locations are flush left and use Row.kind instead.
pub fn isContextLine(line: []const u8) bool {
return std.mem.startsWith(u8, line, " ") 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,
code_start: usize,
colors: []u8 = &.{},
};
pub fn freeRows(gpa: std.mem.Allocator, rows: []Row) void {
for (rows) |row| {
gpa.free(row.path);
gpa.free(row.colors);
}
gpa.free(rows);
}
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 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;
}
return true;
}
/// 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 {
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');
for (rows) |row| {
if (row.kind != .match) return false;
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 (raw_lines.next()) |tail| if (tail.len != 0) return false;
return raw_lines.next() == null;
}
pub const Parsed = struct {
path: []const u8,
at: look.Spot,
end: usize,
code_start: usize,
};
/// The tab separates alignment from code; source indentation after it is data.
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 = 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);
}
fn parseRaw(line: []const u8) Parsed {
var at: usize = 1;
while (at < line.len) : (at += 1) {
if (line[at] != ':' or at + 1 == line.len or !std.ascii.isDigit(line[at + 1])) continue;
const start = at;
at += 1;
while (at < line.len and (std.ascii.isDigit(line[at]) or line[at] == ':' or line[at] == '-')) at += 1;
if (at < line.len and line[at] != ' ' and line[at] != '\t') continue;
const target = look.parsePathLine(line[start..at]);
if (target.at.line > 0 and target.end == at - start)
return .{ .path = line[0..start], .at = target.at, .end = at, .code_start = @min(line.len, at + 1) };
}
return .{ .path = line, .at = .{}, .end = line.len, .code_start = line.len };
}
pub const Result = struct {
content: []u8,
rows: []Row,
anchor: ?usize,
};
const Pending = struct {
path: []const u8,
at: look.Spot,
label: []const u8,
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 {
if (a.at.line != b.at.line) return a.at.line < b.at.line;
return (a.original orelse std.math.maxInt(usize)) < (b.original orelse std.math.maxInt(usize));
}
};
fn sourceText(p: *pardes.Pardes, arena: std.mem.Allocator, dir: []const u8, path: []const u8) ?[]const u8 {
if (std.ascii.eqlIgnoreCase(std.fs.path.extension(path), ".pdf")) return null;
const lexical = std.fs.path.resolvePosix(arena, &.{ dir, path }) catch return null;
var pathbuf: [4096]u8 = undefined;
const full = if (filesystem.resolve(p, path, dir, &pathbuf)) |resolved| resolved.path else lexical;
for (p.panes) |slot| if (slot) |pane| {
if (pane.file) |file| if (file.output == null and (std.mem.eql(u8, file.path, full) or std.mem.eql(u8, file.path, lexical))) return file.content;
};
const bytes = filesystem.read(p, full) catch return null;
defer p.gpa.free(bytes);
return arena.dupe(u8, bytes) catch null;
}
/// Inputs are sorted results. Expand each source group, merge context with
/// matches, then align once across the final rows. 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();
var matches: std.ArrayList(Pending) = .empty;
const body = input[0 .. input.len - @intFromBool(std.mem.endsWith(u8, input, "\n"))];
var lines = std.mem.splitScalar(u8, body, '\n');
while (lines.next()) |line| {
const target = parseRaw(line);
if (target.at.line == 0) return .{ .content = try p.gpa.dupe(u8, input), .rows = &.{}, .anchor = anchor };
try matches.append(arena, .{ .path = target.path, .at = target.at, .label = line[0..target.end], .code = line[target.code_start..], .original = matches.items.len });
}
var pending: std.ArrayList(Pending) = .empty;
var first: usize = 0;
while (first < matches.items.len) {
var end = first + 1;
while (end < matches.items.len and std.mem.eql(u8, matches.items[first].path, matches.items[end].path)) : (end += 1) {}
const path = matches.items[first].path;
const group_start = pending.items.len;
try pending.appendSlice(arena, matches.items[first..end]);
if (expand and (p.locations_config.context > 0 or p.locations_config.tscontext)) {
if (sourceText(p, arena, dir, path)) |source| {
var source_lines: std.ArrayList([]const u8) = .empty;
var source_split = std.mem.splitScalar(u8, source, '\n');
while (source_split.next()) |line| try source_lines.append(arena, line);
const count = source_lines.items.len;
const wanted = try arena.alloc(u8, count);
@memset(wanted, 0);
const source_colors = syntax.highlightFileRange(arena, path, source, 0, source.len) catch &.{};
const declarations = if (p.locations_config.tscontext)
syntax.contextDeclarations(arena, path, source) catch &.{}
else
&.{};
for (pending.items[group_start..]) |*match| {
const row = match.at.line -| 1;
if (row >= count) continue;
const lo = row -| p.locations_config.context;
const hi = @min(count -| 1, row +| p.locations_config.context);
var context_row = lo;
while (context_row <= hi) : (context_row += 1) {
if (context_row == row) continue;
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] |= 2;
}
}
for (wanted, 0..) |kind, row| {
if (kind > 0) {
const line = source_lines.items[row];
const offset = @intFromPtr(line.ptr) - @intFromPtr(source.ptr);
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);
}
}
}
}
std.mem.sort(Pending, pending.items[group_start..], {}, Pending.lessThan);
// Keep every original result, but show each extra source row once.
var write = group_start;
for (pending.items[group_start..]) |row| {
if (row.original == null and write > group_start and pending.items[write - 1].at.line == row.at.line) continue;
pending.items[write] = row;
write += 1;
}
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;
errdefer {
for (rows.items) |row| {
p.gpa.free(row.path);
p.gpa.free(row.colors);
}
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);
}
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;
}
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 labelWidth(row: Pending) usize {
return pardes.panes.File.displayWidth(row.label) +
@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 },
.label = if (hidden) "" else try std.fmt.allocPrint(arena, "{s}:{d}", .{ path, row + 1 }),
.code = source,
.original = null,
.hidden = hidden,
.declaration = declaration,
.colors = colors,
});
}
test "locations tab delimiter preserves source indentation and spaced paths" {
const row = parse("some directory/界.zig:12:3-5 \t\t value");
try std.testing.expectEqualStrings("some directory/界.zig", row.path);
try std.testing.expectEqual(@as(usize, 12), row.at.line);
try std.testing.expectEqualStrings("\t value", "some directory/界.zig:12:3-5 \t\t value"[row.code_start..]);
}
test "locations native results retain the first source tab" {
const text = "some directory/a.zig:3:2 \t value";
const row = parseRaw(text);
try std.testing.expectEqualStrings("some directory/a.zig", row.path);
try std.testing.expectEqualStrings("\t value", text[row.code_start..]);
}
|