feat: use diffs instead of cursor nudges to refresh tree-sitter

This commit is contained in:
CJ van den Berg 2024-10-29 20:44:23 +01:00
parent d412f92cc4
commit f313ea4a09
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -36,7 +36,7 @@ const scroll_step_small = 3;
const scroll_cursor_min_border_distance = 5; const scroll_cursor_min_border_distance = 5;
const double_click_time_ms = 350; const double_click_time_ms = 350;
const syntax_full_reparse_time_limit = 30; // ms const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental)
pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000; pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000;
pub const max_match_lines = 15; pub const max_match_lines = 15;
@ -249,8 +249,9 @@ pub const Editor = struct {
syntax: ?*syntax = null, syntax: ?*syntax = null,
syntax_no_render: bool = false, syntax_no_render: bool = false,
syntax_report_timing: bool = false,
syntax_refresh_full: bool = false, syntax_refresh_full: bool = false,
syntax_token: usize = 0, syntax_last_rendered_root: ?Buffer.Root = null,
syntax_incremental_reparse: bool = false, syntax_incremental_reparse: bool = false,
style_cache: ?StyleCache = null, style_cache: ?StyleCache = null,
@ -441,9 +442,15 @@ pub const Editor = struct {
if (self.buffer) |_| try self.close(); if (self.buffer) |_| try self.close();
self.buffer = new_buf; self.buffer = new_buf;
if (new_buf.root.lines() > root_mod.max_syntax_lines) {
self.logger.print("large file threshold {d} lines < file size {d} lines", .{
root_mod.max_syntax_lines,
new_buf.root.lines(),
});
self.logger.print("syntax highlighting disabled", .{});
self.syntax_no_render = true;
}
self.syntax = syntax: { self.syntax = syntax: {
if (new_buf.root.lines() > root_mod.max_syntax_lines)
break :syntax null;
const lang_override = tp.env.get().str("language"); const lang_override = tp.env.get().str("language");
var content = std.ArrayList(u8).init(self.allocator); var content = std.ArrayList(u8).init(self.allocator);
defer content.deinit(); defer content.deinit();
@ -458,6 +465,7 @@ pub const Editor = struct {
break :syntax syn; break :syntax syn;
}; };
self.syntax_no_render = tp.env.get().is("no-syntax"); self.syntax_no_render = tp.env.get().is("no-syntax");
self.syntax_report_timing = tp.env.get().is("syntax-report-timing");
const ftn = if (self.syntax) |syn| syn.file_type.name else "text"; const ftn = if (self.syntax) |syn| syn.file_type.name else "text";
const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹"; const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹";
@ -1510,28 +1518,15 @@ pub const Editor = struct {
self.collapse_cursors(); self.collapse_cursors();
} }
fn nudge_insert(self: *Self, nudge: Selection, exclude: *const CurSel, size: usize) void { fn nudge_insert(self: *Self, nudge: Selection, exclude: *const CurSel, _: usize) void {
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
if (cursel != exclude) if (cursel != exclude)
cursel.nudge_insert(nudge); cursel.nudge_insert(nudge);
for (self.matches.items) |*match_| if (match_.*) |*match| for (self.matches.items) |*match_| if (match_.*) |*match|
match.nudge_insert(nudge); match.nudge_insert(nudge);
if (self.syntax) |syn| {
const root = self.buf_root() catch return;
const eol_mode = self.buf_eol_mode() catch return;
const start_byte = root.get_byte_pos(nudge.begin, self.metrics, eol_mode) catch return;
syn.edit(.{
.start_byte = @intCast(start_byte),
.old_end_byte = @intCast(start_byte),
.new_end_byte = @intCast(start_byte + size),
.start_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) },
.old_end_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) },
.new_end_point = .{ .row = @intCast(nudge.end.row), .column = @intCast(nudge.end.col) },
});
}
} }
fn nudge_delete(self: *Self, nudge: Selection, exclude: *const CurSel, size: usize) void { fn nudge_delete(self: *Self, nudge: Selection, exclude: *const CurSel, _: usize) void {
for (self.cursels.items, 0..) |*cursel_, i| if (cursel_.*) |*cursel| for (self.cursels.items, 0..) |*cursel_, i| if (cursel_.*) |*cursel|
if (cursel != exclude) if (cursel != exclude)
if (!cursel.nudge_delete(nudge)) { if (!cursel.nudge_delete(nudge)) {
@ -1541,19 +1536,6 @@ pub const Editor = struct {
if (!match.nudge_delete(nudge)) { if (!match.nudge_delete(nudge)) {
self.matches.items[i] = null; self.matches.items[i] = null;
}; };
if (self.syntax) |syn| {
const root = self.buf_root() catch return;
const eol_mode = self.buf_eol_mode() catch return;
const start_byte = root.get_byte_pos(nudge.begin, self.metrics, eol_mode) catch return;
syn.edit(.{
.start_byte = @intCast(start_byte),
.old_end_byte = @intCast(start_byte + size),
.new_end_byte = @intCast(start_byte),
.start_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) },
.old_end_point = .{ .row = @intCast(nudge.end.row), .column = @intCast(nudge.end.col) },
.new_end_point = .{ .row = @intCast(nudge.begin.row), .column = @intCast(nudge.begin.col) },
});
}
} }
fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root {
@ -3071,36 +3053,74 @@ pub const Editor = struct {
pub const disable_jump_mode_meta = .{ .interactive = false }; pub const disable_jump_mode_meta = .{ .interactive = false };
fn update_syntax(self: *Self) !void { fn update_syntax(self: *Self) !void {
const frame = tracy.initZone(@src(), .{ .name = "editor update syntax" });
defer frame.deinit();
const root = try self.buf_root(); const root = try self.buf_root();
const eol_mode = try self.buf_eol_mode(); const eol_mode = try self.buf_eol_mode();
const token = @intFromPtr(root); if (self.syntax_last_rendered_root == root)
if (root.lines() > root_mod.max_syntax_lines)
return;
if (self.syntax_token == token)
return; return;
var kind: enum { full, incremental, none } = .none;
var edit_count: usize = 0;
const start_time = std.time.milliTimestamp();
if (self.syntax) |syn| { if (self.syntax) |syn| {
if (self.syntax_no_render) { if (self.syntax_no_render) {
const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
defer frame.deinit();
syn.reset(); syn.reset();
self.syntax_token = 0; self.syntax_last_rendered_root = null;
return; return;
} }
if (!self.syntax_incremental_reparse) if (!self.syntax_incremental_reparse)
self.syntax_refresh_full = true; self.syntax_refresh_full = true;
if (self.syntax_refresh_full) { if (self.syntax_last_rendered_root == null)
syn.reset(); self.syntax_refresh_full = true;
const start_time = std.time.milliTimestamp(); var content = std.ArrayList(u8).init(self.allocator);
var content = std.ArrayList(u8).init(self.allocator); defer content.deinit();
defer content.deinit(); {
const frame = tracy.initZone(@src(), .{ .name = "editor store syntax" });
defer frame.deinit();
try root.store(content.writer(), eol_mode); try root.store(content.writer(), eol_mode);
try syn.refresh_full(content.items); }
if (self.syntax_refresh_full) {
{
const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
defer frame.deinit();
syn.reset();
}
{
const frame = tracy.initZone(@src(), .{ .name = "editor refresh_full syntax" });
defer frame.deinit();
try syn.refresh_full(content.items);
}
kind = .full;
self.syntax_last_rendered_root = root;
self.syntax_refresh_full = false; self.syntax_refresh_full = false;
const end_time = std.time.milliTimestamp();
if (end_time - start_time > syntax_full_reparse_time_limit)
self.syntax_incremental_reparse = true;
} else { } else {
try syn.refresh_from_buffer(root, self.metrics); if (self.syntax_last_rendered_root) |root_src| {
self.syntax_last_rendered_root = null;
var old_content = std.ArrayList(u8).init(self.allocator);
defer old_content.deinit();
{
const frame = tracy.initZone(@src(), .{ .name = "editor store syntax" });
defer frame.deinit();
try root_src.store(old_content.writer(), eol_mode);
}
{
const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" });
defer frame.deinit();
const diff = @import("diff");
const edits = try diff.diff(self.allocator, content.items, old_content.items);
defer self.allocator.free(edits);
for (edits) |edit|
syntax_process_edit(syn, edit);
edit_count = edits.len;
}
{
const frame = tracy.initZone(@src(), .{ .name = "editor refresh syntax" });
defer frame.deinit();
try syn.refresh(content.items);
}
self.syntax_last_rendered_root = root;
kind = .incremental;
}
} }
} else { } else {
var content = std.ArrayList(u8).init(self.allocator); var content = std.ArrayList(u8).init(self.allocator);
@ -3110,10 +3130,43 @@ pub const Editor = struct {
error.NotFound => null, error.NotFound => null,
else => return e, else => return e,
}; };
if (self.syntax_no_render) return; if (!self.syntax_no_render) {
if (self.syntax) |syn| try syn.refresh_full(content.items); if (self.syntax) |syn| {
const frame = tracy.initZone(@src(), .{ .name = "editor parse syntax" });
defer frame.deinit();
try syn.refresh_full(content.items);
self.syntax_last_rendered_root = root;
}
}
}
const end_time = std.time.milliTimestamp();
if (kind == .full or kind == .incremental) {
const update_time = end_time - start_time;
self.syntax_incremental_reparse = end_time - start_time > syntax_full_reparse_time_limit;
if (self.syntax_report_timing)
self.logger.print("syntax update {s} time: {d}ms ({d} edits)", .{ @tagName(kind), update_time, edit_count });
}
}
fn syntax_process_edit(syn: *syntax, edit: @import("diff").Diff) void {
switch (edit.kind) {
.insert => syn.edit(.{
.start_byte = @intCast(edit.start),
.old_end_byte = @intCast(edit.start),
.new_end_byte = @intCast(edit.start + edit.bytes.len),
.start_point = .{ .row = 0, .column = 0 },
.old_end_point = .{ .row = 0, .column = 0 },
.new_end_point = .{ .row = 0, .column = 0 },
}),
.delete => syn.edit(.{
.start_byte = @intCast(edit.start),
.old_end_byte = @intCast(edit.start + edit.bytes.len),
.new_end_byte = @intCast(edit.start),
.start_point = .{ .row = 0, .column = 0 },
.old_end_point = .{ .row = 0, .column = 0 },
.new_end_point = .{ .row = 0, .column = 0 },
}),
} }
self.syntax_token = token;
} }
fn reset_syntax(self: *Self) void { fn reset_syntax(self: *Self) void {
@ -4014,11 +4067,23 @@ pub const Editor = struct {
pub fn toggle_syntax_highlighting(self: *Self, _: Context) Result { pub fn toggle_syntax_highlighting(self: *Self, _: Context) Result {
self.syntax_no_render = !self.syntax_no_render; self.syntax_no_render = !self.syntax_no_render;
if (self.syntax_no_render) { if (self.syntax_no_render) {
if (self.syntax) |syn| syn.reset(); if (self.syntax) |syn| {
self.syntax_token = 0; const frame = tracy.initZone(@src(), .{ .name = "editor reset syntax" });
defer frame.deinit();
syn.reset();
self.syntax_last_rendered_root = null;
self.syntax_refresh_full = true;
self.syntax_incremental_reparse = false;
}
} }
self.logger.print("syntax highlighting {s}", .{if (self.syntax_no_render) "disabled" else "enabled"});
} }
pub const toggle_syntax_highlighting_meta = .{ .description = "Toggle syntax highlighting" }; pub const toggle_syntax_highlighting_meta = .{ .description = "Toggle syntax highlighting" };
pub fn toggle_syntax_timing(self: *Self, _: Context) Result {
self.syntax_report_timing = !self.syntax_report_timing;
}
pub const toggle_syntax_timing_meta = .{ .description = "Toggle tree-sitter timing reports" };
}; };
pub fn create(allocator: Allocator, parent: Widget) !Widget { pub fn create(allocator: Allocator, parent: Widget) !Widget {