summaryrefslogtreecommitdiff
path: root/src/syntax.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/syntax.zig')
-rw-r--r--src/syntax.zig499
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);
}
+