feat: add caching of tree-sitter query objects

This commit is contained in:
CJ van den Berg 2025-03-18 14:26:31 +01:00
parent 7b133c04fb
commit 47a6024c80
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
4 changed files with 152 additions and 15 deletions

View file

@ -0,0 +1,125 @@
const std = @import("std");
const build_options = @import("build_options");
const treez = if (build_options.use_tree_sitter)
@import("treez")
else
@import("treez_dummy.zig");
const Self = @This();
pub const FileType = @import("file_type.zig");
const Query = treez.Query;
allocator: std.mem.Allocator,
mutex: ?std.Thread.Mutex,
highlights: std.StringHashMapUnmanaged(*Query) = .{},
injections: std.StringHashMapUnmanaged(*Query) = .{},
ref_count: usize = 1,
pub const QueryType = enum {
highlights,
injections,
};
pub const QueryParseError = error{
InvalidSyntax,
InvalidNodeType,
InvalidField,
InvalidCapture,
InvalidStructure,
InvalidLanguage,
};
pub const Error = (error{
NotFound,
OutOfMemory,
} || QueryParseError);
pub fn create(allocator: std.mem.Allocator, opts: struct { lock: bool = false }) !*Self {
const self = try allocator.create(Self);
self.* = .{
.allocator = allocator,
.mutex = if (opts.lock) .{} else null,
};
return self;
}
pub fn deinit(self: *Self) void {
self.release_ref_unlocked_and_maybe_destroy();
}
fn add_ref_locked(self: *Self) void {
std.debug.assert(self.ref_count > 0);
self.ref_count += 1;
}
fn release_ref_unlocked_and_maybe_destroy(self: *Self) void {
{
if (self.mutex) |*mtx| mtx.lock();
defer if (self.mutex) |*mtx| mtx.unlock();
self.ref_count -= 1;
if (self.ref_count > 0) return;
}
var iter_highlights = self.highlights.iterator();
while (iter_highlights.next()) |p| {
self.allocator.free(p.key_ptr.*);
p.value_ptr.*.destroy();
}
var iter_injections = self.injections.iterator();
while (iter_injections.next()) |p| {
self.allocator.free(p.key_ptr.*);
p.value_ptr.*.destroy();
}
self.highlights.deinit(self.allocator);
self.injections.deinit(self.allocator);
self.allocator.destroy(self);
}
fn ReturnType(comptime query_type: QueryType) type {
return switch (query_type) {
.highlights => *Query,
.injections => ?*Query,
};
}
fn get_or_add_internal(self: *Self, file_type: *const FileType, comptime query_type: QueryType) Error!ReturnType(query_type) {
const hash = switch (query_type) {
.highlights => &self.highlights,
.injections => &self.injections,
};
return if (hash.get(file_type.name)) |query| query else blk: {
const lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{file_type.name});
const query = try Query.create(lang, switch (query_type) {
.highlights => file_type.highlights,
.injections => if (file_type.injections) |injections| injections else return null,
});
errdefer query.destroy();
try hash.put(self.allocator, try self.allocator.dupe(u8, file_type.name), query);
break :blk query;
};
}
pub fn pre_load(self: *Self, lang_name: []const u8) Error!void {
if (self.mutex) |*mtx| mtx.lock();
defer if (self.mutex) |*mtx| mtx.unlock();
const file_type = FileType.get_by_name(lang_name) orelse return;
_ = try self.get_or_add_internal(file_type, .highlights);
_ = try self.get_or_add_internal(file_type, .injections);
}
pub fn get(self: *Self, file_type: *const FileType, comptime query_type: QueryType) Error!ReturnType(query_type) {
if (self.mutex) |*mtx| mtx.lock();
defer if (self.mutex) |*mtx| mtx.unlock();
const query = try self.get_or_add_internal(file_type, query_type);
self.add_ref_locked();
return query;
}
pub fn release(self: *Self, query: *Query, comptime query_type: QueryType) void {
_ = query;
_ = query_type;
self.release_ref_unlocked_and_maybe_destroy();
}

View file

