summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/panes.zig')
-rw-r--r--src/panes.zig208
1 files changed, 174 insertions, 34 deletions
diff --git a/src/panes.zig b/src/panes.zig
index 32fe8c28..1487e0ef 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -16,6 +16,7 @@ const build_options = @import("pardes_config");
const lsp = @import("lsp/lsp.zig");
const image = @import("image.zig");
const look = @import("look.zig");
+const locations = @import("locations.zig");
const Key = pardes.Key;
/// An owned editable buffer and the absolute surface row of its first line.
@@ -1011,6 +1012,7 @@ pub const File = struct {
line_starts: []usize = &.{},
/// One tree_sitter_gpa-owned syntax.Syn byte per highlighted source byte.
highlights: []u8 = &.{},
+ location_rows: []locations.Row = &.{},
highlight_start: usize = 0,
syntax_dirty: bool = true,
tree_context: bool = false,
@@ -1040,6 +1042,7 @@ pub const File = struct {
.collapsed = pane.collapsed,
.file = .{
.tree_context = file.tree_context,
+ .location_rows = try dumpLocationRows(arena, file.location_rows),
.path = file.path,
.content = file.content,
.content_b64 = try dump.encodeBytes(arena, file.content),
@@ -1052,6 +1055,87 @@ pub const File = struct {
};
}
+ fn dumpLocationRows(arena: std.mem.Allocator, rows: []const locations.Row) ![]const dump.LocationRow {
+ const saved = try arena.alloc(dump.LocationRow, rows.len);
+ for (rows, saved) |row, *out| out.* = .{
+ .kind = @enumFromInt(@intFromEnum(row.kind)),
+ .path = row.path,
+ .line = row.at.line,
+ .col = row.at.col,
+ .end_line = row.at.end_line,
+ .end_col = row.at.end_col,
+ .location_end = row.location_end,
+ .code_start = row.code_start,
+ .colors_b64 = try dump.encodeBytes(arena, row.colors),
+ };
+ return saved;
+ }
+
+ 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;
+ const rows = try gpa.alloc(locations.Row, saved.len);
+ var initialized: usize = 0;
+ errdefer {
+ for (rows[0..initialized]) |row| {
+ gpa.free(row.path);
+ gpa.free(row.colors);
+ }
+ gpa.free(rows);
+ }
+ var lines = std.mem.splitScalar(u8, content, '\n');
+ for (saved, rows) |row, *out| {
+ 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.kind == .context) != std.mem.startsWith(u8, line, locations.context_prefix) 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 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);
+ errdefer gpa.free(colors);
+ if (colors.len != 0 and colors.len != code_len) return error.InvalidLocationRows;
+ for (colors) |color| if (color > @intFromEnum(syntax.Syn.comment)) return error.InvalidLocationRows;
+ const path = try gpa.dupe(u8, row.path);
+ out.* = .{
+ .kind = @enumFromInt(@intFromEnum(row.kind)),
+ .path = path,
+ .at = .{ .line = row.line, .col = row.col, .end_line = row.end_line, .end_col = row.end_col },
+ .location_end = row.location_end,
+ .code_start = row.code_start,
+ .colors = colors,
+ };
+ initialized += 1;
+ }
+ if (lines.next()) |remaining| if (remaining.len != 0 or lines.next() != null) return error.InvalidLocationRows;
+ return rows;
+ }
+
+ test "location metadata restore rejects offsets and invalid syntax bytes" {
+ const gpa = std.testing.allocator;
+ var row: dump.LocationRow = .{
+ .kind = .context,
+ .path = "x.zig",
+ .line = 1,
+ .location_end = 0,
+ .code_start = 3,
+ };
+ const rows = try restoreLocationRows(gpa, "| \tx\n", &.{row});
+ defer locations.freeRows(gpa, rows);
+ try std.testing.expectEqualStrings("x.zig", rows[0].path);
+ row.code_start = 8;
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "| \tx\n", &.{row}));
+ row.code_start = 3;
+ row.colors_b64 = "/w==";
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "| \tx\n", &.{row}));
+ row.colors_b64 = "";
+ row.kind = .match;
+ try std.testing.expectError(error.InvalidLocationRows, restoreLocationRows(gpa, "| \tx\n", &.{row}));
+ }
+
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;
@@ -1495,6 +1579,10 @@ pub const File = struct {
break :blk value;
} else null;
+ if (saved.location_rows.len != 0 and output == null) return error.InvalidLocationRows;
+ const location_rows = try restoreLocationRows(p.gpa, content, saved.location_rows);
+ errdefer locations.freeRows(p.gpa, location_rows);
+
var mini: ?Mini.State = null;
errdefer if (mini) |*state| state.deinit(p.gpa);
if (saved.mini_source.len > 0) {
@@ -1516,6 +1604,7 @@ pub const File = struct {
.content = content,
.output = output,
.mini = mini,
+ .location_rows = location_rows,
.scroll = src.scroll,
.revision = @intFromBool(saved.dirty),
.history = history,
@@ -1538,6 +1627,7 @@ pub const File = struct {
if (file.mini) |*mini| mini.deinit(p.gpa);
if (file.line_starts.len > 0) p.gpa.free(file.line_starts);
if (file.highlights.len > 0) p.tree_sitter_gpa.free(file.highlights);
+ locations.freeRows(p.gpa, file.location_rows);
if (file.context_declarations.len > 0) p.tree_sitter_gpa.free(file.context_declarations);
for (file.history.undo[0..file.history.undo_len]) |snap| p.gpa.free(snap.content);
for (file.history.redo[0..file.history.redo_len]) |snap| p.gpa.free(snap.content);
@@ -1554,6 +1644,8 @@ pub const File = struct {
}
pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
+ locations.freeRows(p.gpa, f.location_rows);
+ f.location_rows = &.{};
for (p.panes) |slot| if (slot) |pane| {
if (pane.file) |*file| if (file == f) {
for (0..pane.sel.len) |button| {
@@ -1712,7 +1804,10 @@ pub const File = struct {
@max(start, lineStart(p.gpa, f, f.scroll + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS + slack));
const new_highlights = (switch (pane.colorAlgo()) {
.diff => syntax.highlightDiff(p.tree_sitter_gpa, f.content, start, end),
- .locations => syntax.highlightLocations(p.tree_sitter_gpa, f.content, start, end),
+ .locations => if (f.location_rows.len > 0)
+ syntax.highlightLocationRows(p.tree_sitter_gpa, f.content, f.location_rows)
+ else
+ syntax.highlightLocations(p.tree_sitter_gpa, f.content, start, end),
else => syntax.highlightFileRange(p.tree_sitter_gpa, f.path, f.content, start, end),
}) catch {
f.syntax_dirty = false;
@@ -1925,7 +2020,9 @@ pub const File = struct {
line.len;
}
hs = modal.graphemeStart(line, @min(hs, line.len));
- const decoration = Output.rowDecoration(f.output, line);
+ const row_index: usize = @intCast(@max(0, pane.wrapAt(vr).line));
+ const metadata: ?locations.Row = if (row_index < f.location_rows.len) f.location_rows[row_index] else null;
+ const decoration = Output.decorateRow(f.output, line, metadata);
var c: usize = 0;
var screen_c: usize = 0;
while (hs + c < limit and prefix_width + screen_c < tw) {
@@ -2072,23 +2169,28 @@ pub const Output = struct {
};
fn rowDecoration(output: ?State, line: []const u8) RowDecoration {
+ return decorateRow(output, line, null);
+ }
+
+ fn decorateRow(output: ?State, line: []const u8, metadata: ?locations.Row) RowDecoration {
const origin = (output orelse return .{}).from;
- const token_end = std.mem.indexOfAny(u8, line, " \t") orelse line.len;
- const target = look.parsePathLine(line[0..token_end]);
- if (target.at.line == 0 or target.end != token_end) return .{};
- var row: RowDecoration = .{ .location_end = token_end };
- if (token_end == line.len) return row;
- const source_start = token_end + 1;
+ const target = locations.parse(line);
+ const spot = if (metadata) |m| m.at else target.at;
+ if (spot.line == 0) return .{};
+ var row: RowDecoration = .{ .location_end = if (metadata) |m| m.location_end else target.end };
+ const source_start = if (metadata) |m| m.code_start else target.code_start;
+ if (source_start >= line.len) return row;
+ if (metadata) |m| if (m.kind == .context) return row;
const is_search = switch (origin) {
.search => true,
.cmd => |cmd| cmd == .Grep,
else => false,
};
- if (is_search and target.at.col > 0 and target.at.end_line == target.at.line and
- target.at.end_col >= target.at.col)
+ if (is_search and spot.col > 0 and spot.end_line == spot.line and
+ spot.end_col >= spot.col)
{
- row.mark_start = source_start +| (target.at.col - 1);
- row.mark_end = @min(line.len, source_start +| target.at.end_col);
+ row.mark_start = source_start +| (spot.col - 1);
+ row.mark_end = @min(line.len, source_start +| spot.end_col);
}
const is_diagnostic = switch (origin) {
.query => |kind| kind == .diagnostics or kind == .workspace_diagnostics,
@@ -2216,22 +2318,13 @@ pub const Output = struct {
};
pub fn location(line: []const u8) Location {
- const text = std.mem.trimEnd(u8, line, " \t\r\n");
- var at: usize = 1;
- while (at < text.len) {
- if (text[at] != ':' or at + 1 == text.len or !std.ascii.isDigit(text[at + 1])) {
- at += 1;
- continue;
- }
- const start = at;
- at += 1;
- while (at < text.len and (std.ascii.isDigit(text[at]) or text[at] == ':' or text[at] == '-')) at += 1;
- if (at < text.len and text[at] != ' ' and text[at] != '\t') continue;
- const parsed = look.parsePathLine(text[start..at]);
- if (parsed.at.line == 0 or parsed.end != at - start) continue;
- return .{ .path = text[0..start], .at = parsed.at, .end = at };
- }
- return .{ .path = text, .at = .{}, .end = text.len };
+ const target = locations.parse(std.mem.trimEnd(u8, line, " \t\r\n"));
+ return .{ .path = target.path, .at = target.at, .end = target.end };
+ }
+
+ pub fn isResultRow(file: *const File.State, row: usize) bool {
+ if (row < file.location_rows.len) return file.location_rows[row].kind == .match;
+ return !std.mem.startsWith(u8, modal.lineSlice(file.content, row), locations.context_prefix);
}
fn sortResults(arena: std.mem.Allocator, from: Origin, content: []u8, anchor: ?usize) !?usize {
@@ -2328,6 +2421,7 @@ pub const Output = struct {
var first: ?usize = null;
var row: usize = 0;
while (lines.next()) |line| : (row += 1) {
+ if (std.mem.startsWith(u8, line, locations.context_prefix)) continue;
const target = location(line);
if (target.at.line == 0) continue;
if (first == null) first = row;
@@ -2352,10 +2446,45 @@ pub const Output = struct {
}
pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8, initial_anchor: ?usize) !void {
- errdefer p.gpa.free(content);
+ var result_content = content;
+ errdefer p.gpa.free(result_content);
+ var result_rows: []locations.Row = &.{};
+ errdefer locations.freeRows(p.gpa, result_rows);
const pane = p.panes[id] orelse return error.MissingPane;
- const anchor = try sortResults(p.scratch.allocator(), from, content, initial_anchor);
+ if (arg.len > max_arg) return error.ArgumentTooLong;
+ var anchor = try sortResults(p.scratch.allocator(), from, content, initial_anchor);
const by_arg = std.meta.activeTag(from) != .query;
+ if (p.locations_config.context == 0 and !p.locations_config.tscontext) {
+ for (p.panes, 0..) |slot, i| {
+ if (i == id) continue;
+ const rp = slot orelse continue;
+ const rf = if (rp.file) |*f| f else continue;
+ const output = if (rf.output) |*o| o else continue;
+ 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;
+ try setArg(output, arg);
+ p.gpa.free(content);
+ p.active = id;
+ if (traits(from).steps) {
+ pane.search_pane = i;
+ pane.search_row = anchor;
+ p.armLookWalk(i);
+ }
+ return;
+ }
+ }
+ const format_locations = traits(from).steps and !traits(from).commands and
+ !std.meta.eql(from, Origin{ .cmd = .Find }) and
+ !std.meta.eql(from, Origin{ .query = .completion });
+ if (format_locations) {
+ const formatted = try locations.format(p, dir, content, anchor, true);
+ p.gpa.free(result_content);
+ result_content = formatted.content;
+ result_rows = formatted.rows;
+ anchor = formatted.anchor;
+ }
for (p.panes, 0..) |slot, i| {
if (i == id) continue;
const rp = slot orelse continue;
@@ -2365,12 +2494,22 @@ pub const Output = struct {
if (by_arg and !std.mem.eql(u8, o.arg(), arg)) continue;
if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue;
try setArg(o, arg);
- if (std.mem.eql(u8, rf.content, content)) {
- p.gpa.free(content);
+ if (std.mem.eql(u8, rf.content, result_content)) {
+ p.gpa.free(result_content);
} else {
- File.setContent(p, rf, content);
+ File.setContent(p, rf, result_content);
resetBody(p, rp);
}
+ if (locations.equalRows(rf.location_rows, result_rows)) {
+ locations.freeRows(p.gpa, result_rows);
+ } else {
+ locations.freeRows(p.gpa, rf.location_rows);
+ rf.location_rows = result_rows;
+ if (rf.highlights.len > 0) p.tree_sitter_gpa.free(rf.highlights);
+ rf.highlights = &.{};
+ rf.highlight_start = 0;
+ rf.syntax_dirty = true;
+ }
p.active = id;
if (traits(from).steps) {
pane.search_pane = i;
@@ -2380,7 +2519,8 @@ pub const Output = struct {
return;
}
const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, content);
+ const np = try open(p, free, dir, from, arg, result_content);
+ np.file.?.location_rows = result_rows;
p.placeDoc(id, free, np);
p.active = id;
if (traits(from).steps) {