diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 13:21:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 2bdde8c5ec1049ea0f575c6f9165ed4cef00ea50 (patch) | |
| tree | 0d7879e387223bd19d78a295bee0ba7ab3a62e00 /src/locations.zig | |
| parent | 8ec8924cd577f71953f8e420af1cd60da92f3300 (diff) | |
| download | pardes-2bdde8c5ec1049ea0f575c6f9165ed4cef00ea50.tar.gz pardes-2bdde8c5ec1049ea0f575c6f9165ed4cef00ea50.zip | |
Align location results and configure source context
Diffstat (limited to 'src/locations.zig')
| -rw-r--r-- | src/locations.zig | 258 |
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..]); +} |
