summaryrefslogtreecommitdiff
path: root/src/File.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 15:39:20 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitc04df904f2fa686cbe5ce0c078a96260af7b08bd (patch)
treeebb892b5b2f4c91505c241d1408fea1a1904a883 /src/File.zig
parentb9eefda75ab9a63fc510de781d02b92fc04ad31d (diff)
downloadpardes-c04df904f2fa686cbe5ce0c078a96260af7b08bd.tar.gz
pardes-c04df904f2fa686cbe5ce0c078a96260af7b08bd.zip
A diff reads as the code it changes: hunks in their file's language, added and removed rows tinted
A .diff or .patch pane, and a command pane whose output is a diff (`git diff`, `git show`, `diff -u`; told by a `diff --git` line, or `---`/`+++` with a `@@` under them, in its first rows), colour each hunk's code in the language of the file its section names (`+++ b/<path>`, `--- a/<path>` for a deleted file), with tree-sitter, as highlightLocations does for results. src/diff.zig walks a unified diff by its `@@` counts, so a removed line that reads `--- x` is no header. Each side of a hunk is parsed as one text (context and removed lines the old side, context and added lines the new), so a string or comment across lines colours as it does in the file; a side no line takes its colours from is not parsed. A section's hunks share a parse, in pieces of about 40 lines (a hunk past 80 is cut), only those in view, and each piece's colours are kept by its bytes, so scrolling back and a terminal's every repaint parse nothing again. Added and removed lines carry a flag in their style byte; the painter tints their rows to the pane's edge, a little way from the page toward the theme's ANSI green or red, and draws the prefix in that hue pushed to read on it. A file in no known language keeps the old line colours. A command pane is read once its command has finished: its rows are copied once (File.DiffOutput, dropped when the pane runs again), so no frame dumps the scrollback, and painted over what git printed. A running command is shown as it prints. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/File.zig')
-rw-r--r--src/File.zig232
1 files changed, 231 insertions, 1 deletions
diff --git a/src/File.zig b/src/File.zig
index 84cec879..2046a7cd 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -10,10 +10,12 @@ const Surface = pardes.Surface;
const modal = @import("modal.zig");
const filesystem = @import("fs.zig");
const syntax = @import("syntax.zig");
+const diff = @import("diff.zig");
const tracy = @import("tracy.zig");
const dump = @import("dump.zig");
const limits = @import("memory.zig").limits;
const locations = @import("locations.zig");
+const Colors = @import("colors.zig");
const Pane = panes.Pane;
const Output = panes.Output;
const Mini = panes.mini;
@@ -1301,6 +1303,59 @@ pub fn drawGutter(p: *Pardes, s: *Surface, pane: *Pane, r: pardes.Rect, tx: u16,
const SynStyle = struct { fg: [3]u8, bold: bool };
+/// How added and removed diff lines are told apart in every theme: a row
+/// ground a little way from the page toward green or red (the theme's own
+/// ANSI green and red where it has a palette), and the prefix column in
+/// that hue, pushed toward the ink until it reads on that ground. The
+/// syntax colours are drawn over the ground unchanged.
+pub const DiffTints = struct {
+ added_bg: [3]u8,
+ removed_bg: [3]u8,
+ added_fg: [3]u8,
+ removed_fg: [3]u8,
+
+ pub fn of(p: *Pardes) DiffTints {
+ const th = p.theme();
+ const page = th.bg orelse th.tag_bg;
+ const ink = th.fg orelse th.tag_fg;
+ const green: [3]u8 = if (th.palette) |pal| pal[2] else .{ 0x2e, 0xa0, 0x43 };
+ const red: [3]u8 = if (th.palette) |pal| pal[1] else .{ 0xcf, 0x22, 0x2e };
+ const added_bg = Colors.mixOklab(page, green, 0.18);
+ const removed_bg = Colors.mixOklab(page, red, 0.18);
+ return .{
+ .added_bg = added_bg,
+ .removed_bg = removed_bg,
+ .added_fg = legible(green, ink, added_bg),
+ .removed_fg = legible(red, ink, removed_bg),
+ };
+ }
+
+ fn legible(hue: [3]u8, ink: [3]u8, ground: [3]u8) [3]u8 {
+ var fg = hue;
+ var step: usize = 0;
+ while (step < 4 and Colors.contrast(fg, ground) < 3) : (step += 1) fg = Colors.mixOklab(fg, ink, 0.4);
+ return fg;
+ }
+
+ /// The row ground and prefix ink for a line's style byte, if it is an
+ /// added or removed line.
+ pub fn forStyle(t: *const DiffTints, style: u8) ?struct { bg: [3]u8, fg: [3]u8 } {
+ if (style & syntax.diff_added != 0) return .{ .bg = t.added_bg, .fg = t.added_fg };
+ if (style & syntax.diff_removed != 0) return .{ .bg = t.removed_bg, .fg = t.removed_fg };
+ return null;
+ }
+};
+
+/// A diff row's tint: its ground from `x` to the pane's edge.
+fn tintRow(s: *Surface, x: u16, y: u16, end: u16, bg: [3]u8) void {
+ var col = x;
+ while (col < end) : (col += 1) {
+ const cell = s.at(col, y);
+ cell.default = false;
+ cell.style.bg = .{ .rgb = bg };
+ }
+}
+
fn synStyle(p: *Pardes, sy: syntax.Syn) ?SynStyle {
return switch (sy) {
.none => null,
@@ -1320,6 +1375,7 @@ pub fn recolorSyntax(p: *Pardes, s: *Surface, pane: *Pane, f: *State, r: pardes.
const prefix_width = gutterWidth(pane);
const tz_recolor = tracy.zone(@src(), "synRecolor");
defer tz_recolor.end();
+ const tints: ?DiffTints = if (pane.colorAlgo() == .diff and highlights.len > 0) DiffTints.of(p) else null;
// indexed start, same as bodyText — an empty tail simply paints nothing
var flines = std.mem.splitScalar(u8, f.content[lineStart(p.gpa, f, f.scroll)..], '\n');
const total = nlines(p.gpa, f);
@@ -1352,6 +1408,12 @@ pub fn recolorSyntax(p: *Pardes, s: *Surface, pane: *Pane, f: *State, r: pardes.
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);
const chrome: ?*const pardes.ChromeTheme = if (decoration.plain()) null else p.bodyChrome();
+ // An added or removed line's row, every wrapped row of it, is tinted.
+ const tint = if (tints) |*t| if (base >= highlight_start and base - highlight_start < highlights.len)
+ t.forStyle(highlights[base - highlight_start])
+ else
+ null else null;
+ if (tint) |t| if (prefix_width < tw) tintRow(s, tx + @as(u16, @intCast(prefix_width)), body_y + vr, tx + tw, t.bg);
var c: usize = 0;
var screen_c: usize = 0;
while (hs + c < limit and prefix_width + screen_c < tw) {
@@ -1361,7 +1423,11 @@ pub fn recolorSyntax(p: *Pardes, s: *Surface, pane: *Pane, f: *State, r: pardes.
if (idx >= highlight_start) {
const hidx = idx - highlight_start;
if (hidx < highlights.len) {
- if (synStyle(p, @enumFromInt(highlights[hidx]))) |ss| {
+ if (if (tint != null and hs + c == 0)
+ SynStyle{ .fg = tint.?.fg, .bold = true }
+ else
+ synStyle(p, @enumFromInt(highlights[hidx] & syntax.syn_bits))) |ss|
+ {
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);
@@ -1390,6 +1456,170 @@ pub fn recolorSyntax(p: *Pardes, s: *Surface, pane: *Pane, f: *State, r: pardes.
}
}
+/// What finished command panes printed, kept while it cannot change: a
+/// copy of the terminal's rows, and whether they are a diff. Taken once
+/// when a finished pane is first drawn, so neither a frame nor a look dumps
+/// the whole scrollback again; a still running command is shown as it
+/// printed. Keyed by the pane and its width (a resize rewraps every row);
+/// a rerun in the pane drops it (exec.runCommand).
+pub const DiffOutput = struct {
+ const Entry = struct {
+ serial: u32,
+ cols: u16,
+ is_diff: bool,
+ /// The rows joined by '\n'; `rows` are slices of it.
+ text: []u8,
+ rows: [][]const u8,
+ used: u64,
+ };
+ slots: [4]?Entry = @splat(null),
+ clock: u64 = 0,
+
+ pub fn reset(o: *DiffOutput, gpa: std.mem.Allocator) void {
+ for (&o.slots) |*slot| {
+ if (slot.*) |e| {
+ gpa.free(e.text);
+ gpa.free(e.rows);
+ }
+ slot.* = null;
+ }
+ }
+
+ /// A pane's command runs again: what it printed before is gone.
+ pub fn drop(o: *DiffOutput, gpa: std.mem.Allocator, serial: u32) void {
+ for (&o.slots) |*slot| if (slot.*) |e| if (e.serial == serial) {
+ gpa.free(e.text);
+ gpa.free(e.rows);
+ slot.* = null;
+ };
+ }
+
+ /// A finished command pane's rows when they are a diff, else null.
+ pub fn lines(o: *DiffOutput, p: *Pardes, pane: *Pane) ?[]const []const u8 {
+ if (pane.command == null or !pane.command_done or pane.command_pty or pane.terminal == null) return null;
+ o.clock += 1;
+ for (&o.slots) |*slot| if (slot.*) |*e| if (e.serial == pane.serial and e.cols == pane.cols) {
+ e.used = o.clock;
+ return if (e.is_diff) e.rows else null;
+ };
+ const shown = terminal.shellRows(p, pane) catch return null;
+ const is_diff = diff.looksLikeDiff(shown);
+ var total: usize = 0;
+ if (is_diff) for (shown) |row| {
+ total += row.len + 1;
+ };
+ const text = p.gpa.alloc(u8, total) catch return null;
+ const rows = p.gpa.alloc([]const u8, if (is_diff) shown.len else 0) catch {
+ p.gpa.free(text);
+ return null;
+ };
+ var at: usize = 0;
+ for (rows, 0..) |*row, i| {
+ @memcpy(text[at..][0..shown[i].len], shown[i]);
+ row.* = text[at..][0..shown[i].len];
+ at += shown[i].len;
+ text[at] = '\n';
+ at += 1;
+ }
+ var oldest: usize = 0;
+ for (&o.slots, 0..) |*slot, i| {
+ const e = slot.* orelse {
+ oldest = i;
+ break;
+ };
+ if (o.slots[oldest]) |old| if (e.used < old.used) {
+ oldest = i;
+ };
+ }
+ if (o.slots[oldest]) |old| {
+ p.gpa.free(old.text);
+ p.gpa.free(old.rows);
+ }
+ o.slots[oldest] = .{ .serial = pane.serial, .cols = pane.cols, .is_diff = is_diff, .text = text, .rows = rows, .used = o.clock };
+ return if (is_diff) rows else null;
+ }
+};
+
+/// A command pane whose output is a diff (`git diff`, `diff -u`): its
+/// rows coloured as a .diff file's are, over what the program printed.
+/// The rows are the terminal's own, so a line it wrapped goes on in the
+/// row below (syntax.highlightDiffRows).
+pub fn recolorDiffOutput(p: *Pardes, s: *Surface, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, body: []const u8) void {
+ if (pane.command == null or !p.settings.colors) return;
+ const tz = tracy.zone(@src(), "diffRecolor");
+ defer tz.end();
+ const rows = p.diff_output.lines(p, pane) orelse return;
+ // The copy's rows are one text, joined as a file's lines are.
+ const flat_start = @intFromPtr(rows[0].ptr);
+ const last = rows[rows.len - 1];
+ const flat = rows[0].ptr[0 .. @intFromPtr(last.ptr) + last.len - flat_start];
+ const raw = pane.body.mode == .tty;
+ const View = struct { row: usize, at: usize };
+ const arena = p.scratch.allocator();
+ const views = arena.alloc(?View, body_h) catch return;
+ var lo: usize = std.math.maxInt(usize);
+ var hi: usize = 0;
+ var shown = std.mem.splitScalar(u8, body, '\n');
+ for (views, 0..) |*view, vi| {
+ view.* = null;
+ const line = shown.next() orelse continue;
+ const vr: i32 = @intCast(vi);
+ const w = pane.wrapAt(vr);
+ const row_i: i32 = if (raw) terminal.gridOffset(pane) + vr else w.line;
+ if (row_i < 0 or row_i >= @as(i64, @intCast(rows.len))) continue;
+ const row: usize = @intCast(row_i);
+ const from: usize = if (raw) 0 else @intCast(@max(0, w.at));
+ if (from > rows[row].len or !std.mem.startsWith(u8, rows[row][from..], line)) continue;
+ view.* = .{ .row = row, .at = from };
+ const start = @intFromPtr(rows[row].ptr) - flat_start;
+ lo = @min(lo, start);
+ hi = @max(hi, start + rows[row].len);
+ }
+ if (lo > hi) return;
+ const styles = syntax.highlightDiffRows(arena, flat, lo, hi, true) catch return;
+ const tints = DiffTints.of(p);
+ const ink = p.theme().fg orelse p.theme().tag_fg;
+ const body_y = p.bodyTop(pane, r);
+ shown = std.mem.splitScalar(u8, body, '\n');
+ for (views, 0..) |maybe, vr| {
+ const line = shown.next() orelse break;
+ const view = maybe orelse continue;
+ const base = @intFromPtr(rows[view.row].ptr) - flat_start - lo;
+ const tint = if (base < styles.len) tints.forStyle(styles[base]) else null;
+ if (tint) |t| tintRow(s, tx, body_y + @as(u16, @intCast(vr)), tx + tw, t.bg);
+ var c: usize = 0;
+ var screen_c: usize = 0;
+ while (c < line.len and screen_c < tw) {
+ const grapheme_end = modal.nextGrapheme(line, c);
+ if (grapheme_end <= c) break;
+ const cells = graphemeDisplayWidth(line[c..grapheme_end]);
+ const style = styles[base + view.at + c];
+ const prefix = view.at + c == 0 and (line[0] == '+' or line[0] == '-');
+ // On an added or removed line, what the program coloured red or
+ // green is the tint's to say now: its code takes the page's ink.
+ const paint: ?SynStyle = if (tint != null and prefix)
+ .{ .fg = tint.?.fg, .bold = true }
+ else if (synStyle(p, @enumFromInt(style & syntax.syn_bits))) |ss|
+ ss
+ else if (tint != null)
+ .{ .fg = ink, .bold = false }
+ else
+ null;
+ if (paint) |ss| {
+ var fill: usize = 0;
+ while (fill < cells and screen_c + fill < tw) : (fill += 1) {
+ const cell = s.at(tx + @as(u16, @intCast(screen_c + fill)), body_y + @as(u16, @intCast(vr)));
+ if (cell.default) continue;
+ cell.style.fg = .{ .rgb = ss.fg };
+ cell.style.bold = ss.bold;
+ }
+ }
+ screen_c += cells;
+ c = grapheme_end;
+ }
+ }
+}
+
pub fn drawWrapMarkers(
p: *Pardes,
s: *Surface,