fix: Fixed race-conditions related to treesitter parsers.

This commit is contained in:
Spencer Brower
2026-08-31 18:44:57 -04:00
parent b932b22f15
commit 2f180f0e4c
9 changed files with 381 additions and 267 deletions
+13 -7
View File
@@ -125,16 +125,21 @@ unescape_html :: proc(s: string) -> string {
}
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
gc := ts.load_grammar(lang)
if gc == nil {
g, ok := ts.grammar(lang)
if !ok {
return code
}
parser := ts.open_parser(g)
if parser == nil {
return code
}
defer ts.parser_delete(parser)
raw_code := unescape_html(code)
raw_c := strings.clone_to_cstring(raw_code)
defer delete(raw_c)
tree := ts.parser_parse_string(gc.parser, nil, raw_c, u32(len(raw_code)))
tree := ts.parser_parse_string(parser, nil, raw_c, u32(len(raw_code)))
if tree == nil {
return code
}
@@ -156,12 +161,13 @@ highlight_block :: proc(code: string, lang: string, file_path: string) -> string
}
}
cursor := gc.cursor
if cursor == nil {
if g.query == nil {
return code
}
cursor := ts.query_cursor_new()
defer ts.query_cursor_delete(cursor)
ts.query_cursor_exec(cursor, gc.query, root)
ts.query_cursor_exec(cursor, g.query, root)
captures := make([dynamic]Capture, 0, 64, context.temp_allocator)
defer delete(captures)
@@ -174,7 +180,7 @@ highlight_block :: proc(code: string, lang: string, file_path: string) -> string
}
cap := match.captures[capture_idx]
name_len: u32
name_c := ts.query_capture_name_for_id(gc.query, cap.index, &name_len)
name_c := ts.query_capture_name_for_id(g.query, cap.index, &name_len)
if name_c == nil {
continue
}
+83
View File
@@ -0,0 +1,83 @@
#+test
package markdown
import ts "../treesitter"
import "core:mem"
import "core:strings"
import "core:testing"
// highlight_block returns the input string unchanged (borrowed) when it can't
// highlight — no grammar, no query, or zero captures — and a freshly-allocated
// string when it does. Free only the latter; identity (raw_data) tells them
// apart regardless of content.
@(private = "file")
free_highlight :: proc(out, code: string) {
if raw_data(out) != raw_data(code) {
delete(out)
}
}
// A builtin grammar (css) highlights into hl- spans, preserving the source text.
@(test)
test_highlight_block_css :: proc(t: ^testing.T) {
code := "a{color:red}"
out := highlight_block(code, "css", "test")
defer free_highlight(out, code)
testing.expect(t, strings.contains(out, "<span class=\"hl-"), "css should be highlighted")
testing.expect(t, strings.contains(out, "</span>"))
testing.expect(t, strings.contains(out, "color"))
testing.expect(t, strings.contains(out, "red"))
}
// The other builtin grammar (html) highlights, and the source's own '<'/'>' are
// HTML-escaped in the output (so they can't be confused with the wrapping spans).
@(test)
test_highlight_block_html_escapes_source :: proc(t: ^testing.T) {
code := "<p>hi</p>"
out := highlight_block(code, "html", "test")
defer free_highlight(out, code)
testing.expect(t, strings.contains(out, "<span class=\"hl-"), "html should be highlighted")
testing.expect(t, strings.contains(out, "hi"))
// The literal angle brackets of the source are escaped in the output.
testing.expect(t, strings.contains(out, "&lt;"))
testing.expect(t, strings.contains(out, "&gt;"))
}
// A language with no available grammar returns the input verbatim (best-effort:
// highlighting is optional). The returned string must be the same buffer, so
// free_highlight leaves it alone.
@(test)
test_highlight_block_unknown_lang_passthrough :: proc(t: ^testing.T) {
code := "fn main() {}"
out := highlight_block(code, "no-such-language", "test")
defer free_highlight(out, code)
testing.expect_value(t, out, code)
testing.expect(t, raw_data(out) == raw_data(code), "passthrough must return the input buffer")
}
// End-to-end: highlight_code finds a fenced code block in rendered HTML and
// rewrites its body with highlight spans while preserving the <pre><code> shell.
// Run under a scratch arena (as production does) so the intermediate allocations
// highlight_code doesn't individually free are reclaimed in one shot.
@(test)
test_highlight_code_wraps_css_block :: proc(t: ^testing.T) {
arena: mem.Dynamic_Arena
mem.dynamic_arena_init(&arena)
defer mem.dynamic_arena_destroy(&arena)
context.allocator = mem.dynamic_arena_allocator(&arena)
html := `<pre><code class="language-css">a{color:red}</code></pre>`
out := highlight_code(html, "test")
testing.expect(t, strings.contains(out, `<pre><code class="language-css">`))
testing.expect(t, strings.contains(out, "</code></pre>"))
testing.expect(
t,
strings.contains(out, "<span class=\"hl-"),
"block body should be highlighted",
)
}