const std = @import("std"); const config = @import("pardes_config"); const tracy = @import("tracy.zig"); const grammar_manifest = @import("grammar_manifest.zig"); const look = @import("look.zig"); const c_heap = @import("c_heap"); pub const enabled = config.syntax_highlighting; const zig_grammar = config.syntax_zig_grammar; const minimal_grammars = config.syntax_minimal_grammars; const full_grammars = config.syntax_full_grammars; const ts = if (enabled) @import("tree-sitter") else struct { pub const Language = opaque {}; pub const Query = opaque {}; pub const Parser = opaque {}; pub const QueryCursor = opaque {}; }; const ts_queries = if (enabled) @import("ts_queries") else struct {}; const LanguageFn = *const fn () callconv(.c) *const ts.Language; pub const Syn = enum(u8) { none, keyword, string, number, comment }; const Spec = struct { name: []const u8, exts: []const []const u8, language: *const fn () callconv(.c) *const ts.Language, query_src: []const u8, selected: ?Selected = null, capture_styles: [256]u8 = undefined, }; fn grammarSelected(comptime g: grammar_manifest.Grammar) bool { return switch (g.tier) { .zig => zig_grammar, .minimal => minimal_grammars, .full => full_grammars, }; } fn specCount() comptime_int { var count = 0; for (grammar_manifest.all) |g| { if (grammarSelected(g)) count += 1; } return count; } const typst_supplement = \\ \\(call item: (ident) @keyword) \\(call item: (field field: (ident) @keyword)) \\"#" @keyword \\ ; fn querySrc(comptime g: grammar_manifest.Grammar) []const u8 { const base = @field(ts_queries, g.name ++ "_highlights"); const source = if (comptime std.mem.eql(u8, g.name, "typst")) base ++ typst_supplement else base; return comptime colorQuery(source); } fn colorQuery(comptime source: []const u8) []const u8 { @setEvalBranchQuota(2_000_000); var result: [source.len]u8 = undefined; var written: usize = 0; var at: usize = 0; while (at < source.len) { while (at < source.len) { if (std.ascii.isWhitespace(source[at])) { at += 1; } else if (source[at] == ';') { while (at < source.len and source[at] != '\n') at += 1; } else break; } const begin = at; var depth: usize = 0; var colored = false; var complete = false; while (at < source.len) { const byte = source[at]; if (complete and (byte == '(' or byte == '[' or byte == '"')) break; if (byte == ';') { while (at < source.len and source[at] != '\n') at += 1; continue; } if (byte == '"') { at += 1; while (at < source.len) : (at += 1) { if (source[at] == '\\') { at += 1; } else if (source[at] == '"') { at += 1; break; } } if (depth == 0) complete = true; continue; } if (byte == '(' or byte == '[') depth += 1; if (byte == ')' or byte == ']') { depth -= 1; if (depth == 0) complete = true; } if (byte == '@') { const name = at + 1; at = name; while (at < source.len and (std.ascii.isAlphanumeric(source[at]) or source[at] == '_' or source[at] == '.' or source[at] == '-')) at += 1; colored = colored or synFor(source[name..at]) != .none; continue; } at += 1; } if (colored) { @memcpy(result[written..][0 .. at - begin], source[begin..at]); written += at - begin; } } const filtered = result[0..written].*; return &filtered; } fn initSpecs() [specCount()]Spec { var out: [specCount()]Spec = undefined; var i = 0; inline for (grammar_manifest.all) |g| { if (grammarSelected(g)) { out[i] = .{ .name = g.name, .exts = g.exts, .language = @extern(LanguageFn, .{ .name = "tree_sitter_" ++ g.name }), .query_src = querySrc(g), }; i += 1; } } return out; } var specs = initSpecs(); /// What the tree-sitter runtime allocates from: `start`'s allocator, through /// the shared C heap. var syntax_allocator: std.mem.Allocator = undefined; const syntax_heap = c_heap.Heap(&syntax_allocator); var syntax_started = false; extern fn ts_set_allocator( new_malloc: ?*const fn (size: usize) callconv(.c) ?*anyopaque, new_calloc: ?*const fn (nmemb: usize, size: usize) callconv(.c) ?*anyopaque, new_realloc: ?*const fn (ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque, new_free: ?*const fn (ptr: ?*anyopaque) callconv(.c) void, ) void; pub fn start(gpa: std.mem.Allocator) void { if (comptime enabled) { std.debug.assert(!syntax_started); stop(); syntax_allocator = gpa; syntax_started = true; ts_set_allocator(syntax_heap.malloc, syntax_heap.calloc, syntax_heap.realloc, syntax_heap.free); } } pub fn stop() void { if (comptime enabled) { for (&specs) |*spec| { if (spec.selected) |selected| { selected.cursor.destroy(); selected.parser.destroy(); selected.query.destroy(); } spec.selected = null; } ts_set_allocator(null, null, null, null); syntax_started = false; syntax_allocator = undefined; } } const Selected = struct { name: []const u8, lang: *const ts.Language, query: *ts.Query, parser: *ts.Parser, cursor: *ts.QueryCursor, capture_styles: []u8, }; fn ensure(spec: *Spec) !Selected { if (spec.selected) |selected| return selected; const lang = spec.language(); var error_offset: u32 = 0; const query = try ts.Query.create(lang, spec.query_src, &error_offset); errdefer query.destroy(); if (query.captureCount() > spec.capture_styles.len) return error.TooManyCaptures; const capture_styles = spec.capture_styles[0..query.captureCount()]; for (capture_styles, 0..) |*style, id| { const name = query.captureNameForId(@intCast(id)) orelse ""; style.* = @intFromEnum(synFor(name)); if (style.* == 0) query.disableCapture(name); } const parser = ts.Parser.create(); errdefer parser.destroy(); try parser.setLanguage(lang); const selected: Selected = .{ .name = spec.name, .lang = lang, .query = query, .parser = parser, .cursor = ts.QueryCursor.create(), .capture_styles = capture_styles, }; spec.selected = selected; return selected; } fn forExt(ext: []const u8) !?Selected { for (&specs) |*spec| { for (spec.exts) |choice| { if (std.ascii.eqlIgnoreCase(ext, choice)) return try ensure(spec); } } return null; } const LangAlias = struct { []const u8, []const u8 }; const lang_aliases = [_]LangAlias{ .{ "js", "javascript" }, .{ "jsx", "javascript" }, .{ "mjs", "javascript" }, .{ "py", "python" }, .{ "sh", "bash" }, .{ "shell", "bash" }, .{ "zsh", "bash" }, .{ "bash", "bash" }, .{ "rs", "rust" }, .{ "c++", "cpp" }, .{ "cxx", "cpp" }, .{ "cc", "cpp" }, .{ "cs", "c_sharp" }, .{ "kt", "kotlin" }, .{ "rb", "ruby" }, .{ "ml", "ocaml" }, .{ "hs", "haskell" }, }; fn forLang(name: []const u8) !?Selected { var canonical = name; for (lang_aliases) |a| { if (std.ascii.eqlIgnoreCase(name, a[0])) { canonical = a[1]; break; } } for (&specs) |*spec| { if (std.ascii.eqlIgnoreCase(canonical, spec.name)) return try ensure(spec); } return null; } fn runQuery(styles: []u8, sel: Selected, tree: *ts.Tree, base: usize) void { const cursor = sel.cursor; cursor.exec(sel.query, tree.rootNode()); while (cursor.nextMatch()) |match| { for (match.captures) |cap| { const style = sel.capture_styles[cap.index]; if (style == 0) continue; const b = @min(base + cap.node.startByte(), styles.len); const end = @min(base + @as(usize, cap.node.endByte()), styles.len); if (end > b) @memset(styles[b..end], style); } } } fn synFor(name: []const u8) Syn { for ([_]struct { []const u8, Syn }{ .{ "comment", .comment }, .{ "string", .string }, .{ "character", .string }, .{ "number", .number }, .{ "numeric", .number }, .{ "float", .number }, .{ "boolean", .number }, .{ "keyword", .keyword }, .{ "include", .keyword }, .{ "conditional", .keyword }, .{ "repeat", .keyword }, .{ "title", .keyword }, .{ "uri", .string }, .{ "reference", .number }, .{ "heading", .keyword }, .{ "strong", .comment }, .{ "bold", .comment }, .{ "emphasis", .number }, .{ "italic", .number }, .{ "literal", .string }, .{ "raw", .string }, }) |m| { if (std.mem.indexOf(u8, name, m[0]) != null) return m[1]; } return .none; } pub fn highlightFileRange(gpa: std.mem.Allocator, path: []const u8, content: []const u8, start_raw: usize, end_raw: usize) ![]u8 { const zone = tracy.zone(@src(), "highlightFileRange"); defer zone.end(); if (!enabled) return &.{}; const selected = (try forExt(std.fs.path.extension(path))) orelse return &.{}; const start_byte = @min(start_raw, content.len); const end_byte = @max(start_byte, @min(end_raw, content.len)); const source = content[start_byte..end_byte]; const styles = try gpa.alloc(u8, source.len); @memset(styles, 0); paint(styles, source, selected); return styles; } /// Source spans are independent of editor rows. Locations output uses the same /// declarations as sticky editor headers. Byte ends are exclusive; rows are /// zero-based and inclusive, including the last signature/opening-brace row. pub const ContextDeclaration = struct { start_byte: usize, end_byte: usize, start_line: usize, end_line: usize, header_end_byte: usize, header_end_line: usize, }; pub fn supportsPath(path: []const u8) bool { if (!enabled) return false; const ext = std.fs.path.extension(path); for (specs) |spec| for (spec.exts) |choice| { if (std.ascii.eqlIgnoreCase(ext, choice)) return true; }; return false; } /// Owned source analysis. Both slices use the allocator passed to analyzeSource. /// The parsed tree is released before returning; callers can cache this result. pub const SourceAnalysis = struct { declarations: []ContextDeclaration = &.{}, colors: []u8 = &.{}, pub fn deinit(self: *SourceAnalysis, gpa: std.mem.Allocator) void { gpa.free(self.declarations); gpa.free(self.colors); self.* = .{}; } }; pub fn analyzeSource(gpa: std.mem.Allocator, path: []const u8, content: []const u8, want_declarations: bool, want_colors: bool) !SourceAnalysis { if (!enabled or content.len == 0 or (!want_declarations and !want_colors)) return .{}; const selected = (try forExt(std.fs.path.extension(path))) orelse return .{}; const tree = selected.parser.parseString(content, null) orelse return .{}; defer tree.destroy(); var result: SourceAnalysis = .{}; errdefer result.deinit(gpa); if (want_declarations) result.declarations = try declarationsFromTree(gpa, content, tree); if (want_colors) { result.colors = try gpa.alloc(u8, content.len); @memset(result.colors, 0); paintTree(result.colors, content, selected, tree); } return result; } pub fn contextDeclarations(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]ContextDeclaration { const analysis = try analyzeSource(gpa, path, content, true, false); return analysis.declarations; } fn declarationsFromTree(gpa: std.mem.Allocator, content: []const u8, tree: *ts.Tree) ![]ContextDeclaration { var result: std.ArrayList(ContextDeclaration) = .empty; errdefer result.deinit(gpa); // The cursor retains its ancestor stack: sibling visits do not repeatedly // reconstruct parents in broad syntax trees. Preorder preserves source order. var cursor = tree.rootNode().walk(); defer cursor.destroy(); while (true) { const node = cursor.node(); if (node.isNamed()) if (contextSpan(node, content)) |span| { if (result.items.len == 0 or result.items[result.items.len - 1].start_line != span.start_line or result.items[result.items.len - 1].end_line != span.end_line) try result.append(gpa, span); }; if (cursor.gotoFirstChild()) continue; while (!cursor.gotoNextSibling()) { if (!cursor.gotoParent()) return result.toOwnedSlice(gpa); } } } fn contextSpan(node: ts.Node, source: []const u8) ?ContextDeclaration { const kind = node.kind(); var body: ?ts.Node = null; var accepted = false; if (std.mem.eql(u8, kind, "Decl")) { // Zig's FnProto excludes its body: the surrounding Decl owns both. accepted = childOfKind(node, "FnProto") != null; body = childOfKind(node, "Block"); if (body == null) return null; } else if (std.mem.eql(u8, kind, "ContainerDecl") or std.mem.eql(u8, kind, "TestDecl")) { accepted = true; body = childOfKind(node, "Block"); } else { const kinds = [_][]const u8{ "function_definition", "function_declaration", "function_item", "function", "method_definition", "method_declaration", "method", "singleton_method", "constructor_declaration", "destructor_definition", "class_definition", "class_declaration", "class_specifier", "class", "singleton_class", "object_declaration", "struct_item", "struct_specifier", "struct_declaration", "union_specifier", "union_item", "union_declaration", "enum_item", "enum_specifier", "enum_declaration", "interface_declaration", "trait_item", "trait_definition", "impl_item", "mod_item", "module_definition", "module_declaration", "module", "namespace_definition", "namespace_declaration", "record_declaration", "type_declaration", "extension_declaration", "protocol_declaration", }; for (kinds) |candidate| if (std.mem.eql(u8, kind, candidate)) { accepted = true; break; }; body = node.childByFieldName("body"); } if (!accepted) return null; var owner = node; if (std.mem.eql(u8, kind, "ContainerDecl")) { // A multiline `const Name = struct` starts at the binding, not at the // struct token. Stop at real scopes so returned anonymous containers // do not inherit an enclosing function's signature. var ancestor = node.parent(); while (ancestor) |parent| { const parent_kind = parent.kind(); if (std.mem.eql(u8, parent_kind, "VarDecl")) { owner = parent; break; } if (std.mem.eql(u8, parent_kind, "Block") or std.mem.eql(u8, parent_kind, "Decl") or std.mem.eql(u8, parent_kind, "ContainerDecl") or std.mem.eql(u8, parent_kind, "FnProto") or std.mem.eql(u8, parent_kind, "ContainerField") or std.mem.eql(u8, parent_kind, "ParamDecl")) break; ancestor = parent.parent(); } } const beginning = owner.startPoint(); const end = node.endPoint(); if (beginning.row >= end.row) return null; var header_end: usize = node.startByte(); if (body) |block| { header_end = block.startByte(); // Indentation grammars start their body at its first statement. if (header_end < source.len and source[header_end] == '{') { header_end += 1; } else { while (header_end > node.startByte() and std.ascii.isWhitespace(source[header_end - 1])) header_end -= 1; } } else { // Containers often expose their braces directly, without a body node. var i: u32 = 0; while (i < node.childCount()) : (i += 1) { const child = node.child(i) orelse continue; if (std.mem.eql(u8, child.kind(), "{")) { header_end = child.endByte(); break; } } // Ruby and similar grammars have a body_statement without a field. if (header_end == node.startByte()) { header_end = std.mem.indexOfScalarPos(u8, source, header_end, '\n') orelse source.len; } } const line_start = @as(usize, owner.startByte()) - beginning.column; const header_line = beginning.row + std.mem.count(u8, source[line_start..@min(header_end, source.len)], "\n"); const header_line_end = std.mem.indexOfScalarPos(u8, source, @min(header_end, source.len), '\n') orelse source.len; return .{ .start_byte = line_start, .end_byte = node.endByte(), .start_line = beginning.row, .end_line = end.row - @as(usize, if (end.column == 0) 1 else 0), .header_end_byte = header_line_end, .header_end_line = header_line, }; } fn paint(styles: []u8, source: []const u8, selected: Selected) void { const tree = selected.parser.parseString(source, null) orelse return; defer tree.destroy(); paintTree(styles, source, selected, tree); } fn paintTree(styles: []u8, source: []const u8, selected: Selected, tree: *ts.Tree) void { runQuery(styles, selected, tree, 0); if (std.mem.eql(u8, selected.name, "markdown")) { inject(styles, source, tree.rootNode(), true); } else if (std.mem.eql(u8, selected.name, "typst")) { inject(styles, source, tree.rootNode(), false); } } test "syntax context nested Zig containers and multiline function signatures" { if (!enabled) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const source = "pub const Outer = struct {\n" ++ " const Inner = struct {\n" ++ " pub fn run(\n" ++ " value: u32,\n" ++ " ) void {\n" ++ " const ignored = .{\n" ++ " value,\n" ++ " };\n" ++ " }\n" ++ " };\n" ++ "};\n"; const declarations = try contextDeclarations(gpa, "nested.zig", source); defer gpa.free(declarations); try std.testing.expectEqual(@as(usize, 3), declarations.len); try std.testing.expectEqual(@as(usize, 0), declarations[0].start_line); try std.testing.expectEqual(@as(usize, 10), declarations[0].end_line); try std.testing.expectEqual(@as(usize, 1), declarations[1].start_line); try std.testing.expectEqual(@as(usize, 2), declarations[2].start_line); try std.testing.expectEqual(@as(usize, 4), declarations[2].header_end_line); try std.testing.expectEqual(@as(usize, 8), declarations[2].end_line); try std.testing.expectEqualStrings(" pub fn run(\n value: u32,\n ) void {", source[declarations[2].start_byte..declarations[2].header_end_byte]); const unsupported = try contextDeclarations(gpa, "notes.unknown", source); defer gpa.free(unsupported); try std.testing.expectEqual(@as(usize, 0), unsupported.len); } test "syntax context Python class and function body excludes first statement" { if (!enabled or !full_grammars) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const source = "class Outer:\n def run(\n self, value,\n ):\n return value\n"; const declarations = try contextDeclarations(gpa, "nested.py", source); defer gpa.free(declarations); try std.testing.expectEqual(@as(usize, 2), declarations.len); try std.testing.expectEqual(@as(usize, 0), declarations[0].header_end_line); try std.testing.expectEqual(@as(usize, 3), declarations[1].header_end_line); } test "syntax context multiline bindings and C++ namespaces preserve declaration starts" { if (!enabled) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const zig_source = "const Outer =\n struct {\n field: u8,\n };\n"; const zig_declarations = try contextDeclarations(gpa, "nested.zig", zig_source); defer gpa.free(zig_declarations); try std.testing.expectEqual(@as(usize, 1), zig_declarations.len); try std.testing.expectEqual(@as(usize, 0), zig_declarations[0].start_line); try std.testing.expectEqual(@as(usize, 1), zig_declarations[0].header_end_line); if (!minimal_grammars and !full_grammars) return; const cpp_source = "namespace example {\nstruct Outer {\n int run() {\n return 1;\n }\n};\n}\n"; const cpp_declarations = try contextDeclarations(gpa, "nested.cpp", cpp_source); defer gpa.free(cpp_declarations); try std.testing.expectEqual(@as(usize, 3), cpp_declarations.len); for (cpp_declarations, 0..) |declaration, row| { try std.testing.expectEqual(row, declaration.start_line); try std.testing.expectEqual(row, declaration.header_end_line); } } test "syntax context Rust modules impls and methods remain nested" { if (!enabled or !full_grammars) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const source = "mod outer {\n impl Example {\n fn run(&self) {\n work();\n }\n }\n}\n"; const declarations = try contextDeclarations(gpa, "nested.rs", source); defer gpa.free(declarations); try std.testing.expectEqual(@as(usize, 3), declarations.len); for (declarations, 0..) |declaration, row| { try std.testing.expectEqual(row, declaration.start_line); try std.testing.expectEqual(row, declaration.header_end_line); try std.testing.expectEqual(@as(usize, 6) - row, declaration.end_line); } } pub fn highlightLocations(gpa: std.mem.Allocator, content: []const u8, start_byte_raw: usize, end_byte_raw: usize) ![]u8 { const tz = tracy.zone(@src(), "highlightLocations"); defer tz.end(); 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]; if (!enabled) return &.{}; const styles = try gpa.alloc(u8, source.len); errdefer gpa.free(styles); @memset(styles, 0); var memo_ext: []const u8 = "\x00"; var memo: ?Selected = null; var painted = false; var offset: usize = 0; var lines = std.mem.splitScalar(u8, source, '\n'); while (lines.next()) |line| { defer offset += line.len + 1; const code = codeAfterLocation(line) orelse continue; const ext = std.fs.path.extension(code.path); if (!std.mem.eql(u8, ext, memo_ext)) { memo_ext = ext; memo = forExt(ext) catch null; } const selected = memo orelse continue; paint(styles[offset + code.at ..][0..code.text.len], code.text, selected); painted = true; } if (!painted) { gpa.free(styles); return &.{}; } return styles; } /// Formatted results carry the exact source boundary independently of their /// padded location column. This also colors context whose location is hidden. /// Consecutive source rows share one parse, retaining multiline syntax. pub fn highlightLocationRows(gpa: std.mem.Allocator, content: []const u8, rows: anytype) ![]u8 { if (!enabled or content.len == 0 or rows.len == 0) return &.{}; const styles = try gpa.alloc(u8, content.len); errdefer gpa.free(styles); @memset(styles, 0); const code = try gpa.alloc(u8, content.len); defer gpa.free(code); const colors = try gpa.alloc(u8, content.len); defer gpa.free(colors); const SourceRow = struct { index: usize, start: usize, len: usize }; const sources = try gpa.alloc(SourceRow, rows.len); defer gpa.free(sources); var source_count: usize = 0; var offset: usize = 0; for (rows, 0..) |row, index| { if (offset >= content.len) break; const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len; const source_start = offset + @min(row.code_start, end - offset); var address_only = false; if (comptime @hasField(@TypeOf(row), "location_end")) { address_only = row.location_end > 0 and source_start == end; } if (!address_only) { sources[source_count] = .{ .index = index, .start = source_start, .len = end - source_start }; source_count += 1; } offset = @min(end + 1, content.len); } var painted = false; var first: usize = 0; while (first < source_count) { const first_row = rows[sources[first].index]; var end = first + 1; while (end < source_count) : (end += 1) { const row = rows[sources[end].index]; const previous = rows[sources[end - 1].index]; if (!std.mem.eql(u8, row.path, first_row.path) or row.at.line != previous.at.line +| 1) break; } const language = (forExt(std.fs.path.extension(first_row.path)) catch null); if (language) |selected| { var code_len: usize = 0; for (sources[first..end]) |source| { @memcpy(code[code_len..][0..source.len], content[source.start..][0..source.len]); code_len += source.len; if (code_len < code.len) { code[code_len] = '\n'; code_len += 1; } } if (code_len > 0) { @memset(colors[0..code_len], 0); paint(colors[0..code_len], code[0..code_len], selected); painted = true; var at: usize = 0; for (sources[first..end]) |source| { @memcpy(styles[source.start..][0..source.len], colors[at..][0..source.len]); at += source.len + 1; } } } first = end; } // A shown context range can begin inside a comment/string whose opener was // omitted. Formatter snapshots from the complete source take precedence, // including zero styles that remove misleading fragment-parser captures. if (comptime @hasField(@TypeOf(rows[0]), "colors")) { offset = 0; for (rows) |row| { if (offset >= content.len) break; const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len; const source_start = offset + @min(row.code_start, end - offset); const len = @min(row.colors.len, end - source_start); if (len > 0) { @memcpy(styles[source_start..][0..len], row.colors[0..len]); painted = true; } offset = @min(end + 1, content.len); } } // Declaration context is intentionally muted by the output painter. Keep // its source in parsing groups, then remove every syntax/style flag after // complete-source snapshots have been applied. if (comptime @hasField(@TypeOf(rows[0]), "declaration")) { offset = 0; for (rows) |row| { if (offset >= content.len) break; const end = std.mem.indexOfScalarPos(u8, content, offset, '\n') orelse content.len; if (row.declaration) { const source_start = offset + @min(row.code_start, end - offset); @memset(styles[source_start..end], 0); } offset = @min(end + 1, content.len); } } if (!painted) { gpa.free(styles); return &.{}; } return styles; } fn codeAfterLocation(line: []const u8) ?struct { path: []const u8, at: usize, text: []const u8 } { const token_end = std.mem.indexOfAny(u8, line, " \t") orelse return null; if (token_end == 0) return null; const token = line[0..token_end]; const target = look.parsePathLine(token); if (target.end != token.len) return null; if (target.at.line == 0) return null; const at = token_end + 1; if (at >= line.len) return null; return .{ .path = target.path, .at = at, .text = line[at..] }; } test "syntax formatted locations color hidden context and preserve multiline source" { if (!enabled or (!minimal_grammars and !full_grammars)) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const Row = struct { path: []const u8, code_start: usize, at: look.Spot }; const first = "a path.c:1 \t/* open"; const second = " \t| comment body"; const third = "a path.c:3 \t*/ int value = 42;"; const content = first ++ "\n" ++ second ++ "\n" ++ third ++ "\n"; const rows = [_]Row{ .{ .path = "a path.c", .code_start = "a path.c:1 \t".len, .at = .{ .line = 1 } }, .{ .path = "a path.c", .code_start = " \t| ".len, .at = .{ .line = 2 } }, .{ .path = "a path.c", .code_start = "a path.c:3 \t".len, .at = .{ .line = 3 } }, }; const styles = try highlightLocationRows(gpa, content, &rows); defer gpa.free(styles); const body = std.mem.indexOf(u8, content, "comment body").?; try std.testing.expectEqual(@intFromEnum(Syn.comment), styles[body]); try std.testing.expectEqual(@intFromEnum(Syn.none), styles[body - 1]); const number = std.mem.indexOf(u8, content, "42").?; try std.testing.expectEqual(@intFromEnum(Syn.number), styles[number]); try std.testing.expectEqual(@intFromEnum(Syn.none), styles[0]); } test "syntax formatted locations separate alignment from Markdown indentation" { if (!enabled or !full_grammars) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const Row = struct { path: []const u8, code_start: usize, at: look.Spot }; const content = "short.md:1 \t# Heading\n" ++ "longer path.md:5 \t # Code\n"; const rows = [_]Row{ .{ .path = "short.md", .code_start = "short.md:1 \t".len, .at = .{ .line = 1 } }, .{ .path = "longer path.md", .code_start = "longer path.md:5 \t".len, .at = .{ .line = 5 } }, }; const styles = try highlightLocationRows(gpa, content, &rows); defer gpa.free(styles); try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[std.mem.indexOf(u8, content, "Heading").?]); try std.testing.expectEqual(@intFromEnum(Syn.string), styles[std.mem.indexOf(u8, content, "Code").?]); } fn inject(styles: []u8, source: []const u8, node: ts.Node, markdown: bool) void { const kind = node.kind(); if (markdown and std.mem.eql(u8, kind, "inline")) { const begin: usize = node.startByte(); const end: usize = node.endByte(); if (begin >= end or end > source.len) return; const text = source[begin..end]; // Every colored inline capture requires one of these delimiters. if (std.mem.indexOfAny(u8, text, "*_`[<\\\r\n") == null) return; const selected = (forLang("markdown_inline") catch return) orelse return; const tree = selected.parser.parseString(text, null) orelse return; defer tree.destroy(); runQuery(styles, selected, tree, begin); return; } if (std.mem.eql(u8, kind, if (markdown) "fenced_code_block" else "raw_blck")) { const lang = if (markdown) blk: { const info = childOfKind(node, "info_string") orelse return; break :blk childOfKind(info, "language") orelse return; } else node.childByFieldName("lang") orelse return; const content = childOfKind(node, if (markdown) "code_fence_content" else "blob") orelse return; const selected = (forLang(source[lang.startByte()..lang.endByte()]) catch return) orelse return; const begin: usize = content.startByte(); const end: usize = content.endByte(); if (begin > end or end > source.len) return; const tree = selected.parser.parseString(source[begin..end], null) orelse return; defer tree.destroy(); @memset(styles[begin..end], 0); runQuery(styles, selected, tree, begin); return; } var i: u32 = 0; const count = node.childCount(); while (i < count) : (i += 1) { if (node.child(i)) |child| inject(styles, source, child, markdown); } } fn childOfKind(node: ts.Node, kind: []const u8) ?ts.Node { var i: u32 = 0; const count = node.childCount(); while (i < count) : (i += 1) { if (node.child(i)) |c| { if (std.mem.eql(u8, c.kind(), kind)) return c; } } 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); errdefer gpa.free(styles); @memset(styles, 0); 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)); 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; } test "syntax a results row is coloured by the file its location names" { if (!enabled) return; start(std.testing.allocator); defer stop(); const gpa = std.testing.allocator; const content = "src/a.zig:1:1 const S = struct {};\n" ++ "src/b.md:2:1 # heading\n" ++ "just some prose with a colon: here\n" ++ "src/c.zig:3:1\n"; const styles = try highlightLocations(gpa, content, 0, content.len); defer gpa.free(styles); try std.testing.expectEqual(content.len, styles.len); for (styles[0.."src/a.zig:1:1".len]) |b| try std.testing.expectEqual(@as(u8, 0), b); const zig_kw = std.mem.indexOf(u8, content, "struct").?; try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[zig_kw]); try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[zig_kw + 5]); const prose = std.mem.indexOf(u8, content, "just some prose").?; for (styles[prose .. prose + 14]) |b| try std.testing.expectEqual(@as(u8, 0), b); const bare = std.mem.indexOf(u8, content, "src/c.zig").?; for (styles[bare..]) |b| try std.testing.expectEqual(@as(u8, 0), b); } test "syntax a buffer with no locations in it keeps no highlights at all" { if (!enabled) return; start(std.testing.allocator); defer stop(); const content = "nothing has been said yet\n0: save: AccessDenied (x2)\n"; const styles = try highlightLocations(std.testing.allocator, content, 0, content.len); defer std.testing.allocator.free(styles); try std.testing.expectEqual(@as(usize, 0), styles.len); } test "syntax codeAfterLocation takes whole-token locations and nothing else" { const got = codeAfterLocation("src/x.zig:7:2-9 fn main() void {") orelse return error.ShouldBeALocation; try std.testing.expectEqualStrings("src/x.zig", got.path); try std.testing.expectEqualStrings("fn main() void {", got.text); try std.testing.expect(codeAfterLocation("main.zig some words") == null); try std.testing.expect(codeAfterLocation("note: this is prose") == null); try std.testing.expect(codeAfterLocation("src/x.zig:7:2x rest") == null); try std.testing.expect(codeAfterLocation("src/x.zig:7:2") == null); try std.testing.expect(codeAfterLocation("") == null); try std.testing.expect(codeAfterLocation(" leading space") == null); } test "syntax default full grammar set highlights Typst source" { if (!enabled or !full_grammars) return; start(std.testing.allocator); defer stop(); const source = "// note\n#let answer = 42\n#let text = \"hello\"\n"; const styles = try highlightFileRange(std.testing.allocator, "paper.typst", source, 0, source.len); defer std.testing.allocator.free(styles); const comment_at = std.mem.indexOf(u8, source, "// note").?; const keyword_at = std.mem.indexOf(u8, source, "let").?; const number_at = std.mem.indexOf(u8, source, "42").?; const string_at = std.mem.indexOf(u8, source, "\"hello\"").?; try std.testing.expectEqual(Syn.comment, @as(Syn, @enumFromInt(styles[comment_at]))); try std.testing.expectEqual(Syn.keyword, @as(Syn, @enumFromInt(styles[keyword_at]))); try std.testing.expectEqual(Syn.number, @as(Syn, @enumFromInt(styles[number_at]))); try std.testing.expectEqual(Syn.string, @as(Syn, @enumFromInt(styles[string_at]))); const short_ext = try highlightFileRange(std.testing.allocator, "paper.typ", source, 0, source.len); defer std.testing.allocator.free(short_ext); try std.testing.expectEqual(Syn.keyword, @as(Syn, @enumFromInt(short_ext[keyword_at]))); } test "syntax Typst markup constructs paint and raw blocks inject their language" { if (!enabled or !full_grammars) return; start(std.testing.allocator); defer stop(); const source = \\// note \\= Heading Title \\Some *bold* text with `raw` inline. \\#import "mod.typ": helper \\#let answer = 42 \\#emit(7, "arg") \\```zig \\fn widget() u8 { return 99; } \\``` \\ ; const styles = try highlightFileRange(std.testing.allocator, "paper.typ", source, 0, source.len); defer std.testing.allocator.free(styles); const synAt = struct { fn f(s: []const u8, src: []const u8, needle: []const u8, offset: usize) Syn { return @enumFromInt(s[std.mem.indexOf(u8, src, needle).? + offset]); } }.f; try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "= Heading", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "Heading", 0)); try std.testing.expectEqual(Syn.comment, synAt(styles, source, "bold", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "raw` inline", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "emit", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "#emit", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "```zig", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "```zig", 3)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "fn widget", 0)); try std.testing.expectEqual(Syn.number, synAt(styles, source, "99", 0)); try std.testing.expectEqual(Syn.none, synAt(styles, source, "widget", 0)); try std.testing.expectEqual(Syn.comment, synAt(styles, source, "// note", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "import", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "let", 0)); try std.testing.expectEqual(Syn.number, synAt(styles, source, "42", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "\"arg\"", 0)); } test "syntax markdown highlights markup, injects inline spans and fenced code blocks" { if (!enabled or !full_grammars) return; start(std.testing.allocator); defer stop(); const source = "# Heading *slant* Title\n" ++ "\n" ++ "Prose with **stout** and *lean* plus `snippet` inline.\n" ++ "\n" ++ "```zig\n" ++ "fn gadget() u8 { return 77; }\n" ++ "```\n"; const styles = try highlightFileRange(std.testing.allocator, "doc.md", source, 0, source.len); defer std.testing.allocator.free(styles); const synAt = struct { fn f(s: []const u8, src: []const u8, needle: []const u8, offset: usize) Syn { return @enumFromInt(s[std.mem.indexOf(u8, src, needle).? + offset]); } }.f; try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "Title", 0)); try std.testing.expectEqual(Syn.number, synAt(styles, source, "slant", 0)); try std.testing.expectEqual(Syn.comment, synAt(styles, source, "stout", 0)); try std.testing.expectEqual(Syn.number, synAt(styles, source, "lean", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "snippet", 0)); try std.testing.expectEqual(Syn.string, synAt(styles, source, "```zig", 0)); try std.testing.expectEqual(Syn.keyword, synAt(styles, source, "fn gadget", 0)); try std.testing.expectEqual(Syn.number, synAt(styles, source, "77", 0)); try std.testing.expectEqual(Syn.none, synAt(styles, source, "gadget", 0)); } test "syntax inline fast path agrees with full Markdown query" { if (!enabled or !full_grammars) return; start(std.testing.allocator); defer stop(); const selected = (try forLang("markdown_inline")).?; const check = struct { fn compare(sel: Selected, source: []const u8) !void { const tree = sel.parser.parseString(source, null) orelse return error.ParseFailed; defer tree.destroy(); const expected = try std.testing.allocator.alloc(u8, source.len); defer std.testing.allocator.free(expected); const actual = try std.testing.allocator.alloc(u8, source.len); defer std.testing.allocator.free(actual); for ([_]Syn{ .none, .keyword }) |background| { @memset(expected, @intFromEnum(background)); @memset(actual, @intFromEnum(background)); runQuery(expected, sel, tree, 0); inject(actual, source, tree.rootNode(), true); if (!std.mem.eql(u8, expected, actual)) std.debug.print("inline mismatch: {s}\n", .{source}); try std.testing.expectEqualSlices(u8, expected, actual); } } }.compare; for ([_][]const u8{ "", "plain prose", "ação Ελληνικά 日本語 🙂", "123 456", "tabs\tand spaces", "'quoted' (parentheses) \"double quotes\"", "https://example.org a@b.org", "& ", "~~struck~~ $formula$", "*emphasis* __strong__", "**bold** _emphasis_", "`code` and ``a`b``", "[text](target \"title\")", "![description](image)", "[shortcut] [reference][label]", "[[wiki|text]]", " ", "text", "\\*escaped\\*", "soft\nline", "hard \nline", "hard\\\nline", "tab\t\nline", "hard \r\nline", "hard \rline", "**broken", "[broken](", "`broken", }) |source| try check(selected, source); for (0..128) |byte| { const char: u8 = @intCast(byte); const source = [_]u8{ char, 'a', 'b', char, ' ', char, char, 'c', char, char }; try check(selected, &source); } } test "syntax plain Markdown keeps block styles without starting the inline parser" { if (!enabled or !full_grammars) return; start(std.testing.allocator); defer stop(); const source = "# Heading\n\nPlain prose.\n\n indented code\n"; const styles = try highlightFileRange(std.testing.allocator, "a.md", source, 0, source.len); defer std.testing.allocator.free(styles); try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[2]); try std.testing.expectEqual(@intFromEnum(Syn.none), styles[std.mem.indexOf(u8, source, "Plain").?]); try std.testing.expectEqual(@intFromEnum(Syn.string), styles[std.mem.indexOf(u8, source, "indented").?]); for (specs) |spec| { if (std.mem.eql(u8, spec.name, "markdown_inline")) try std.testing.expect(spec.selected == null); } } test "syntax highlightDiff colors unified diff lines by prefix" { const diff = "diff --git a/x b/x\n" ++ "--- a/x\n" ++ "+++ b/x\n" ++ "@@ -1,3 +1,3 @@\n" ++ " context\n" ++ "-old line\n" ++ "+new line\n"; const styles = try highlightDiff(std.testing.allocator, diff, 0, diff.len); defer std.testing.allocator.free(styles); 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; 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 result fragments preserve source indentation and inline markup" { if (!enabled) return; start(std.testing.allocator); defer stop(); const fixtures = [_]struct { path: []const u8, source: []const u8 }{ .{ .path = "a.zig", .source = " const number = 42; // note" }, .{ .path = "a.md", .source = "# Heading *slant*" }, .{ .path = "a.md", .source = "**bold** and `code`" }, .{ .path = "a.md", .source = " # this is indented code" }, .{ .path = "a.md", .source = "\t# tab-indented code" }, .{ .path = "a.py", .source = " return \"hello\"" }, }; for (fixtures) |fixture| { const expected = try highlightFileRange(std.testing.allocator, fixture.path, fixture.source, 0, fixture.source.len); defer std.testing.allocator.free(expected); const row = try std.fmt.allocPrint(std.testing.allocator, "{s}:12:3-9 {s}", .{ fixture.path, fixture.source }); defer std.testing.allocator.free(row); const actual = try highlightLocations(std.testing.allocator, row, 0, row.len); defer std.testing.allocator.free(actual); if (expected.len == 0) { try std.testing.expectEqual(@as(usize, 0), actual.len); continue; } const code_at = row.len - fixture.source.len; try std.testing.expectEqualSlices(u8, expected, actual[code_at..]); for (actual[0..code_at]) |style| try std.testing.expectEqual(@as(u8, 0), style); } } test "syntax query filtering preserves upstream colors" { if (!enabled) return; var allocator: std.heap.DebugAllocator(.{ .stack_trace_frames = 0, .safety = true }) = .init; defer if (allocator.deinit() != .ok) @panic("leaked syntax query allocations"); start(allocator.allocator()); defer stop(); const source = "// comment\n# Heading *inline*\nconst value = 42;\nif (true) { return \"quoted\"; }\n/* multi\nline */\n"; inline for (grammar_manifest.all) |grammar| { if (comptime grammarSelected(grammar)) { const selected = (try forLang(grammar.name)).?; const raw_source = @field(ts_queries, grammar.name ++ "_highlights") ++ (if (comptime std.mem.eql(u8, grammar.name, "typst")) typst_supplement else ""); var error_offset: u32 = 0; const raw_query = try ts.Query.create(selected.lang, raw_source, &error_offset); defer raw_query.destroy(); var reference = selected; reference.query = raw_query; reference.capture_styles = try std.testing.allocator.alloc(u8, raw_query.captureCount()); defer std.testing.allocator.free(reference.capture_styles); for (reference.capture_styles, 0..) |*style, id| style.* = @intFromEnum(synFor(raw_query.captureNameForId(@intCast(id)) orelse "")); const tree = selected.parser.parseString(source, null) orelse return error.ParseFailed; defer tree.destroy(); var expected: [source.len]u8 = @splat(0); var actual: [source.len]u8 = @splat(0); runQuery(&expected, reference, tree, 0); runQuery(&actual, selected, tree, 0); if (!std.mem.eql(u8, &expected, &actual)) std.debug.print("query mismatch: {s}\n", .{grammar.name}); try std.testing.expectEqualSlices(u8, &expected, &actual); } } } test "syntax Zig keyword captures cover both bytes beyond line ten thousand" { if (!enabled) return; start(std.testing.allocator); defer stop(); const code = "pub fn main() void {\n if (true) return;\n}\n"; const source = try std.testing.allocator.alloc(u8, 10_001 + code.len); defer std.testing.allocator.free(source); @memset(source[0..10_001], '\n'); @memcpy(source[10_001..], code); const styles = try highlightFileRange(std.testing.allocator, "a.zig", source, 10_001, source.len); defer std.testing.allocator.free(styles); for ([_][]const u8{ "fn", "if" }) |keyword| { const at = std.mem.indexOf(u8, code, keyword).?; try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[at]); try std.testing.expectEqual(@intFromEnum(Syn.keyword), styles[at + 1]); } } test "syntax allocator switching clears default-runtime caches" { if (!enabled) return; const source = "fn main() void {}"; const initial = try highlightFileRange(std.testing.allocator, "a.zig", source, 0, source.len); std.testing.allocator.free(initial); start(std.testing.allocator); const custom = try highlightFileRange(std.testing.allocator, "a.zig", source, 0, source.len); std.testing.allocator.free(custom); stop(); const restored = try highlightFileRange(std.testing.allocator, "a.zig", source, 0, source.len); defer std.testing.allocator.free(restored); defer stop(); try std.testing.expectEqual(@intFromEnum(Syn.keyword), restored[0]); try std.testing.expectEqual(@intFromEnum(Syn.keyword), restored[1]); } test "syntax location context snapshots retain omitted multiline comment scope" { if (!enabled or (!minimal_grammars and !full_grammars)) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const source = "/* documentation\nconst int value = 42;\n*/\n"; const source_colors = try highlightFileRange(gpa, "a.c", source, 0, source.len); defer gpa.free(source_colors); const code = "const int value = 42;"; const source_start = std.mem.indexOf(u8, source, code).?; const Row = struct { path: []const u8, code_start: usize, at: look.Spot, colors: []const u8 }; const content = "| \t" ++ code ++ "\n"; const rows = [_]Row{.{ .path = "a.c", .code_start = "| \t".len, .at = .{ .line = 2 }, .colors = source_colors[source_start..][0..code.len], }}; const styles = try highlightLocationRows(gpa, content, &rows); defer gpa.free(styles); for (styles[rows[0].code_start..][0..code.len]) |style| try std.testing.expectEqual(@intFromEnum(Syn.comment), style); try std.testing.expectEqual(@intFromEnum(Syn.none), styles[0]); } test "syntax declaration locations clear snapshots without muting ordinary context" { if (!enabled) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const header = "pub fn run() void {"; const content = "| \t" ++ header ++ "\n| \t const value = 42;\nfile.zig:3\t}\n"; const snapshots = [_]u8{0xff} ** header.len; const Row = struct { path: []const u8 = "file.zig", code_start: usize, at: look.Spot, colors: []const u8 = &.{}, declaration: bool = false, }; const rows = [_]Row{ .{ .code_start = 3, .at = .{ .line = 1 }, .colors = &snapshots, .declaration = true }, .{ .code_start = 3, .at = .{ .line = 2 } }, .{ .code_start = "file.zig:3\t".len, .at = .{ .line = 3 } }, }; const styles = try highlightLocationRows(gpa, content, &rows); defer gpa.free(styles); for (styles[3..][0..header.len]) |style| try std.testing.expectEqual(@as(u8, 0), style); try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "42").?]); } test "syntax stacked addresses do not split multiline source groups" { if (!enabled or (!minimal_grammars and !full_grammars)) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const Row = struct { path: []const u8 = "a.c", code_start: usize, location_end: usize, at: look.Spot }; const content = "a.c:1:1\n/* open\na.c:2:1\nint value = 42;\na.c:3:1\n*/ int other = 7;\n"; const rows = [_]Row{ .{ .code_start = 7, .location_end = 7, .at = .{ .line = 1 } }, .{ .code_start = 0, .location_end = 0, .at = .{ .line = 1 } }, .{ .code_start = 7, .location_end = 7, .at = .{ .line = 2 } }, .{ .code_start = 0, .location_end = 0, .at = .{ .line = 2 } }, .{ .code_start = 7, .location_end = 7, .at = .{ .line = 3 } }, .{ .code_start = 0, .location_end = 0, .at = .{ .line = 3 } }, }; const styles = try highlightLocationRows(gpa, content, &rows); defer gpa.free(styles); try std.testing.expectEqual(@intFromEnum(Syn.comment), styles[std.mem.indexOf(u8, content, "42").?]); try std.testing.expectEqual(@intFromEnum(Syn.number), styles[std.mem.indexOf(u8, content, "7;").?]); for ([_][]const u8{ "a.c:1:1", "a.c:2:1", "a.c:3:1" }) |label| { const at = std.mem.indexOf(u8, content, label).?; for (styles[at..][0..label.len]) |style| try std.testing.expectEqual(@intFromEnum(Syn.none), style); } } test "syntax source analysis preserves declarations and injected colors" { if (!enabled) return; const gpa = std.testing.allocator; start(gpa); defer stop(); const Fixture = struct { path: []const u8, source: []const u8 }; for ([_]Fixture{ .{ .path = "a.zig", .source = "const Outer = struct {\n // comment\n const Inner = struct {\n pub fn run(\n x: u32,\n ) u32 { return x; }\n };\n};\n" }, .{ .path = "a.cpp", .source = "namespace Outer {\nstruct Inner {\nint run(\n int x\n) { return x; }\n};\n}\n" }, .{ .path = "a.rs", .source = "mod outer {\nstruct Inner {}\nimpl Inner {\nfn run(\n &self\n) {}\n}\n}\n" }, .{ .path = "a.js", .source = "function outer() {\nclass Inner {\nrun(\n x\n) { return x; }\n}\n}\n" }, .{ .path = "a.py", .source = "class Outer:\n class Inner:\n def run(\n self, x\n ):\n return x\n" }, .{ .path = "a.md", .source = "# Heading\n\n```zig\npub fn run() void {}\n```\n" }, .{ .path = "a.typst", .source = "= Heading\n\n```zig\npub fn run() void {}\n```\n" }, }) |fixture| { if (!supportsPath(fixture.path)) continue; var analysis = try analyzeSource(gpa, fixture.path, fixture.source, true, true); defer analysis.deinit(gpa); const colors = try highlightFileRange(gpa, fixture.path, fixture.source, 0, fixture.source.len); defer gpa.free(colors); try std.testing.expectEqualSlices(u8, colors, analysis.colors); // Compare the previous named-node walk to the cursor walk, including // source ordering, wrapper deduplication and inclusive declaration ends. const selected = (try forExt(std.fs.path.extension(fixture.path))).?; const tree = selected.parser.parseString(fixture.source, null).?; defer tree.destroy(); var reference: std.ArrayList(ContextDeclaration) = .empty; defer reference.deinit(gpa); var node = tree.rootNode(); walk: while (true) { if (contextSpan(node, fixture.source)) |span| { if (reference.items.len == 0 or reference.items[reference.items.len - 1].start_line != span.start_line or reference.items[reference.items.len - 1].end_line != span.end_line) try reference.append(gpa, span); } if (node.namedChild(0)) |child| { node = child; continue; } while (node.nextNamedSibling() == null) node = node.parent() orelse break :walk; node = node.nextNamedSibling().?; } try std.testing.expectEqual(reference.items.len, analysis.declarations.len); for (reference.items, analysis.declarations) |old, new| try std.testing.expect(std.meta.eql(old, new)); var declarations_only = try analyzeSource(gpa, fixture.path, fixture.source, true, false); defer declarations_only.deinit(gpa); try std.testing.expectEqual(@as(usize, 0), declarations_only.colors.len); try std.testing.expectEqual(analysis.declarations.len, declarations_only.declarations.len); var colors_only = try analyzeSource(gpa, fixture.path, fixture.source, false, true); defer colors_only.deinit(gpa); try std.testing.expectEqual(@as(usize, 0), colors_only.declarations.len); try std.testing.expectEqualSlices(u8, analysis.colors, colors_only.colors); } var unsupported = try analyzeSource(gpa, "a.unknown", "text", true, true); unsupported.deinit(gpa); try std.testing.expectEqual(@as(usize, 0), unsupported.declarations.len); try std.testing.expectEqual(@as(usize, 0), unsupported.colors.len); }