summaryrefslogtreecommitdiff
path: root/src/syntax.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-05 20:21:50 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitde4def3548a6729b0dfd2120495a61beef8c8c2c (patch)
tree12144f0f64dc96bb4741459c1f9db57f44349330 /src/syntax.zig
parent7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff)
downloadpardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz
pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'src/syntax.zig')
-rw-r--r--src/syntax.zig173
1 files changed, 173 insertions, 0 deletions
diff --git a/src/syntax.zig b/src/syntax.zig
new file mode 100644
index 00000000..a9914fd7
--- /dev/null
+++ b/src/syntax.zig
@@ -0,0 +1,173 @@
+//! Tree-sitter syntax highlighting: one style byte per content byte, filled by
+//! running each grammar's highlights.scm query (slurped at build time into the
+//! ts_queries options module). Grammar set is tiered: `minimal` (c, cpp, zig)
+//! compiles queries at startup; `full` adds ~23 languages lazily on first use.
+const std = @import("std");
+const config = @import("pardes_config");
+
+pub const enabled = config.syntax_highlighting;
+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 {};
+};
+const ts_queries = if (enabled) @import("ts_queries") else struct {};
+
+extern fn tree_sitter_ada() callconv(.c) *const ts.Language;
+extern fn tree_sitter_bash() callconv(.c) *const ts.Language;
+extern fn tree_sitter_c() callconv(.c) *const ts.Language;
+extern fn tree_sitter_c_sharp() callconv(.c) *const ts.Language;
+extern fn tree_sitter_clojure() callconv(.c) *const ts.Language;
+extern fn tree_sitter_cpp() callconv(.c) *const ts.Language;
+extern fn tree_sitter_css() callconv(.c) *const ts.Language;
+extern fn tree_sitter_elixir() callconv(.c) *const ts.Language;
+extern fn tree_sitter_erlang() callconv(.c) *const ts.Language;
+extern fn tree_sitter_fortran() callconv(.c) *const ts.Language;
+extern fn tree_sitter_go() callconv(.c) *const ts.Language;
+extern fn tree_sitter_haskell() callconv(.c) *const ts.Language;
+extern fn tree_sitter_html() callconv(.c) *const ts.Language;
+extern fn tree_sitter_java() callconv(.c) *const ts.Language;
+extern fn tree_sitter_javascript() callconv(.c) *const ts.Language;
+extern fn tree_sitter_json() callconv(.c) *const ts.Language;
+extern fn tree_sitter_kotlin() callconv(.c) *const ts.Language;
+extern fn tree_sitter_ocaml() callconv(.c) *const ts.Language;
+extern fn tree_sitter_pascal() callconv(.c) *const ts.Language;
+extern fn tree_sitter_php() callconv(.c) *const ts.Language;
+extern fn tree_sitter_powershell() callconv(.c) *const ts.Language;
+extern fn tree_sitter_python() callconv(.c) *const ts.Language;
+extern fn tree_sitter_ruby() callconv(.c) *const ts.Language;
+extern fn tree_sitter_rust() callconv(.c) *const ts.Language;
+extern fn tree_sitter_scala() callconv(.c) *const ts.Language;
+extern fn tree_sitter_zig() callconv(.c) *const ts.Language;
+
+pub const Syn = enum(u8) { none, keyword, string, number, comment };
+
+const Spec = struct {
+ exts: []const []const u8,
+ language: *const fn () callconv(.c) *const ts.Language,
+ query_src: []const u8,
+ compiled_query: ?*ts.Query = null,
+};
+
+var minimal_specs = if (minimal_grammars) [_]Spec{
+ .{ .exts = &.{ ".c", ".h" }, .language = tree_sitter_c, .query_src = ts_queries.c_highlights },
+ .{ .exts = &.{ ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx" }, .language = tree_sitter_cpp, .query_src = ts_queries.cpp_highlights },
+ .{ .exts = &.{ ".zig", ".zon" }, .language = tree_sitter_zig, .query_src = ts_queries.zig_highlights },
+} else [_]Spec{};
+
+var full_specs = if (full_grammars) [_]Spec{
+ .{ .exts = &.{ ".adb", ".ads", ".ada" }, .language = tree_sitter_ada, .query_src = ts_queries.ada_highlights },
+ .{ .exts = &.{ ".sh", ".bash", ".zsh" }, .language = tree_sitter_bash, .query_src = ts_queries.bash_highlights },
+ .{ .exts = &.{ ".cs", ".csx" }, .language = tree_sitter_c_sharp, .query_src = ts_queries.c_sharp_highlights },
+ .{ .exts = &.{ ".clj", ".cljs", ".cljc", ".edn" }, .language = tree_sitter_clojure, .query_src = ts_queries.clojure_highlights },
+ .{ .exts = &.{".css"}, .language = tree_sitter_css, .query_src = ts_queries.css_highlights },
+ .{ .exts = &.{ ".ex", ".exs" }, .language = tree_sitter_elixir, .query_src = ts_queries.elixir_highlights },
+ .{ .exts = &.{ ".erl", ".hrl" }, .language = tree_sitter_erlang, .query_src = ts_queries.erlang_highlights },
+ .{ .exts = &.{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" }, .language = tree_sitter_fortran, .query_src = ts_queries.fortran_highlights },
+ .{ .exts = &.{".go"}, .language = tree_sitter_go, .query_src = ts_queries.go_highlights },
+ .{ .exts = &.{ ".hs", ".lhs" }, .language = tree_sitter_haskell, .query_src = ts_queries.haskell_highlights },
+ .{ .exts = &.{ ".html", ".htm" }, .language = tree_sitter_html, .query_src = ts_queries.html_highlights },
+ .{ .exts = &.{".java"}, .language = tree_sitter_java, .query_src = ts_queries.java_highlights },
+ .{ .exts = &.{ ".js", ".jsx", ".mjs", ".cjs" }, .language = tree_sitter_javascript, .query_src = ts_queries.javascript_highlights },
+ .{ .exts = &.{".json"}, .language = tree_sitter_json, .query_src = ts_queries.json_highlights },
+ .{ .exts = &.{ ".kt", ".kts" }, .language = tree_sitter_kotlin, .query_src = ts_queries.kotlin_highlights },
+ .{ .exts = &.{ ".ml", ".mli" }, .language = tree_sitter_ocaml, .query_src = ts_queries.ocaml_highlights },
+ .{ .exts = &.{ ".pas", ".pp", ".p" }, .language = tree_sitter_pascal, .query_src = ts_queries.pascal_highlights },
+ .{ .exts = &.{ ".php", ".phtml", ".php3", ".php4", ".php5" }, .language = tree_sitter_php, .query_src = ts_queries.php_highlights },
+ .{ .exts = &.{ ".ps1", ".psm1", ".psd1" }, .language = tree_sitter_powershell, .query_src = ts_queries.powershell_highlights },
+ .{ .exts = &.{ ".py", ".pyw" }, .language = tree_sitter_python, .query_src = ts_queries.python_highlights },
+ .{ .exts = &.{ ".rb", ".rake" }, .language = tree_sitter_ruby, .query_src = ts_queries.ruby_highlights },
+ .{ .exts = &.{".rs"}, .language = tree_sitter_rust, .query_src = ts_queries.rust_highlights },
+ .{ .exts = &.{ ".scala", ".sc" }, .language = tree_sitter_scala, .query_src = ts_queries.scala_highlights },
+} else [_]Spec{};
+
+const Selected = struct { lang: *const ts.Language, query: *ts.Query };
+
+// NOTE: don't lang.destroy() — the tree_sitter_*() languages are static
+// singletons reused on every open; destroying one use-after-frees the next.
+fn ensure(spec: *Spec) !Selected {
+ 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 forExt(ext: []const u8) !?Selected {
+ for (&minimal_specs) |*spec| {
+ for (spec.exts) |choice| {
+ if (std.ascii.eqlIgnoreCase(ext, choice)) return try ensure(spec);
+ }
+ }
+ for (&full_specs) |*spec| {
+ for (spec.exts) |choice| {
+ if (std.ascii.eqlIgnoreCase(ext, choice)) return try ensure(spec);
+ }
+ }
+ return null;
+}
+
+/// compile the minimal-tier queries eagerly so the first .zig/.c open is fast
+pub fn compileStartupQueries() void {
+ if (!enabled) return;
+ for (&minimal_specs) |*spec| _ = ensure(spec) catch {};
+}
+
+fn synFor(name: []const u8) Syn {
+ for ([_]struct { []const u8, Syn }{
+ .{ "comment", .comment },
+ .{ "string", .string },
+ .{ "character", .string },
+ .{ "number", .number },
+ .{ "float", .number },
+ .{ "boolean", .number },
+ .{ "keyword", .keyword },
+ .{ "include", .keyword },
+ .{ "conditional", .keyword },
+ .{ "repeat", .keyword },
+ }) |m| {
+ if (std.mem.indexOf(u8, name, m[0]) != null) return m[1];
+ }
+ return .none;
+}
+
+pub fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]u8 {
+ return highlightFileRange(gpa, path, content, 0, content.len);
+}
+
+/// One Syn byte per content byte in [start, end). Caller frees.
+pub fn highlightFileRange(gpa: std.mem.Allocator, path: []const u8, content: []const u8, start_byte_raw: usize, end_byte_raw: usize) ![]u8 {
+ if (!enabled) return &.{};
+ const ext = std.fs.path.extension(path);
+ const selected = (forExt(ext) catch return &.{}) orelse return &.{};
+
+ 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(source, null) orelse return styles;
+ defer tree.destroy();
+
+ const cursor = ts.QueryCursor.create();
+ defer cursor.destroy();
+ cursor.exec(selected.query, tree.rootNode());
+ while (cursor.nextMatch()) |match| {
+ for (match.captures) |cap| {
+ 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);
+ while (b < end) : (b += 1) styles[b] = @intFromEnum(syn);
+ }
+ }
+ return styles;
+}