summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig286
1 files changed, 250 insertions, 36 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index c4893b25..7fe4d95f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -14,6 +14,7 @@ const uucode = @import("uucode");
const modal = @import("modal.zig");
const look = @import("look.zig");
const syntax = @import("syntax.zig");
+const tracy = @import("tracy.zig");
pub const image = @import("image.zig");
pub const dump = @import("dump.zig");
@@ -31,6 +32,9 @@ pub const BOX_H: u16 = 1;
pub const MINW: u16 = 10;
pub const MINH: u16 = 3;
pub const PREFIX_W: u16 = 5;
+/// vim 'scrolloff': keyboard cursor moves keep this many context rows visible
+/// above/below the cursor (clamped at file boundaries and short panes)
+const scroll_off = 3;
pub const topbar_str = "Kill Newcol Tutor Debug Colors NextColor Dump";
const pane_builtins_str = "Del";
@@ -462,6 +466,14 @@ pub const Pane = struct {
vsel: CharSel = .{},
/// one pending prefix codepoint (0 = none): `g` and `z`
pending: u21 = 0,
+ /// `/` file search: the pattern is typed into the tag tail (no popup);
+ /// search_at is the tail length before " /" was appended (submit/cancel
+ /// restore it). n/N walk the grep results in pane search_pane, starting
+ /// just below search_row (the results shell's prompt row at submit).
+ search_edit: bool = false,
+ search_at: u16 = 0,
+ search_pane: ?usize = null,
+ search_row: i32 = 0,
/// the editable tag tail: seeded with the default builtins on first touch;
/// edited in place by a modal one-line editor sharing the pane's mode
tag_tail: std.ArrayList(u8) = .empty,
@@ -523,9 +535,19 @@ pub const Pane = struct {
}
fn ensureCursorVisible(pane: *Pane) void {
+ // scrolloff margin, shrunk on short panes so the band stays non-empty
+ const margin: i32 = @min(scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2));
const off = pane.scroll();
const last = off + @as(i32, pane.rows) - 1;
- if (pane.cur_row < off) pane.scrollBy(pane.cur_row - off) else if (pane.cur_row > last) pane.scrollBy(pane.cur_row - last);
+ if (pane.cur_row < off + margin) {
+ pane.scrollBy(pane.cur_row - margin - off); // scrollBy clamps at line 0
+ } else if (pane.cur_row > last - margin) {
+ // don't scroll a file past EOF-at-bottom-row (vim's bottom clamp);
+ // terminals overshoot harmlessly — the vt clamps at the live bottom
+ var to = pane.cur_row + margin;
+ if (pane.file) |f| to = @min(to, @as(i32, @intCast(modal.lineCount(f.content) -| 1)));
+ pane.scrollBy(@max(0, to - last));
+ }
}
/// the last navigable row: file -> last content line; terminal -> viewport bottom
@@ -628,9 +650,11 @@ pub const Pardes = struct {
/// press during an active middle select-drag; consumed on middle release
chord_arg: ?[]u8 = null,
- effects: [64]Effect = undefined,
+ /// pending effects, drained by the shell after each update. Growable: one
+ /// update can emit unbounded writes (a big selection executed is chunked
+ /// into 64-byte write effects) — a fixed ring overflows and panics.
+ effects: std.ArrayList(Effect) = .empty,
effects_head: usize = 0,
- effects_tail: usize = 0,
/// modal yank register (gpa-owned); a yank also mirrors out to the system
/// clipboard via the set_clipboard effect.
@@ -678,6 +702,7 @@ pub const Pardes = struct {
if (p.yank) |y| gpa.free(y);
if (p.dump_out) |d| gpa.free(d);
if (p.chord_arg) |a| gpa.free(a);
+ p.effects.deinit(gpa);
p.scratch.deinit();
gpa.free(p.surface.cells);
gpa.destroy(p);
@@ -717,6 +742,7 @@ pub const Pardes = struct {
fn newShell(p: *Pardes, id: usize, cwd: []const u8) !*Pane {
std.debug.assert(p.panes[id] == null);
+ if (cwd.len > 256) return error.PathTooLong; // spawn effect cwd is a Buf(256)
const pane = try p.gpa.create(Pane);
pane.* = .{
.vt = try ghostty_vt.Terminal.init(p.gpa, .{
@@ -780,14 +806,16 @@ pub const Pardes = struct {
}
fn emit(p: *Pardes, e: Effect) void {
- std.debug.assert(p.effects_tail - p.effects_head < p.effects.len);
- p.effects[p.effects_tail % p.effects.len] = e;
- p.effects_tail += 1;
+ p.effects.append(p.gpa, e) catch {};
}
pub fn nextEffect(p: *Pardes) ?Effect {
- if (p.effects_head == p.effects_tail) return null;
- const e = p.effects[p.effects_head % p.effects.len];
+ if (p.effects_head == p.effects.items.len) {
+ p.effects.clearRetainingCapacity();
+ p.effects_head = 0;
+ return null;
+ }
+ const e = p.effects.items[p.effects_head];
p.effects_head += 1;
return e;
}
@@ -840,13 +868,13 @@ pub const Pardes = struct {
return std.fmt.allocPrint(arena, "img {s} Petscii {s} Ascii {s}", .{ iv.path, mode, pane_builtins_str });
}
if (pane.file) |f| {
- const fmode = if (pane.mode == .insert) "in" else "nm";
+ const fmode = if (pane.mode == .insert) "INS" else "NOR";
return std.fmt.allocPrint(arena, "{s} {s}", .{ fmode, f.path });
}
const mode = switch (pane.mode) {
- .normal => "nm",
- .insert => "in",
- .tty => "sy",
+ .normal => "NOR",
+ .insert => "INS",
+ .tty => "TTY",
};
return std.fmt.allocPrint(arena, "{s} {s}", .{ mode, pane.cwdSlice() });
}
@@ -878,6 +906,7 @@ pub const Pardes = struct {
fn exitTagEdit(pane: *Pane) void {
pane.tag_edit = false;
pane.tag_sel = false;
+ pane.search_edit = false; // an abandoned `/` input stays as tag text
}
fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } {
@@ -1145,12 +1174,14 @@ pub const Pardes = struct {
nt.greet = true;
const f = p.layoutFindTerm(p.active).?;
p.layoutInsert(f.col, f.idx + 1, free);
+ p.splitBelow(p.active, nt);
p.active = free;
return;
}
if (key.alt and !key.ctrl and key.cp == 'c') {
const f = p.layoutFindTerm(p.active).?;
if (p.ncol < MAX_COLS and p.col_n[f.col] > 1) {
+ p.absorbVWeight(p.active);
p.layoutRemove(p.active);
p.layoutAppendColumn(p.active);
}
@@ -1165,6 +1196,16 @@ pub const Pardes = struct {
} else p.enterTty(p.active);
return;
}
+ // `/` search input in flight: Enter greps, Esc abandons; both restore
+ // the tag tail and hand focus back to the body.
+ if (pane.search_edit and (is(key, Key.enter) or is(key, Key.escape))) {
+ if (is(key, Key.enter)) p.submitSearch(pane);
+ pane.tag_tail.shrinkRetainingCapacity(@min(pane.search_at, pane.tag_tail.items.len));
+ exitTagEdit(pane);
+ pane.mode = .normal;
+ pane.pending = 0;
+ return;
+ }
// normal-mode Enter/Tab: the acme chords — look at (Enter) or execute
// (Tab) the modal selection, else the file-ish word under the cursor
// (on the tag tail when it's focused for editing, else the body).
@@ -1522,6 +1563,129 @@ pub const Pardes = struct {
if (is(key, 'y')) return p.normalYank(pane);
if (is(key, 'u')) return p.doUndo(pane);
if (is(key, 'U')) return p.doRedo(pane);
+ // file search: `/` types a pattern into the tag, n/N walk the results
+ if (pane.file != null and is(key, '/')) return p.startSearch(pane);
+ if (pane.file != null and is(key, 'n')) return p.searchStep(pane, 1);
+ if (pane.file != null and is(key, 'N')) return p.searchStep(pane, -1);
+ // terminal n/N: look the next/previous output row that looks somewhere
+ if (pane.isTerminal() and is(key, 'n')) return p.lookStep(pane, pl, 1);
+ if (pane.isTerminal() and is(key, 'N')) return p.lookStep(pane, pl, -1);
+ }
+
+ // ---- file search (`/` + n/N): grep into a shell pane, look the results ----
+
+ /// `/` on a file pane: type the pattern into the tag tail after a " /"
+ /// marker — the existing modal one-line editor, visible while typing,
+ /// nothing that disappears. Enter/Esc are intercepted in handleKey.
+ fn startSearch(p: *Pardes, pane: *Pane) void {
+ if (!pane.tag_init) {
+ pane.tag_tail.appendSlice(p.gpa, file_pane_tail) catch return;
+ pane.tag_init = true;
+ }
+ pane.search_at = @intCast(pane.tag_tail.items.len);
+ pane.tag_tail.appendSlice(p.gpa, " /") catch return;
+ pane.search_edit = true;
+ pane.tag_edit = true;
+ pane.tag_sel = false;
+ pane.mode = .insert;
+ pane.pending = 0;
+ pane.tag_col = @intCast(pane.tag_tail.items.len);
+ }
+
+ /// Enter on a `/` input: run `grep -nH '<pat>' <path>` in a shell in the
+ /// file's directory (the same path middle-click send takes), so the
+ /// results are ordinary persistent terminal text. Remember the results
+ /// pane and start n/N below its current prompt row.
+ fn submitSearch(p: *Pardes, pane: *Pane) void {
+ const f = pane.file orelse return;
+ const tail = pane.tag_tail.items;
+ const pat = std.mem.trim(u8, tail[@min(@as(usize, pane.search_at) + 2, tail.len)..], " \t\r\n");
+ if (pat.len == 0) return;
+ const arena = p.scratch.allocator();
+ var cmd: std.ArrayList(u8) = .empty;
+ cmd.appendSlice(arena, "grep -nH '") catch return;
+ for (pat) |ch| {
+ if (ch == '\'') cmd.appendSlice(arena, "'\\''") catch return else cmd.append(arena, ch) catch return;
+ }
+ cmd.appendSlice(arena, "' '") catch return;
+ cmd.appendSlice(arena, f.path) catch return;
+ cmd.append(arena, '\'') catch return;
+ const dst = p.ttyForDir(std.fs.path.dirname(f.path) orelse "/") orelse return;
+ const rp = p.panes[dst] orelse return;
+ p.padOutputBelowEdits(dst);
+ p.emitWrite(dst, cmd.items);
+ p.emitWrite(dst, "\r");
+ pane.search_pane = dst;
+ // absolute row of the shell's cursor (the prompt echoing the grep);
+ // results land below it, so n scans from there — older output and
+ // stale results of a previous search stay behind
+ const sb = rp.vt.screens.active.pages.scrollbar();
+ pane.search_row = @intCast(sb.total -| rp.rows + rp.vt.screens.active.cursor.y);
+ }
+
+ /// n/N: select the next/previous grep result row in the results pane and
+ /// look it. Result rows read `path:NNN:...` and are found by their path
+ /// prefix; prompt/echo rows are already blanked by paneCursorLines (the
+ /// OSC 133 semantic row marks), so only real output is walked.
+ fn searchStep(p: *Pardes, pane: *Pane, delta: i32) void {
+ const f = pane.file orelse return;
+ const rid = pane.search_pane orelse return;
+ const rp = p.panes[rid] orelse return;
+ if (!rp.isTerminal()) return;
+ const pl = p.paneCursorLines(rp) catch return;
+ const nrows: i32 = @intCast(pl.lines.len);
+ var r = pane.search_row + delta;
+ while (r >= 0 and r < nrows) : (r += delta) {
+ const ln = pl.lines[@intCast(r)];
+ if (!std.mem.startsWith(u8, ln, f.path) or ln.len <= f.path.len or ln[f.path.len] != ':') continue;
+ pane.search_row = r;
+ // select the result line in the results pane and keep it in view
+ rp.msel = .{ .active = true, .r0 = r, .r1 = r };
+ rp.vsel.active = false;
+ rp.cur_row = r;
+ rp.cur_col = 0;
+ rp.cur_off = 0;
+ rp.cur_pinned = true;
+ rp.ensureCursorVisible();
+ // look the leading file-ish word (path:NNN:) — the ordinary look
+ // path scrolls this file pane to the line
+ var hi: usize = 0;
+ while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
+ p.actOnSelection(.right, rid, ln[0..hi], null);
+ return;
+ }
+ // no result that way (or none at all): nowhere to go, stay put
+ }
+
+ /// n/N on a terminal pane (normal mode): select the next/previous output
+ /// row whose leading file-ish word looks somewhere — resolves to a
+ /// file/dir/path:NNN exactly as a right click would — and look it. The
+ /// scan only resolves (realpath, no pane opens, no scrolls); the one
+ /// chosen row gets the real look. The modal cursor is the position, so
+ /// the walk continues from wherever j/k left it. The look may focus what
+ /// it opened; focus comes back here so the next n keeps stepping.
+ fn lookStep(p: *Pardes, pane: *Pane, pl: PaneLines, delta: i32) void {
+ const id = p.active;
+ const nrows: i32 = @intCast(pl.lines.len);
+ var realbuf: [4096]u8 = undefined;
+ var r = pane.cur_row + delta;
+ while (r >= 0 and r < nrows) : (r += delta) {
+ const ln = pl.lines[@intCast(r)];
+ var hi: usize = 0;
+ while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
+ if (hi == 0 or look.resolve(ln[0..hi], pane.cwdSlice(), &realbuf) == .none) continue;
+ pane.msel = .{ .active = true, .r0 = r, .r1 = r };
+ pane.vsel.active = false;
+ pane.cur_row = r;
+ pane.cur_col = 0;
+ pane.cur_off = 0;
+ pane.cur_pinned = true;
+ pane.ensureCursorVisible();
+ p.actOnSelection(.right, id, ln[0..hi], null);
+ p.active = id;
+ return;
+ }
+ // nothing lookable that way: stay put
}
const InsertAt = enum { at, append, line_start, line_end, open_below, open_above };
@@ -2433,29 +2597,49 @@ pub const Pardes = struct {
p.ncol += 1;
}
- /// Hand a dying pane's vertical weight to ONE sibling (the pane above, or
- /// below for the topmost) so the rest of the column keeps its sizes — the
- /// deletion mirror of splitBelow. Call while `id` is still in the layout.
+ /// Snap every pane in column `c` to its on-screen row count so later
+ /// weight edits move ONLY the panes they name: computeGeom rounds
+ /// round(avail*w/vsum) per pane, and with fractional weights a split or
+ /// absorb elsewhere in the column can jiggle a bystander by a row.
+ fn snapColWeights(p: *Pardes, c: usize) void {
+ for (0..p.col_n[c]) |k| {
+ const pid = p.col_terms[c][k];
+ if (p.panes[pid]) |pp| pp.vweight = @floatFromInt(@max(1, p.rects[pid].h));
+ }
+ }
+
+ /// Hand a dying pane's rows to ONE sibling (the pane above, or below for
+ /// the topmost) so the rest of the column keeps its sizes bit-identical —
+ /// the deletion mirror of splitBelow. Call while `id` is still in the
+ /// layout, with rects current.
fn absorbVWeight(p: *Pardes, id: usize) void {
const f = p.layoutFindTerm(id) orelse return;
if (p.col_n[f.col] <= 1) return;
- const gone = p.panes[id] orelse return;
+ p.snapColWeights(f.col);
const sib = if (f.idx > 0) p.col_terms[f.col][f.idx - 1] else p.col_terms[f.col][f.idx + 1];
- if (p.panes[sib]) |s| s.vweight += gone.vweight;
+ if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h)));
}
/// Open a window BELOW `src` (acme-style) without rebalancing the column:
/// shrink ONLY src to its content height (cursor row kept visible) and hand
- /// the freed rows to `nw` — together they fill src's old slot.
+ /// the freed rows to `nw` — together they fill src's old slot and the other
+ /// panes keep their sizes bit-identical (weights snap to row counts).
fn splitBelow(p: *Pardes, src_id: usize, nw: *Pane) void {
const src = p.panes[src_id] orelse return;
const src_h = p.rects[src_id].h;
const body: u16 = if (src_h > BOX_H) src_h - BOX_H else 1;
const cur: u16 = if (src.file != null) body / 2 else @as(u16, @intCast(src.vt.screens.active.cursor.y)) + 1;
- const keep = std.math.clamp(cur, 1, @max(1, body -| 1));
- const w = src.vweight;
- src.vweight = w * @as(f32, @floatFromInt(BOX_H + keep)) / @as(f32, @floatFromInt(src_h));
- nw.vweight = w - src.vweight;
+ // cap keep so a content-full source still leaves the new pane a tag +
+ // a few body rows (an Alt-n from a full shell was born 0 rows tall)
+ const keep = std.math.clamp(cur, 1, @max(1, body -| (BOX_H + 3)));
+ if (p.layoutFindTerm(src_id)) |f| for (0..p.col_n[f.col]) |k| {
+ const pid = p.col_terms[f.col][k];
+ if (p.panes[pid]) |pp| if (pp != nw) {
+ pp.vweight = @floatFromInt(@max(1, p.rects[pid].h));
+ };
+ };
+ src.vweight = @floatFromInt(BOX_H + keep);
+ nw.vweight = @floatFromInt(@max(1, src_h -| (BOX_H + keep)));
}
/// a shell nobody has touched: no typing, cursor on the first prompt line,
@@ -2482,6 +2666,7 @@ pub const Pardes = struct {
if (n_tty != 1) return;
const tt = p.panes[tty_id] orelse return;
if (!isPristineTty(tt)) return;
+ p.computeGeom(); // a just-stacked doc has no rect yet; absorb snaps to rows
p.absorbVWeight(tty_id);
p.layoutRemove(tty_id);
p.deinitPane(tt);
@@ -2501,6 +2686,7 @@ pub const Pardes = struct {
const nt = p.newShell(free, dir) catch return null;
nt.greet = false;
const rc = if (p.ncol > 0) p.ncol - 1 else 0;
+ if (p.col_n[rc] > 0) p.splitBelow(p.col_terms[rc][p.col_n[rc] - 1], nt);
p.layoutInsert(rc, p.col_n[rc], free);
return free;
}
@@ -2558,18 +2744,17 @@ pub const Pardes = struct {
}
/// stack a fresh doc pane at the top of the LEFT column (acme convention:
- /// files left, terminals right) with a fair share, then evict a leftover
- /// pristine shell
+ /// files left, terminals right), halving ONLY the old top pane's slot so
+ /// the rest of the column keeps its sizes; then evict a leftover pristine
+ /// shell
fn stackDocLeft(p: *Pardes, free: usize, nt: *Pane) void {
const lc = 0;
- const ln = p.col_n[lc];
- if (ln > 0) {
- var vsum: f32 = 0;
- for (0..ln) |i| if (p.panes[p.col_terms[lc][i]]) |pp| {
- vsum += pp.vweight;
- };
- nt.vweight = vsum / @as(f32, @floatFromInt(ln));
- }
+ if (p.col_n[lc] > 0) if (p.panes[p.col_terms[lc][0]]) |top| {
+ p.snapColWeights(lc);
+ const h = p.rects[p.col_terms[lc][0]].h;
+ nt.vweight = @floatFromInt(@max(1, h / 2));
+ top.vweight = @floatFromInt(@max(1, h -| h / 2));
+ };
p.layoutInsert(lc, 0, free);
p.active = free;
p.evictLonePristineTty(lc, free);
@@ -2607,7 +2792,7 @@ pub const Pardes = struct {
};
switch (look.resolve(txt, cwd, &realbuf)) {
.none => {},
- .url => |u| p.emit(.{ .open_link = .from(u) }),
+ .url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
// focus an existing terminal on this dir, else fork one below
for (p.panes, 0..) |slot, i| {
@@ -2631,11 +2816,17 @@ pub const Pardes = struct {
if (slot) |tt| if (tt.file) |*ff| if (std.mem.eql(u8, ff.path, target.path)) {
p.active = i;
if (target.line > 0 and target.line <= fileLineCount(ff.content)) {
- const next = target.line - 1;
+ const next = (target.line - 1) -| tt.rows / 2; // center, clamp at top
if (next != ff.scroll) {
ff.scroll = next;
ff.syntax_dirty = true;
}
+ // land the modal cursor on the target line (and keep
+ // ensureCursorVisible agreeing with the recenter —
+ // a stale cursor would yank the view right back)
+ tt.cur_row = @intCast(target.line - 1);
+ tt.cur_col = 0;
+ tt.cur_pinned = true;
}
return;
};
@@ -2643,6 +2834,11 @@ pub const Pardes = struct {
const free = p.freeSlot() orelse return;
const nt = p.openFileView(free, target.path, target.line) catch return;
p.placeDoc(id, free, nt);
+ // center the target line: the pane's real body height only
+ // exists after placement, so lay out now and pull the
+ // scroll up by half a body (line 0 opens stay at the top)
+ p.computeGeom();
+ nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
},
.image => |target| {
for (p.panes, 0..) |slot, i| {
@@ -2918,7 +3114,7 @@ pub const Pardes = struct {
if (src.scroll > 0) pane.vt.screens.active.scroll(.{ .delta_row = -@as(isize, @intCast(src.scroll)) });
}
p.setCwd(i, t.cwd);
- if (std.mem.startsWith(u8, src.tag, "sy ")) pane.mode = .tty;
+ if (std.mem.startsWith(u8, src.tag, "TTY ")) pane.mode = .tty;
p.restoreTail(pane, src.tag);
},
.file => {
@@ -3077,6 +3273,8 @@ pub const Pardes = struct {
/// re-highlight the visible window of any file whose syntax went stale
/// (edit, scroll, load) — visible-range-first so big files stay snappy
fn refreshDirtyHighlights(p: *Pardes) void {
+ const tz = tracy.zone(@src(), "refreshDirtyHighlights");
+ defer tz.end();
for (p.panes) |slot| {
const pane = slot orelse continue;
if (pane.file == null) continue;
@@ -3226,6 +3424,8 @@ pub const Pardes = struct {
}
fn renderPane(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, active: bool) !void {
+ const tz = tracy.zone(@src(), "renderPane");
+ defer tz.end();
if (r.w <= GUTTER or r.h == 0) return;
const s = &p.surface;
const th = p.theme();
@@ -3278,7 +3478,9 @@ pub const Pardes = struct {
.fg = if (th.fg) |c| .{ .rgb = c } else .default,
.bg = if (th.bg) |c| .{ .rgb = c } else .default,
};
+ const tz_body = tracy.zone(@src(), "bodyText");
const body = try p.bodyText(arena, pane);
+ tz_body.end();
var it = std.mem.splitScalar(u8, body, '\n');
var i: u16 = 0;
while (it.next()) |line| : (i += 1) {
@@ -3288,14 +3490,24 @@ pub const Pardes = struct {
// file line-number gutter: mute the first PREFIX_W columns. Cheap
// chrome, not gated on colors_on; selection/cursor passes still win.
+ // The cursor row's number takes the tag style (same row math as the
+ // cursor pass below) so the eye finds the current line.
if (pane.file != null) {
+ const goff = pane.scroll();
+ const gcur = pane.vt.screens.active.cursor;
+ const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, @intCast(gcur.y)) + goff;
+ const cur_vr: i32 = if (active and !pane.tag_edit) gcrow - goff else -1;
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
+ const on_cursor = @as(i32, vr) == cur_vr;
var c: u16 = 0;
while (c < PREFIX_W and c < tw) : (c += 1) {
const cell = s.at(tx + c, r.y + BOX_H + vr);
cell.default = false; // paints blank gutter rows too
- cell.style.fg = .{ .rgb = th.lineno };
+ if (on_cursor) {
+ cell.style.fg = .{ .rgb = th.tag_fg };
+ cell.style.bg = .{ .rgb = th.tag_bg };
+ } else cell.style.fg = .{ .rgb = th.lineno };
}
}
}
@@ -3303,6 +3515,8 @@ pub const Pardes = struct {
// tree-sitter style byte; content starts after the lineno gutter
if (p.colors_on) if (pane.file) |*f| {
if (f.highlights.len > 0) {
+ const tz_recolor = tracy.zone(@src(), "synRecolor");
+ defer tz_recolor.end();
var flines = std.mem.splitScalar(u8, f.content, '\n');
var skip: usize = 0;
while (skip < f.scroll) : (skip += 1) {