1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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;
}
|