From f3b62fe53b19e36f21f8020831718cbf095d6a7b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 3 Jul 2026 17:33:05 -0300 Subject: performance pass --- paint.zig | 197 +++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 105 insertions(+), 92 deletions(-) (limited to 'paint.zig') diff --git a/paint.zig b/paint.zig index b4ab932b..97efc334 100644 --- a/paint.zig +++ b/paint.zig @@ -7,6 +7,7 @@ const syntax_minimal_grammars = app_config.syntax_minimal_grammars; const syntax_full_grammars = app_config.syntax_full_grammars; const ts = if (syntax_highlighting) @import("tree-sitter") else struct { pub const Language = opaque {}; + pub const Query = opaque {}; }; const ts_queries = if (syntax_highlighting) @import("ts_queries") else struct {}; @@ -134,6 +135,99 @@ pub const SynStyle = struct { bold: bool, }; +const HighlightLanguage = struct { + lang: *const ts.Language, + query: *ts.Query, +}; + +const HighlightTier = enum { minimal, full }; + +const HighlightSpec = struct { + tier: HighlightTier, + exts: []const []const u8, + language: *const fn () callconv(.c) *const ts.Language, + query_src: []const u8, + compiled_query: ?*ts.Query = null, +}; + +var highlight_specs = if (syntax_minimal_grammars) [_]HighlightSpec{ + .{ .tier = .minimal, .exts = &[_][]const u8{ ".c", ".h" }, .language = tree_sitter_c, .query_src = ts_queries.c_highlights }, + .{ .tier = .minimal, .exts = &[_][]const u8{ ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx" }, .language = tree_sitter_cpp, .query_src = ts_queries.cpp_highlights }, + .{ .tier = .minimal, .exts = &[_][]const u8{ ".zig", ".zon" }, .language = tree_sitter_zig, .query_src = ts_queries.zig_highlights }, +} else [_]HighlightSpec{}; + +var full_highlight_specs = if (syntax_full_grammars) [_]HighlightSpec{ + .{ .tier = .full, .exts = &[_][]const u8{ ".adb", ".ads", ".ada" }, .language = tree_sitter_ada, .query_src = ts_queries.ada_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".sh", ".bash", ".zsh" }, .language = tree_sitter_bash, .query_src = ts_queries.bash_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".cs", ".csx" }, .language = tree_sitter_c_sharp, .query_src = ts_queries.c_sharp_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".clj", ".cljs", ".cljc", ".edn" }, .language = tree_sitter_clojure, .query_src = ts_queries.clojure_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{".css"}, .language = tree_sitter_css, .query_src = ts_queries.css_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".ex", ".exs" }, .language = tree_sitter_elixir, .query_src = ts_queries.elixir_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".erl", ".hrl" }, .language = tree_sitter_erlang, .query_src = ts_queries.erlang_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" }, .language = tree_sitter_fortran, .query_src = ts_queries.fortran_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{".go"}, .language = tree_sitter_go, .query_src = ts_queries.go_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".hs", ".lhs" }, .language = tree_sitter_haskell, .query_src = ts_queries.haskell_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".html", ".htm" }, .language = tree_sitter_html, .query_src = ts_queries.html_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{".java"}, .language = tree_sitter_java, .query_src = ts_queries.java_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".js", ".jsx", ".mjs", ".cjs" }, .language = tree_sitter_javascript, .query_src = ts_queries.javascript_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{".json"}, .language = tree_sitter_json, .query_src = ts_queries.json_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".kt", ".kts" }, .language = tree_sitter_kotlin, .query_src = ts_queries.kotlin_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".ml", ".mli" }, .language = tree_sitter_ocaml, .query_src = ts_queries.ocaml_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".pas", ".pp", ".p" }, .language = tree_sitter_pascal, .query_src = ts_queries.pascal_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".php", ".phtml", ".php3", ".php4", ".php5" }, .language = tree_sitter_php, .query_src = ts_queries.php_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".ps1", ".psm1", ".psd1" }, .language = tree_sitter_powershell, .query_src = ts_queries.powershell_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".py", ".pyw" }, .language = tree_sitter_python, .query_src = ts_queries.python_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".rb", ".rake" }, .language = tree_sitter_ruby, .query_src = ts_queries.ruby_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{".rs"}, .language = tree_sitter_rust, .query_src = ts_queries.rust_highlights }, + .{ .tier = .full, .exts = &[_][]const u8{ ".scala", ".sc" }, .language = tree_sitter_scala, .query_src = ts_queries.scala_highlights }, +} else [_]HighlightSpec{}; + +fn tierEnabled(tier: HighlightTier) bool { + return switch (tier) { + .minimal => syntax_minimal_grammars, + .full => syntax_full_grammars, + }; +} + +fn specMatchesExt(spec: HighlightSpec, ext: []const u8) bool { + for (spec.exts) |choice| { + if (std.ascii.eqlIgnoreCase(ext, choice)) return true; + } + return false; +} + +fn ensureHighlightLanguage(spec: *HighlightSpec) !HighlightLanguage { + const lang = spec.language(); + if (spec.compiled_query) |query| return .{ .lang = lang, .query = query }; + var error_offset: u32 = 0; + const query = try ts.Query.create(lang, spec.query_src, &error_offset); + spec.compiled_query = query; + return .{ .lang = lang, .query = query }; +} + +fn highlightLanguageForExt(ext: []const u8) !?HighlightLanguage { + for (&highlight_specs) |*spec| { + if (!tierEnabled(spec.tier)) continue; + if (!specMatchesExt(spec.*, ext)) continue; + return try ensureHighlightLanguage(spec); + } + for (&full_highlight_specs) |*spec| { + if (!tierEnabled(spec.tier)) continue; + if (!specMatchesExt(spec.*, ext)) continue; + return try ensureHighlightLanguage(spec); + } + return null; +} + +pub fn compileStartupHighlightQueries() void { + if (!syntax_highlighting) return; + for (&highlight_specs) |*spec| { + if (spec.tier != .minimal) continue; + if (!tierEnabled(spec.tier)) continue; + _ = ensureHighlightLanguage(spec) catch {}; + } +} + pub fn synStyle(s: Syn) ?SynStyle { return switch (s) { .none => null, @@ -154,7 +248,6 @@ fn synFor(name: []const u8) Syn { .{ "boolean", .number }, .{ "keyword", .keyword }, .{ "include", .keyword }, - .{ "operator", .keyword }, .{ "conditional", .keyword }, .{ "repeat", .keyword }, }) |m| { @@ -163,115 +256,35 @@ fn synFor(name: []const u8) Syn { return .none; } -fn extIs(ext: []const u8, comptime choices: anytype) bool { - inline for (choices) |choice| { - if (std.ascii.eqlIgnoreCase(ext, choice)) return true; - } - return false; +pub fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]u8 { + return highlightFileRange(gpa, path, content, 0, content.len); } -pub fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]u8 { +pub fn highlightFileRange(gpa: std.mem.Allocator, path: []const u8, content: []const u8, start_byte_raw: usize, end_byte_raw: usize) ![]u8 { if (!syntax_highlighting) return &.{}; const ext = std.fs.path.extension(path); - const selected = blk: { - const Selected = struct { - lang: *const ts.Language, - query_src: []const u8, - }; - - if (syntax_full_grammars) { - if (extIs(ext, .{ ".adb", ".ads", ".ada" })) { - break :blk Selected{ .lang = tree_sitter_ada(), .query_src = ts_queries.ada_highlights }; - } else if (extIs(ext, .{ ".sh", ".bash", ".zsh" })) { - break :blk Selected{ .lang = tree_sitter_bash(), .query_src = ts_queries.bash_highlights }; - } - } - if (syntax_minimal_grammars) { - if (extIs(ext, .{ ".c", ".h" })) { - break :blk Selected{ .lang = tree_sitter_c(), .query_src = ts_queries.c_highlights }; - } - } - if (syntax_full_grammars) { - if (extIs(ext, .{ ".cs", ".csx" })) { - break :blk Selected{ .lang = tree_sitter_c_sharp(), .query_src = ts_queries.c_sharp_highlights }; - } else if (extIs(ext, .{ ".clj", ".cljs", ".cljc", ".edn" })) { - break :blk Selected{ .lang = tree_sitter_clojure(), .query_src = ts_queries.clojure_highlights }; - } - } - if (syntax_minimal_grammars) { - if (extIs(ext, .{ ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx" })) { - break :blk Selected{ .lang = tree_sitter_cpp(), .query_src = ts_queries.cpp_highlights }; - } - } - if (syntax_full_grammars) { - if (extIs(ext, .{".css"})) { - break :blk Selected{ .lang = tree_sitter_css(), .query_src = ts_queries.css_highlights }; - } else if (extIs(ext, .{ ".ex", ".exs" })) { - break :blk Selected{ .lang = tree_sitter_elixir(), .query_src = ts_queries.elixir_highlights }; - } else if (extIs(ext, .{ ".erl", ".hrl" })) { - break :blk Selected{ .lang = tree_sitter_erlang(), .query_src = ts_queries.erlang_highlights }; - } else if (extIs(ext, .{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" })) { - break :blk Selected{ .lang = tree_sitter_fortran(), .query_src = ts_queries.fortran_highlights }; - } else if (extIs(ext, .{".go"})) { - break :blk Selected{ .lang = tree_sitter_go(), .query_src = ts_queries.go_highlights }; - } else if (extIs(ext, .{ ".hs", ".lhs" })) { - break :blk Selected{ .lang = tree_sitter_haskell(), .query_src = ts_queries.haskell_highlights }; - } else if (extIs(ext, .{ ".html", ".htm" })) { - break :blk Selected{ .lang = tree_sitter_html(), .query_src = ts_queries.html_highlights }; - } else if (extIs(ext, .{".java"})) { - break :blk Selected{ .lang = tree_sitter_java(), .query_src = ts_queries.java_highlights }; - } else if (extIs(ext, .{ ".js", ".jsx", ".mjs", ".cjs" })) { - break :blk Selected{ .lang = tree_sitter_javascript(), .query_src = ts_queries.javascript_highlights }; - } else if (extIs(ext, .{".json"})) { - break :blk Selected{ .lang = tree_sitter_json(), .query_src = ts_queries.json_highlights }; - } else if (extIs(ext, .{ ".kt", ".kts" })) { - break :blk Selected{ .lang = tree_sitter_kotlin(), .query_src = ts_queries.kotlin_highlights }; - } else if (extIs(ext, .{ ".ml", ".mli" })) { - break :blk Selected{ .lang = tree_sitter_ocaml(), .query_src = ts_queries.ocaml_highlights }; - } else if (extIs(ext, .{ ".pas", ".pp", ".p" })) { - break :blk Selected{ .lang = tree_sitter_pascal(), .query_src = ts_queries.pascal_highlights }; - } else if (extIs(ext, .{ ".php", ".phtml", ".php3", ".php4", ".php5" })) { - break :blk Selected{ .lang = tree_sitter_php(), .query_src = ts_queries.php_highlights }; - } else if (extIs(ext, .{ ".ps1", ".psm1", ".psd1" })) { - break :blk Selected{ .lang = tree_sitter_powershell(), .query_src = ts_queries.powershell_highlights }; - } else if (extIs(ext, .{ ".py", ".pyw" })) { - break :blk Selected{ .lang = tree_sitter_python(), .query_src = ts_queries.python_highlights }; - } else if (extIs(ext, .{ ".rb", ".rake" })) { - break :blk Selected{ .lang = tree_sitter_ruby(), .query_src = ts_queries.ruby_highlights }; - } else if (extIs(ext, .{".rs"})) { - break :blk Selected{ .lang = tree_sitter_rust(), .query_src = ts_queries.rust_highlights }; - } else if (extIs(ext, .{ ".scala", ".sc" })) { - break :blk Selected{ .lang = tree_sitter_scala(), .query_src = ts_queries.scala_highlights }; - } - } - if (syntax_minimal_grammars) { - if (extIs(ext, .{ ".zig", ".zon" })) { - break :blk Selected{ .lang = tree_sitter_zig(), .query_src = ts_queries.zig_highlights }; - } - } - return &.{}; - }; + const selected = (highlightLanguageForExt(ext) catch return &.{}) orelse return &.{}; - const styles = try gpa.alloc(u8, content.len); + 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); const parser = ts.Parser.create(); defer parser.destroy(); parser.setLanguage(selected.lang) catch return styles; - const tree = parser.parseString(content, null) orelse return styles; + const tree = parser.parseString(source, null) orelse return styles; defer tree.destroy(); - var error_offset: u32 = 0; - const query = ts.Query.create(selected.lang, selected.query_src, &error_offset) catch return styles; - defer query.destroy(); const cursor = ts.QueryCursor.create(); defer cursor.destroy(); - cursor.exec(query, tree.rootNode()); + cursor.exec(selected.query, tree.rootNode()); while (cursor.nextMatch()) |match| { for (match.captures) |cap| { - const syn = synFor(query.captureNameForId(cap.index) orelse ""); + const syn = synFor(selected.query.captureNameForId(cap.index) orelse ""); if (syn == .none) continue; var b: usize = cap.node.startByte(); const end = @min(@as(usize, cap.node.endByte()), styles.len); -- cgit v1.3