@ -10,6 +10,7 @@ const Self = @This();
pub const Edit = treez.InputEdit; pub const Edit = treez.InputEdit;
pub const FileType = @import("file_type.zig"); pub const FileType = @import("file_type.zig");
pub const QueryCache = @import("QueryCache.zig");
pub const Range = treez.Range; pub const Range = treez.Range;
pub const Point = treez.Point; pub const Point = treez.Point;
const Input = treez.Input; const Input = treez.Input;
@ -26,34 +27,37 @@ query: *Query,
injections: ?*Query, injections: ?*Query,
tree: ?*treez.Tree = null, tree: ?*treez.Tree = null,
pub fn create(file_type: *const FileType, allocator: std.mem.Allocator) !*Self { pub fn create(file_type: *const FileType, allocator: std.mem.Allocator, query_cache: *QueryCache) !*Self {
const query = try query_cache.get(file_type, .highlights);
const injections = try query_cache.get(file_type, .injections);
const self = try allocator.create(Self); const self = try allocator.create(Self);
self.* = .{ self.* = .{
.allocator = allocator, .allocator = allocator,
.lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{file_type.name}), .lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{file_type.name}),
.file_type = file_type, .file_type = file_type,
.parser = try Parser.create(), .parser = try Parser.create(),
.query = try Query.create(self.lang, file_type.highlights), .query = query,
.injections = if (file_type.injections) |injections| try Query.create(self.lang, injections) else null, .injections = injections,
}; };
errdefer self.destroy(); errdefer self.destroy(query_cache);
try self.parser.setLanguage(self.lang); try self.parser.setLanguage(self.lang);
return self; return self;
} }
pub fn create_file_type(allocator: std.mem.Allocator, lang_name: []const u8) !*Self { pub fn create_file_type(allocator: std.mem.Allocator, lang_name: []const u8, query_cache: *QueryCache) !*Self {
const file_type = FileType.get_by_name(lang_name) orelse return error.NotFound; const file_type = FileType.get_by_name(lang_name) orelse return error.NotFound;
return create(file_type, allocator); return create(file_type, allocator, query_cache);
} }
pub fn create_guess_file_type(allocator: std.mem.Allocator, content: []const u8, file_path: ?[]const u8) !*Self { pub fn create_guess_file_type(allocator: std.mem.Allocator, content: []const u8, file_path: ?[]const u8, query_cache: *QueryCache) !*Self {
const file_type = FileType.guess(file_path, content) orelse return error.NotFound; const file_type = FileType.guess(file_path, content) orelse return error.NotFound;
return create(file_type, allocator); return create(file_type, allocator, query_cache);
} }
pub fn destroy(self: *Self) void { pub fn destroy(self: *Self, query_cache: *QueryCache) void {
if (self.tree) |tree| tree.destroy(); if (self.tree) |tree| tree.destroy();
self.query.destroy(); query_cache.release(self.query, .highlights);
if (self.injections) |injections| query_cache.release(injections, .injections);
self.parser.destroy(); self.parser.destroy();
self.allocator.destroy(self); self.allocator.destroy(self);
} }

View file

