diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-11 12:38:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 682e237df7e8b22f820d14a4adee58e6e2f84268 (patch) | |
| tree | 5d888cb46f1a49e89fc55e2a4c232d3af0265b1d /src/panes.zig | |
| parent | 6878e1c309172d624c0d7a3555f6f0217ae6770e (diff) | |
| download | pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.tar.gz pardes-682e237df7e8b22f820d14a4adee58e6e2f84268.zip | |
trunk: resume before the Reload experiment
Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip.
Diffstat (limited to 'src/panes.zig')
| -rw-r--r-- | src/panes.zig | 158 |
1 files changed, 139 insertions, 19 deletions
diff --git a/src/panes.zig b/src/panes.zig index 6db7f53b..6a5a2739 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -83,6 +83,8 @@ pub const Pane = struct { serial: u32 = 0, mode: Mode = .normal, vweight: f32 = 1, + /// A folded pane keeps its expansion weight, but occupies only its tag row. + collapsed: bool = false, cols: u16, rows: u16, greet: bool = false, @@ -125,6 +127,12 @@ pub const Pane = struct { tag_mode: Mode = .normal, tag_col: u16 = 0, tag_anchor: u16 = 0, + /// Filename edits are staged separately from the live buffer identity. + /// Enter commits the name; leaving tag editing discards the draft. + tag_name: [limits.host_path_cap]u8 = undefined, + tag_name_len: ?u16 = null, + /// Display columns hidden to reveal the tag caret; tag addresses stay whole. + tag_scroll: u16 = 0, ed_undo: [Terminal.history_max]Terminal.Snapshot = undefined, ed_undo_len: usize = 0, ed_redo: [Terminal.history_max]Terminal.Snapshot = undefined, @@ -965,6 +973,7 @@ pub const File = struct { .cols = pane.cols, .rows = pane.rows, .vweight = pane.vweight, + .collapsed = pane.collapsed, .file = .{ .path = file.path, .content = file.content, @@ -1728,7 +1737,6 @@ pub const File = struct { pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, active: bool) void { const s = &p.surface; const prefix_width = gutterWidth(pane); - const ch = p.chromeTheme(); const goff = pane.scroll(); const gcur = Terminal.gridCursor(pane); const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, gcur.y) + goff; @@ -1745,10 +1753,11 @@ pub const File = struct { while (c < prefix_width and c < tw) : (c += 1) { const cell = s.at(tx + c, body_y + vr); cell.default = false; // paints blank gutter rows too - if (on_cursor) { - cell.style.fg = .{ .rgb = ch.tag_fg }; - cell.style.bg = .{ .rgb = ch.tag_bg }; - } else cell.style.fg = .{ .rgb = ch.lineno }; + cell.style.fg = .{ .rgb = if (on_cursor) + p.theme().lineno_active orelse p.theme().lineno + else + p.theme().lineno }; + cell.style.bold = on_cursor; } } } @@ -1758,10 +1767,10 @@ pub const File = struct { fn synStyle(p: *Pardes, sy: syntax.Syn) ?SynStyle { return switch (sy) { .none => null, - .keyword => .{ .fg = p.theme().kw, .bold = true }, + .keyword => .{ .fg = p.theme().kw, .bold = p.settings.syntax_bold }, .string => .{ .fg = p.theme().str, .bold = false }, .number => .{ .fg = p.theme().num, .bold = false }, - .comment => .{ .fg = p.theme().comment, .bold = true }, + .comment => .{ .fg = p.theme().comment, .bold = false }, }; } @@ -1770,7 +1779,7 @@ pub const File = struct { pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *State, r: pardes.Rect, tx: u16, tw: u16, body_h: u16) void { const highlights = if (f.mini) |mini| mini.colors else f.highlights; const highlight_start = if (f.mini != null) 0 else f.highlight_start; - if (highlights.len == 0) return; + if (highlights.len == 0 and f.output == null) return; const s = &p.surface; const prefix_width = gutterWidth(pane); const tz_recolor = tracy.zone(@src(), "synRecolor"); @@ -1803,6 +1812,7 @@ pub const File = struct { line.len; } hs = modal.graphemeStart(line, @min(hs, line.len)); + const decoration = Output.rowDecoration(f.output, line); var c: usize = 0; var screen_c: usize = 0; while (hs + c < limit and prefix_width + screen_c < tw) { @@ -1823,6 +1833,18 @@ pub const File = struct { } } } + // Results keep their exact live text. Only the location, + // matched source range and diagnostic label gain emphasis. + if (decoration.styleAt(p, hs + c)) |style| { + var fill: usize = 0; + while (fill < cells and prefix_width + screen_c + fill < tw) : (fill += 1) { + const cell = s.at(tx + @as(u16, @intCast(prefix_width + screen_c + fill)), body_y + vr); + if (cell.default) continue; + cell.style.fg = style.fg; + cell.style.bold = style.bold; + if (style.bg != .default) cell.style.bg = style.bg; + } + } screen_c += cells; c = grapheme_end - hs; } @@ -1910,6 +1932,80 @@ pub const Output = struct { } }; + const RowDecoration = struct { + location_end: usize = 0, + mark_start: usize = 0, + mark_end: usize = 0, + severity: enum { none, err, warning, info, hint } = .none, + + fn styleAt(row: RowDecoration, p: *const Pardes, byte: usize) ?pardes.CellStyle { + // Body paper changes immediately, so its ink must change with it + // even while the surrounding tags are still fading. + const chrome = pardes.ChromeTheme.fromTheme(p.theme()); + if (byte < row.location_end) return .{ .fg = .{ .rgb = chrome.lineno } }; + if (byte < row.mark_start or byte >= row.mark_end) return null; + if (row.severity == .none) return .{ + .fg = .{ .rgb = chrome.search_fg }, + .bg = .{ .rgb = chrome.search_bg }, + }; + return .{ .fg = .{ .rgb = switch (row.severity) { + .err => chrome.diagnostic_error, + .warning => chrome.diagnostic_warning, + .info => chrome.diagnostic_info, + .hint => chrome.diagnostic_hint, + .none => unreachable, + } }, .bold = true }; + } + }; + + fn rowDecoration(output: ?State, line: []const u8) 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 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) + { + row.mark_start = source_start +| (target.at.col - 1); + row.mark_end = @min(line.len, source_start +| target.at.end_col); + } + const is_diagnostic = switch (origin) { + .query => |kind| kind == .diagnostics or kind == .workspace_diagnostics, + else => false, + }; + if (is_diagnostic) { + const text = line[source_start..]; + inline for (.{ .{ "error:", .err }, .{ "warning:", .warning }, .{ "info:", .info }, .{ "hint:", .hint } }) |label| { + if (std.mem.startsWith(u8, text, label[0])) { + row.mark_start = source_start; + row.mark_end = source_start + label[0].len; + row.severity = label[1]; + } + } + } + return row; + } + + test "result decoration follows source byte ranges and diagnostic provenance" { + const row = rowDecoration(.{ .from = .search }, "src/a.rs:3:5-7 let abc = 1;"); + try std.testing.expectEqualStrings("abc", "src/a.rs:3:5-7 let abc = 1;"[row.mark_start..row.mark_end]); + const prose = rowDecoration(.{ .from = .{ .query = .hover } }, "src/a.rs:3:5-7 error: prose"); + try std.testing.expectEqual(@as(usize, 0), prose.mark_end); + const diagnostic = rowDecoration(.{ .from = .{ .query = .diagnostics } }, "src/a.rs:3:5-7 warning: unused"); + try std.testing.expectEqual(.warning, diagnostic.severity); + try std.testing.expectEqualStrings("warning:", "src/a.rs:3:5-7 warning: unused"[diagnostic.mark_start..diagnostic.mark_end]); + const malformed = rowDecoration(.{ .from = .search }, "src/a.rs:3:99999999999999999999-99999999999999999999 text"); + try std.testing.expect(malformed.mark_end <= "src/a.rs:3:99999999999999999999-99999999999999999999 text".len); + } + pub fn setArg(o: *State, text: []const u8) error{ArgumentTooLong}!void { if (text.len > max_arg) return error.ArgumentTooLong; o.arg_len = @intCast(text.len); @@ -2215,12 +2311,23 @@ pub const Output = struct { pub fn openThemes(p: *Pardes, id: usize) !void { var out: std.Io.Writer.Allocating = .init(p.gpa); errdefer out.deinit(); - for (pardes.themes) |t| + try out.writer.writeAll("# Pardes themes\n"); + for (pardes.themes, 0..) |t, i| { + if (i == pardes.native_theme_count) + try out.writer.writeAll("\n# Legacy and imported themes\n"); try out.writer.print(comptime config.Runtime.findAction(.theme).?.word ++ " {s}\n", .{t.name}); + } const content = try out.toOwnedSlice(); try openStepped(p, id, .{ .cmd = .ThemeSel }, content); } + pub fn isWalkRow(out: ?State, line: []const u8) bool { + const state = out orelse return true; + if (!std.meta.eql(state.from, Origin{ .cmd = .ThemeSel })) return true; + // Section labels are for reading, not stops in the command walk. + return std.mem.startsWith(u8, std.mem.trimStart(u8, line, " \t"), comptime config.Runtime.findAction(.theme).?.word ++ " "); + } + pub fn openFonts(p: *Pardes, id: usize) !void { if (builtins.capabilities.font_picker) { const arena = p.scratch.allocator(); @@ -2827,6 +2934,7 @@ pub const Image = struct { .cols = pane.cols, .rows = pane.rows, .vweight = pane.vweight, + .collapsed = pane.collapsed, .image = .{ .path = path, .bytes_b64 = if (bytes.len > 0) try dump.encodeBytes(arena, bytes) else "", @@ -3026,6 +3134,15 @@ pub const Pdf = struct { }; pub fn legacySavedPrefix(path: []const u8, saved_tag: []const u8) ?[]const u8 { + if (std.mem.startsWith(u8, saved_tag, path) and std.mem.startsWith(u8, saved_tag[path.len..], " [")) { + const start = path.len + 2; + const end = std.mem.indexOfScalarPos(u8, saved_tag, start, ']') orelse return null; + var parts = std.mem.splitScalar(u8, saved_tag[start..end], '/'); + _ = std.fmt.parseInt(usize, parts.next() orelse return null, 10) catch return null; + _ = std.fmt.parseInt(usize, parts.next() orelse return null, 10) catch return null; + if (parts.next() != null) return null; + return saved_tag[0 .. end + 1]; + } if (!std.mem.startsWith(u8, saved_tag, "pdf ")) return null; const marker = " PdfSections "; const marker_at = std.mem.indexOf(u8, saved_tag, marker) orelse return null; @@ -6513,6 +6630,7 @@ pub const Terminal = struct { .cols = pane.cols, .rows = pane.rows, .vweight = pane.vweight, + .collapsed = pane.collapsed, .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), .stream = try arena.dupe(u8, text), @@ -6561,6 +6679,7 @@ pub const Terminal = struct { .cols = pane.cols, .rows = pane.rows, .vweight = pane.vweight, + .collapsed = pane.collapsed, .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), .stream = stream[0..len], @@ -7193,7 +7312,8 @@ pub const Terminal = struct { test "terminal Filter keys indexed truecolor OSC and background-only cells through the theme" { const testing = std.testing; - const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 18, .rows = 6 }); + // Raw index assertions require a theme that inherits the ANSI palette. + const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 18, .rows = 6 }); defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; @@ -7332,10 +7452,8 @@ pub const Terminal = struct { } test "terminal Filter keeps extended keys dark-to-light on a light theme" { - const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + const p = try Pardes.init(std.testing.allocator, .{ .startup_config = "Theme acme\n", .tty_only = true }); defer p.deinit(); - // Curated order is a public theme contract: helix, dark, acme. - p.settings.theme = 2; try std.testing.expectEqualStrings("acme", p.theme().name); var cache: FilterPalette = .{}; const palette = cache.get(p.theme()); @@ -7385,7 +7503,9 @@ pub const Terminal = struct { const pane = p.panes[0].?; try testing.expect(pane.tty_filter); - for (0..3) |t| { + // Native dark/light palettes plus legacy helix/dark/acme, including + // the null-page fallback that only the legacy dark theme exercises. + for (0..9) |t| { p.settings.theme = @intCast(t); const stage_one = FilteredColors.init(p, pane); // `dark` declares no background of its own, which is exactly why the @@ -7424,7 +7544,7 @@ pub const Terminal = struct { const tx = r.x + config.GUTTER; const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - for (0..3) |t| { + for (0..9) |t| { p.settings.theme = @intCast(t); var fc = FilteredColors.init(p, pane); const page = asPardesColor(fc.default_bg); @@ -7476,7 +7596,7 @@ pub const Terminal = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - for (0..3) |t| { + for (0..9) |t| { p.settings.theme = @intCast(t); var fc = FilteredColors.init(p, pane); var refused: usize = 0; @@ -7515,8 +7635,8 @@ pub const Terminal = struct { const r = p.rects[0]; const tx = r.x + config.GUTTER; const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const red: pardes.Color = .{ .index = 1 }; - const blue: pardes.Color = .{ .index = 4 }; + const red: pardes.Color = .{ .rgb = p.theme().palette.?[1] }; + const blue: pardes.Color = .{ .rgb = p.theme().palette.?[4] }; // tty mode projects the emulator's ansi colours cell for cell. pane.mode = .tty; @@ -7554,7 +7674,7 @@ pub const Terminal = struct { const s = try p.render(frame.allocator()); try testing.expectEqualStrings(" ", s.at(tx, body_y).grapheme()); - try testing.expect(!std.meta.eql(pardes.Color{ .index = 1 }, s.at(tx, body_y).style.bg)); + try testing.expect(!std.meta.eql(pardes.Color{ .rgb = p.theme().palette.?[1] }, s.at(tx, body_y).style.bg)); } pub fn palColor(p: *Pardes, idx: u8) pardes.Color { |
