summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-11 12:38:56 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit682e237df7e8b22f820d14a4adee58e6e2f84268 (patch)
tree5d888cb46f1a49e89fc55e2a4c232d3af0265b1d /src/panes.zig
parent6878e1c309172d624c0d7a3555f6f0217ae6770e (diff)
downloadpardes-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.zig158
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 {