summaryrefslogtreecommitdiff
path: root/src/locations.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/locations.zig')
-rw-r--r--src/locations.zig258
1 files changed, 258 insertions, 0 deletions
diff --git a/src/locations.zig b/src/locations.zig
new file mode 100644
index 00000000..8f8546f6
--- /dev/null
+++ b/src/locations.zig
@@ -0,0 +1,258 @@
+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;
+pub const context_prefix = "| ";
+
+pub const Row = struct {
+ kind: enum { match, context },
+ 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 !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 = 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 };
+ }
+ 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,
+ 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 (matches.items[first..end]) |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] = @max(wanted[context_row], 1);
+ }
+ for (declarations) |declaration| {
+ if (declaration.start_line > row) break;
+ if (declaration.end_line < row) continue;
+ 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;
+ }
+ }
+ 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);
+ }
+ }
+ }
+ }
+ 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, pardes.panes.File.displayWidth(row.label) + @as(usize, if (row.original == null) 2 else 0));
+ 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;
+ 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);
+ 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, .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 appendContext(arena: std.mem.Allocator, rows: *std.ArrayList(Pending), path: []const u8, source: []const u8, row: usize, 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,
+ .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..]);
+}