diff options
Diffstat (limited to 'src/panes.zig')
| -rw-r--r-- | src/panes.zig | 208 |
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) { |