@ -464,7 +464,7 @@ pub const Editor = struct {
if (self.buffer) |_| self.write_state(meta.writer()) catch {}; if (self.buffer) |_| self.write_state(meta.writer()) catch {};
for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator); for (self.diagnostics.items) |*d| d.deinit(self.diagnostics.allocator);
self.diagnostics.deinit(); self.diagnostics.deinit();
if (self.syntax) |syn| syn.destroy(); if (self.syntax) |syn| syn.destroy(tui.query_cache());
self.cursels.deinit(); self.cursels.deinit();
self.matches.deinit(); self.matches.deinit();
self.handlers.deinit(); self.handlers.deinit();
@ -588,7 +588,7 @@ pub const Editor = struct {
const frame_ = tracy.initZone(@src(), .{ .name = "create" }); const frame_ = tracy.initZone(@src(), .{ .name = "create" });
defer frame_.deinit(); defer frame_.deinit();
break :blk if (syn_file_type) |ft| break :blk if (syn_file_type) |ft|
syntax.create(ft, self.allocator) catch null syntax.create(ft, self.allocator, tui.query_cache()) catch null
else else
null; null;
}; };
@ -4301,7 +4301,7 @@ pub const Editor = struct {
var content = std.ArrayList(u8).init(self.allocator); var content = std.ArrayList(u8).init(self.allocator);
defer content.deinit(); defer content.deinit();
try root.store(content.writer(), eol_mode); try root.store(content.writer(), eol_mode);
self.syntax = syntax.create_guess_file_type(self.allocator, content.items, self.file_path) catch |e| switch (e) { self.syntax = syntax.create_guess_file_type(self.allocator, content.items, self.file_path, tui.query_cache()) catch |e| switch (e) {
error.NotFound => null, error.NotFound => null,
else => return e, else => return e,
}; };
@ -5447,7 +5447,7 @@ pub const Editor = struct {
if (!try ctx.args.match(.{tp.extract(&file_type)})) if (!try ctx.args.match(.{tp.extract(&file_type)}))
return error.InvalidSetFileTypeArgument; return error.InvalidSetFileTypeArgument;
if (self.syntax) |syn| syn.destroy(); if (self.syntax) |syn| syn.destroy(tui.query_cache());
self.syntax_last_rendered_root = null; self.syntax_last_rendered_root = null;
self.syntax_refresh_full = true; self.syntax_refresh_full = true;
self.syntax_incremental_reparse = false; self.syntax_incremental_reparse = false;
@ -5457,7 +5457,7 @@ pub const Editor = struct {
defer content.deinit(); defer content.deinit();
const root = try self.buf_root(); const root = try self.buf_root();
try root.store(content.writer(), try self.buf_eol_mode()); try root.store(content.writer(), try self.buf_eol_mode());
const syn = syntax.create_file_type(self.allocator, file_type) catch null; const syn = syntax.create_file_type(self.allocator, file_type, tui.query_cache()) catch null;
if (syn) |syn_| if (self.file_path) |file_path| if (syn) |syn_| if (self.file_path) |file_path|
project_manager.did_open( project_manager.did_open(
file_path, file_path,

View file

@ -12,6 +12,7 @@ pub const renderer = @import("renderer");
const command = @import("command"); const command = @import("command");
const EventHandler = @import("EventHandler"); const EventHandler = @import("EventHandler");
const keybind = @import("keybind"); const keybind = @import("keybind");
const syntax = @import("syntax");
const Widget = @import("Widget.zig"); const Widget = @import("Widget.zig");
const MessageFilter = @import("MessageFilter.zig"); const MessageFilter = @import("MessageFilter.zig");
@ -56,6 +57,7 @@ default_cursor: keybind.CursorShape = .default,
fontface_: []const u8 = "", fontface_: []const u8 = "",
fontfaces_: std.ArrayListUnmanaged([]const u8) = .{}, fontfaces_: std.ArrayListUnmanaged([]const u8) = .{},
enable_mouse_idle_timer: bool = false, enable_mouse_idle_timer: bool = false,
query_cache_: *syntax.QueryCache,
const keepalive = std.time.us_per_day * 365; // one year const keepalive = std.time.us_per_day * 365; // one year
const idle_frames = 0; const idle_frames = 0;
@ -120,6 +122,7 @@ fn init(allocator: Allocator) !*Self {
)), )),
.theme_ = theme_, .theme_ = theme_,
.no_sleep = tp.env.get().is("no-sleep"), .no_sleep = tp.env.get().is("no-sleep"),
.query_cache_ = try syntax.QueryCache.create(allocator, .{}),
}; };
instance_ = self; instance_ = self;
defer instance_ = null; defer instance_ = null;
@ -211,6 +214,7 @@ fn deinit(self: *Self) void {
self.rdr_.stop(); self.rdr_.stop();
self.rdr_.deinit(); self.rdr_.deinit();
self.logger.deinit(); self.logger.deinit();
self.query_cache_.deinit();
self.allocator.destroy(self); self.allocator.destroy(self);
} }
@ -1052,6 +1056,10 @@ pub fn mini_mode() ?*MiniMode {
return if (current().mini_mode_) |*p| p else null; return if (current().mini_mode_) |*p| p else null;
} }
pub fn query_cache() *syntax.QueryCache {
return current().query_cache_;
}
pub fn config() *const @import("config") { pub fn config() *const @import("config") {
return &current().config_; return &current().config_;
} }