diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 15:39:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | c04df904f2fa686cbe5ce0c078a96260af7b08bd (patch) | |
| tree | ebb892b5b2f4c91505c241d1408fea1a1904a883 /src/syntax.zig | |
| parent | b9eefda75ab9a63fc510de781d02b92fc04ad31d (diff) | |
| download | pardes-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/syntax.zig')
| -rw-r--r-- | src/syntax.zig | 499 |
1 files changed, 462 insertions, 37 deletions
diff --git a/src/syntax.zig b/src/syntax.zig index 8ed00bfa..b7406b04 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -5,6 +5,11 @@ const grammar_manifest = @import("grammar_manifest.zig"); const look = @import("look.zig"); const regexp = @import("regexp.zig"); const c_heap = @import("c_heap"); +const diff = @import("diff.zig"); + +test { + _ = diff; +} pub const enabled = config.syntax_highlighting; const zig_grammar = config.syntax_zig_grammar; @@ -183,6 +188,7 @@ pub fn stop() void { spec.objects = null; spec.objects_failed = false; } + dropDiffPieces(); ts_set_allocator(null, null, null, null); syntax_started = false; syntax_allocator = undefined; @@ -1029,35 +1035,269 @@ fn childOfKind(node: ts.Node, kind: []const u8) ?ts.Node { return null; } -pub fn highlightDiff(gpa: std.mem.Allocator, content: []const u8, start_byte_raw: usize, end_byte_raw: usize) ![]u8 { - const start_byte = @min(start_byte_raw, content.len); - const end_byte = @max(start_byte, @min(end_byte_raw, content.len)); - const source = content[start_byte..end_byte]; - const styles = try gpa.alloc(u8, source.len); +/// A diff line's style byte carries whether the line was added or removed +/// over its syntax style (the low bits), so the painter can tint its row +/// and mark its prefix. +pub const diff_added: u8 = 0x80; +pub const diff_removed: u8 = 0x40; +pub const syn_bits: u8 = 0x3f; + +/// A file section's hunks are parsed together, each side as one text, in +/// pieces: a piece takes whole hunks until it holds `diff_piece_lines` +/// lines, and a hunk longer than `diff_piece_cut` is cut there, counted +/// from the section's first hunk. So a view parses about what a file's +/// view does (its rows and a margin), however many small hunks it shows, +/// and a 20k-line new file is parsed where it is shown, not whole. A string +/// or comment across a cut colours as a fragment would. +const diff_piece_lines = 40; +const diff_piece_cut = 80; + +/// Painted hunk pieces, keyed by their bytes and language: scrolling back +/// over a hunk, or a terminal repainting the same output every frame, +/// parses nothing again. Freed by `stop`. +const DiffPiece = struct { hash: u64, styles: []u8, used: u64 }; +var diff_pieces: [96]?DiffPiece = @splat(null); +var diff_clock: u64 = 0; + +fn dropDiffPieces() void { + for (&diff_pieces) |*slot| { + if (slot.*) |piece| syntax_allocator.free(piece.styles); + slot.* = null; + } + diff_clock = 0; +} + +/// The styles of `content[start..end)` read as a unified diff: headers +/// muted, `@@` lines as keywords, and each hunk's code in the language of +/// the file its section names. A hunk's old side (context and removed +/// lines) and new side (context and added lines) are each parsed whole, so +/// a string or comment that spans lines colours as it does in the file. +/// Added and removed lines carry `diff_added`/`diff_removed`. A file in no +/// language pardes knows keeps the plain colours: added lines as strings, +/// removed ones as numbers. +pub fn highlightDiff(gpa: std.mem.Allocator, content: []const u8, start_raw: usize, end_raw: usize) ![]u8 { + return highlightDiffRows(gpa, content, start_raw, end_raw, false); +} + +/// The same for a terminal's rows, where a row the terminal wrapped +/// continues the diff line above it. +pub fn highlightDiffRows(gpa: std.mem.Allocator, content: []const u8, start_raw: usize, end_raw: usize, continued_rows: bool) ![]u8 { + const tz = tracy.zone(@src(), "highlightDiff"); + defer tz.end(); + const start_byte = @min(start_raw, content.len); + const end_byte = @max(start_byte, @min(end_raw, content.len)); + const styles = try gpa.alloc(u8, end_byte - start_byte); errdefer gpa.free(styles); @memset(styles, 0); + const Window = struct { + styles: []u8, + start: usize, + fn put(w: @This(), at: usize, bytes: []const u8) void { + const lo = @max(at, w.start); + const hi = @min(at + bytes.len, w.start + w.styles.len); + if (lo < hi) @memcpy(w.styles[lo - w.start .. hi - w.start], bytes[lo - at .. hi - at]); + } + fn fill(w: @This(), at: usize, len: usize, style: u8) void { + const lo = @max(at, w.start); + const hi = @min(at + len, w.start + w.styles.len); + if (lo < hi) @memset(w.styles[lo - w.start .. hi - w.start], style); + } + }; + const window: Window = .{ .styles = styles, .start = start_byte }; + + var walk: diff.Walk = .{ .continued_rows = continued_rows }; + // The run of hunk lines not painted yet, with each line's kind. + var piece_start: ?usize = null; + var piece_end: usize = 0; + var piece_path: []const u8 = ""; + var kinds: [diff_piece_cut]diff.Kind = undefined; + var nkinds: usize = 0; + var at = diff.anchorBefore(content, start_byte); + while (true) { + const nl = std.mem.indexOfScalarPos(u8, content, at, '\n') orelse content.len; + const got = walk.step(content[at..nl]); + const body = switch (got.kind) { + .context, .added, .removed, .no_newline => true, + // The next hunk of the section goes on in the same piece. + .hunk => piece_start != null and nkinds < diff_piece_lines, + else => false, + }; + if (piece_start) |ps| if (!body or nkinds == diff_piece_cut) { + if (piece_end >= start_byte) try paintDiffPiece(gpa, window, content[ps..piece_end], ps, kinds[0..nkinds], piece_path); + piece_start = null; + nkinds = 0; + }; + if (at >= end_byte and piece_start == null) break; + if (body and (piece_start != null or got.kind != .hunk)) { + if (piece_start == null) { + piece_start = at; + piece_path = walk.path(); + } + kinds[nkinds] = got.kind; + nkinds += 1; + piece_end = nl; + } else if (nl >= start_byte) { + const style: Syn = switch (got.kind) { + .hunk => .keyword, + .meta, .old_path, .new_path => .comment, + else => .none, + }; + if (style != .none) window.fill(at, nl - at, @intFromEnum(style)); + } + if (nl >= content.len) break; + at = nl + 1; + } + if (piece_start) |ps| if (piece_end >= start_byte) + try paintDiffPiece(gpa, window, content[ps..piece_end], ps, kinds[0..nkinds], piece_path); + return styles; +} +fn paintDiffPiece(gpa: std.mem.Allocator, window: anytype, bytes: []const u8, at: usize, kinds: []const diff.Kind, path: []const u8) !void { + const selected: ?Selected = if (enabled and syntax_started) (forExt(std.fs.path.extension(path)) catch null) else null; + if (selected) |sel| { + const hash = std.hash.Wyhash.hash(std.hash.Wyhash.hash(0, sel.name), bytes); + diff_clock += 1; + for (&diff_pieces) |*slot| if (slot.*) |*piece| if (piece.hash == hash and piece.styles.len == bytes.len) { + piece.used = diff_clock; + return window.put(at, piece.styles); + }; + const colored = try syntax_allocator.alloc(u8, bytes.len); + errdefer syntax_allocator.free(colored); + try paintHunk(gpa, colored, bytes, kinds, sel); + window.put(at, colored); + var oldest: usize = 0; + for (&diff_pieces, 0..) |*slot, i| { + const piece = slot.* orelse { + oldest = i; + break; + }; + if (diff_pieces[oldest]) |o| if (piece.used < o.used) { + oldest = i; + }; + } + if (diff_pieces[oldest]) |old| syntax_allocator.free(old.styles); + diff_pieces[oldest] = .{ .hash = hash, .styles = colored, .used = diff_clock }; + return; + } + // No language: each line in one colour, as diffs were always shown. + var lines = std.mem.splitScalar(u8, bytes, '\n'); var offset: usize = 0; - var lines = std.mem.splitScalar(u8, source, '\n'); - while (lines.next()) |line| { - const syn = diffLineSyn(line); - if (syn != .none) @memset(styles[offset .. offset + line.len], @intFromEnum(syn)); + for (kinds) |kind| { + const line = lines.next() orelse break; + const style: u8 = switch (kind) { + .added => @intFromEnum(Syn.string) | diff_added, + .removed => @intFromEnum(Syn.number) | diff_removed, + .no_newline => @intFromEnum(Syn.comment), + .hunk => @intFromEnum(Syn.keyword), + else => 0, + }; + if (style != 0) window.fill(at + offset, line.len, style); offset += line.len + 1; } - return styles; } -fn diffLineSyn(line: []const u8) Syn { - if (std.mem.startsWith(u8, line, "@@")) return .keyword; - if (std.mem.startsWith(u8, line, "+++") or - std.mem.startsWith(u8, line, "---") or - std.mem.startsWith(u8, line, "diff ") or - std.mem.startsWith(u8, line, "index ") or - std.mem.startsWith(u8, line, "\\ No newline")) return .comment; - if (line.len == 0) return .none; - if (line[0] == '+') return .string; - if (line[0] == '-') return .number; - return .none; +/// One piece's styles: its old side and its new side each parsed as one +/// text, and every line's code coloured from its side, behind its +/// one-character prefix. A `@@` line between its hunks is in neither. +fn paintHunk(gpa: std.mem.Allocator, out: []u8, bytes: []const u8, kinds: []const diff.Kind, sel: Selected) !void { + @memset(out, 0); + const old_src = try gpa.alloc(u8, bytes.len); + defer gpa.free(old_src); + const new_src = try gpa.alloc(u8, bytes.len); + defer gpa.free(new_src); + var old_len: usize = 0; + var new_len: usize = 0; + var lines = std.mem.splitScalar(u8, bytes, '\n'); + for (kinds) |kind| { + const line = lines.next() orelse break; + const code = line[codeStart(line, kind)..]; + // A row a terminal wrapped goes on the line above, as it was printed. + const joined = continuesRow(line, kind); + if (kind == .context or kind == .removed) { + if (joined and old_len > 0) old_len -= 1; + @memcpy(old_src[old_len..][0..code.len], code); + old_len += code.len; + old_src[old_len] = '\n'; + old_len += 1; + } + if (kind == .context or kind == .added) { + if (joined and new_len > 0) new_len -= 1; + @memcpy(new_src[new_len..][0..code.len], code); + new_len += code.len; + new_src[new_len] = '\n'; + new_len += 1; + } + } + // A side is parsed only when a line takes its colours from it: the old + // side for removed lines, the new side for added lines, and context + // from the new side, else from the old. A piece that only adds or only + // removes is one parse. + var removes = false; + var adds = false; + for (kinds) |kind| { + removes = removes or kind == .removed; + adds = adds or kind == .added; + } + const context_new = adds or !removes; + const old_colors = try gpa.alloc(u8, old_len); + defer gpa.free(old_colors); + @memset(old_colors, 0); + if (!context_new or removes) paint(old_colors, old_src[0..old_len], sel); + const new_colors = try gpa.alloc(u8, new_len); + defer gpa.free(new_colors); + @memset(new_colors, 0); + if (context_new) paint(new_colors, new_src[0..new_len], sel); + + lines = std.mem.splitScalar(u8, bytes, '\n'); + var offset: usize = 0; + var old_at: usize = 0; + var new_at: usize = 0; + for (kinds) |kind| { + const line = lines.next() orelse break; + defer offset += line.len + 1; + const skip = codeStart(line, kind); + const code_len = line.len - skip; + const dst = out[offset + skip ..][0..code_len]; + if (continuesRow(line, kind)) { + if ((kind == .context or kind == .removed) and old_at > 0) old_at -= 1; + if ((kind == .context or kind == .added) and new_at > 0) new_at -= 1; + } + switch (kind) { + .removed => @memcpy(dst, old_colors[old_at..][0..code_len]), + .added => @memcpy(dst, new_colors[new_at..][0..code_len]), + .context => @memcpy(dst, if (context_new) new_colors[new_at..][0..code_len] else old_colors[old_at..][0..code_len]), + .no_newline => @memset(out[offset..][0..line.len], @intFromEnum(Syn.comment)), + .hunk => @memset(out[offset..][0..line.len], @intFromEnum(Syn.keyword)), + else => {}, + } + if (kind == .context or kind == .removed) old_at += code_len + 1; + if (kind == .context or kind == .added) new_at += code_len + 1; + const flag: u8 = switch (kind) { + .added => diff_added, + .removed => diff_removed, + else => 0, + }; + if (flag != 0) for (out[offset..][0..line.len]) |*b| { + b.* |= flag; + }; + } +} + +fn continuesRow(line: []const u8, kind: diff.Kind) bool { + return line.len > 0 and codeStart(line, kind) == 0 and + (kind == .context or kind == .added or kind == .removed); +} + +/// Where a hunk line's code starts: past its prefix, which a blank context +/// line (its space stripped) and a terminal's wrapped row have none of. +fn codeStart(line: []const u8, kind: diff.Kind) usize { + if (line.len == 0) return 0; + return switch (kind) { + .context => @intFromBool(line[0] == ' '), + .added => @intFromBool(line[0] == '+'), + .removed => @intFromBool(line[0] == '-'), + else => 0, + }; } test "syntax a results row is coloured by the file its location names" { @@ -1268,31 +1508,215 @@ test "syntax plain Markdown keeps block styles without starting the inline parse } test "syntax highlightDiff colors unified diff lines by prefix" { - const diff = + const diff_text = "diff --git a/x b/x\n" ++ "--- a/x\n" ++ "+++ b/x\n" ++ - "@@ -1,3 +1,3 @@\n" ++ + "@@ -1,2 +1,2 @@\n" ++ " context\n" ++ "-old line\n" ++ "+new line\n"; - const styles = try highlightDiff(std.testing.allocator, diff, 0, diff.len); + const styles = try highlightDiff(std.testing.allocator, diff_text, 0, diff_text.len); defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "diff --git", 0)); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "--- a/x", 0)); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "+++ b/x", 0)); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "@@ -1,2", 0)); + try std.testing.expectEqual(Syn.none, diffSynAt(styles, diff_text, " context", 0)); + // A file in no known language: each line in one colour, and tinted. + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "-old line", 0)); + try std.testing.expectEqual(Syn.string, diffSynAt(styles, diff_text, "+new line", 0)); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "-old line").?] & diff_removed != 0); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "+new line").? + 4] & diff_added != 0); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, " context").?] & (diff_added | diff_removed) == 0); +} - const byteSyn = struct { - fn at(s: []const u8, src: []const u8, needle: []const u8) Syn { - const i = std.mem.indexOf(u8, src, needle).?; - return @enumFromInt(s[i]); - } - }.at; +fn diffSynAt(styles: []const u8, text: []const u8, needle: []const u8, offset: usize) Syn { + return @enumFromInt(styles[std.mem.indexOf(u8, text, needle).? + offset] & syn_bits); +} - try std.testing.expectEqual(Syn.comment, byteSyn(styles, diff, "diff --git")); - try std.testing.expectEqual(Syn.comment, byteSyn(styles, diff, "--- a/x")); - try std.testing.expectEqual(Syn.comment, byteSyn(styles, diff, "+++ b/x")); - try std.testing.expectEqual(Syn.keyword, byteSyn(styles, diff, "@@ -1,3")); - try std.testing.expectEqual(Syn.none, byteSyn(styles, diff, " context")); - try std.testing.expectEqual(Syn.number, byteSyn(styles, diff, "-old line")); - try std.testing.expectEqual(Syn.string, byteSyn(styles, diff, "+new line")); +test "syntax a Zig hunk is coloured as Zig, each side parsed whole" { + if (!enabled) return; + start(std.testing.allocator); + defer stop(); + const diff_text = + "diff --git a/src/shape.zig b/src/shape.zig\n" ++ + "index 1111111..2222222 100644\n" ++ + "--- a/src/shape.zig\n" ++ + "+++ b/src/shape.zig\n" ++ + "@@ -1,6 +1,6 @@\n" ++ + " const Shape = struct {\n" ++ + "- side: u8 = 7,\n" ++ + "+ side: u16 = 42,\n" ++ + " const note =\n" ++ + "- \\\\old words defer\n" ++ + "+ \\\\new words defer\n" ++ + " ;\n" ++ + " };\n"; + const styles = try highlightDiff(std.testing.allocator, diff_text, 0, diff_text.len); + defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "struct", 0)); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "7,", 0)); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "42", 0)); + // A multiline string, told apart only by parsing the side it is on: + // `defer` inside it is no keyword. + try std.testing.expectEqual(Syn.string, diffSynAt(styles, diff_text, "old words defer", 10)); + try std.testing.expectEqual(Syn.string, diffSynAt(styles, diff_text, "new words defer", 10)); + // The prefix is no code: it carries the tint alone. + const added = std.mem.indexOf(u8, diff_text, "+ side").?; + try std.testing.expectEqual(diff_added, styles[added]); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "42").?] & diff_added != 0); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "7,").?] & diff_removed != 0); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "index 111", 0)); + + // A window in the middle is coloured as the whole is. + const from = std.mem.indexOf(u8, diff_text, " const note").?; + const part = try highlightDiff(std.testing.allocator, diff_text, from, diff_text.len); + defer std.testing.allocator.free(part); + try std.testing.expectEqualSlices(u8, styles[from..], part); +} + +test "syntax a Python hunk is coloured as Python" { + if (!enabled or !full_grammars) return; + start(std.testing.allocator); + defer stop(); + const diff_text = + "--- a/tool.py\n" ++ + "+++ b/tool.py\n" ++ + "@@ -1,3 +1,4 @@\n" ++ + " def run(n):\n" ++ + "- return n\n" ++ + "+ # doubled now\n" ++ + "+ return n * 2\n" ++ + " \n"; + const styles = try highlightDiff(std.testing.allocator, diff_text, 0, diff_text.len); + defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "def", 0)); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "return n\n", 0)); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "# doubled", 2)); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "2\n", 0)); +} + +test "syntax a deleted file's lines are coloured in its language" { + if (!enabled) return; + start(std.testing.allocator); + defer stop(); + const diff_text = + "diff --git a/gone.zig b/gone.zig\n" ++ + "deleted file mode 100644\n" ++ + "--- a/gone.zig\n" ++ + "+++ /dev/null\n" ++ + "@@ -1,2 +0,0 @@\n" ++ + "-const Gone = enum { a };\n" ++ + "-// the end\n"; + const styles = try highlightDiff(std.testing.allocator, diff_text, 0, diff_text.len); + defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "enum", 0)); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "// the end", 3)); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "enum").?] & diff_removed != 0); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, diff_text, "deleted file", 0)); +} + +test "syntax a hunk in no known language keeps the line colours" { + if (!enabled) return; + start(std.testing.allocator); + defer stop(); + const diff_text = + "--- a/notes.unknownext\n" ++ + "+++ b/notes.unknownext\n" ++ + "@@ -1 +1 @@\n" ++ + "-const struct 1\n" ++ + "+const struct 2\n"; + const styles = try highlightDiff(std.testing.allocator, diff_text, 0, diff_text.len); + defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "struct 1", 0)); + try std.testing.expectEqual(Syn.string, diffSynAt(styles, diff_text, "struct 2", 0)); + try std.testing.expectEqual(Syn.string, diffSynAt(styles, diff_text, "+const struct 2", 0)); +} + +test "syntax git diff output in a command pane is told and coloured by its rows" { + if (!enabled) return; + start(std.testing.allocator); + defer stop(); + // A command pane's rows: its command line, then git's output, with one + // long added line the terminal wrapped onto a second row. + const rows = [_][]const u8{ + "git diff", + "diff --git a/main.zig b/main.zig", + "--- a/main.zig", + "+++ b/main.zig", + "@@ -1,2 +1,2 @@", + " pub fn main() void {", + "+ const answer: u32 = 42; // the ", + "wrapped rest", + "- return;", + "", + }; + try std.testing.expect(diff.looksLikeDiff(&rows)); + try std.testing.expect(!diff.looksLikeDiff(rows[5..])); + const text = try std.mem.join(std.testing.allocator, "\n", &rows); + defer std.testing.allocator.free(text); + const styles = try highlightDiffRows(std.testing.allocator, text, 0, text.len, true); + defer std.testing.allocator.free(styles); + try std.testing.expectEqual(Syn.none, diffSynAt(styles, text, "git diff", 0)); + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, text, "fn main", 0)); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, text, "42", 0)); + // The wrapped row stays an added line, and inside the comment. + try std.testing.expect(styles[std.mem.indexOf(u8, text, "wrapped rest").?] & diff_added != 0); + try std.testing.expectEqual(Syn.comment, diffSynAt(styles, text, "wrapped rest", 0)); + try std.testing.expect(styles[std.mem.indexOf(u8, text, "return").?] & diff_removed != 0); +} + +test "syntax a section's small hunks share one parse" { + if (!enabled) return; + const gpa = std.testing.allocator; + start(gpa); + defer stop(); + const diff_text = + "--- a/x.zig\n+++ b/x.zig\n" ++ + "@@ -1 +1 @@\n-const a = 1;\n+const a = 2;\n" ++ + "@@ -10 +10 @@\n-const b = 1;\n+const b = 2;\n" ++ + "@@ -20 +20 @@\n-const c = 1;\n+const c = 2;\n" ++ + "--- a/y.zig\n+++ b/y.zig\n" ++ + "@@ -1 +1 @@\n-const d = 1;\n+const d = 2;\n"; + const styles = try highlightDiff(gpa, diff_text, 0, diff_text.len); + defer gpa.free(styles); + var cached: usize = 0; + for (diff_pieces) |slot| cached += @intFromBool(slot != null); + try std.testing.expectEqual(@as(usize, 2), cached); // x.zig's three hunks, and y.zig's + try std.testing.expectEqual(Syn.keyword, diffSynAt(styles, diff_text, "@@ -10", 0)); + try std.testing.expectEqual(Syn.number, diffSynAt(styles, diff_text, "2;\n@@ -20", 0)); + try std.testing.expect(styles[std.mem.indexOf(u8, diff_text, "@@ -10").?] & (diff_added | diff_removed) == 0); +} + +test "syntax a hunk painted once is kept, and a long one is parsed in pieces" { + if (!enabled) return; + const gpa = std.testing.allocator; + start(gpa); + defer stop(); + var text: std.Io.Writer.Allocating = .init(gpa); + defer text.deinit(); + const n = diff_piece_cut * 20 + 10; + try text.writer.print("--- /dev/null\n+++ b/big.zig\n@@ -0,0 +1,{d} @@\n", .{n}); + for (0..n) |i| try text.writer.print("+const v{d} = {d};\n", .{ i, i }); + const content = text.written(); + const at = std.mem.indexOf(u8, content, "+const v1000 ").?; + const window = try highlightDiff(gpa, content, at, at + 200); + defer gpa.free(window); + try std.testing.expectEqual(Syn.number, @as(Syn, @enumFromInt(window["+const v1000 = ".len] & syn_bits))); + var cached: usize = 0; + for (diff_pieces) |slot| if (slot) |piece| { + cached += 1; + // One piece of many, not the whole hunk. + try std.testing.expect(piece.styles.len < content.len / 4); + }; + try std.testing.expectEqual(@as(usize, 1), cached); + const again = try highlightDiff(gpa, content, at, at + 200); + defer gpa.free(again); + try std.testing.expectEqualSlices(u8, window, again); + cached = 0; + for (diff_pieces) |slot| cached += @intFromBool(slot != null); + try std.testing.expectEqual(@as(usize, 1), cached); } test "syntax result fragments preserve source indentation and inline markup" { @@ -1526,3 +1950,4 @@ test "syntax source analysis preserves declarations and injected colors" { try std.testing.expectEqual(@as(usize, 0), unsupported.declarations.len); try std.testing.expectEqual(@as(usize, 0), unsupported.colors.len); } + |
