From ad496d3bd63a760030f3706c2ceff79f0d462be0 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 8 Dec 2025 21:15:03 +0100 Subject: [PATCH 1/5] refactor: add snippet parser module --- build.zig | 5 ++ src/snippet.zig | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/snippet.zig diff --git a/build.zig b/build.zig index 8dfec44..16046a8 100644 --- a/build.zig +++ b/build.zig @@ -414,6 +414,10 @@ pub fn build_exe( .root_source_file = b.path("src/bin_path.zig"), }); + const snippet_mod = b.createModule(.{ + .root_source_file = b.path("src/snippet.zig"), + }); + const Buffer_mod = b.createModule(.{ .root_source_file = b.path("src/buffer/Buffer.zig"), .imports = &.{ @@ -630,6 +634,7 @@ pub fn build_exe( .{ .name = "zeit", .module = zeit_mod }, .{ .name = "VcsStatus", .module = VcsStatus_mod }, .{ .name = "bin_path", .module = bin_path_mod }, + .{ .name = "snippet", .module = snippet_mod }, }, }); diff --git a/src/snippet.zig b/src/snippet.zig new file mode 100644 index 0000000..d2bddea --- /dev/null +++ b/src/snippet.zig @@ -0,0 +1,163 @@ +text: []const u8, +tabstops: [][]Range, + +const Snippet = @This(); +const Range = struct { begin: Position, end: ?Position = null }; +const Position = struct { usize }; + +const Tabstop = struct { + id: usize, + range: Range, +}; + +pub fn deinit(self: *const Snippet, allocator: std.mem.Allocator) void { + for (self.tabstops) |tabstop| allocator.free(tabstop); + allocator.free(self.tabstops); + allocator.free(self.text); +} + +pub fn parse(allocator: std.mem.Allocator, snippet: []const u8) Error!Snippet { + var tabstops: std.ArrayList(struct { id: usize, range: Range }) = .empty; + defer tabstops.deinit(allocator); + var id: ?usize = null; + var content_begin: ?Position = null; + var max_id: usize = 0; + var text: std.Io.Writer.Allocating = .init(allocator); + defer text.deinit(); + + var state: enum { + initial, + escape, + tabstop, + placeholder, + content, + content_escape, + } = .initial; + + var iter = snippet; + while (iter.len > 0) : (iter = iter[1..]) { + const c = iter[0]; + fsm: switch (state) { + .initial => switch (c) { + '\\' => { + state = .escape; + }, + '$' => { + state = .tabstop; + try text.writer.writeByte(c); + }, + else => try text.writer.writeByte(c), + }, + .escape => { + try text.writer.writeByte(c); + state = .initial; + }, + .tabstop => switch (c) { + '{' => { + state = .placeholder; + }, + '0'...'9' => { + const digit: usize = @intCast(c - '0'); + id = if (id) |id_| (id_ * 10) + digit else digit; + }, + else => { + const pos = snippet.len - iter.len; + if (id == null) + return invalid(snippet, pos, error.InvalidIdValue); + (try tabstops.addOne(allocator)).* = .{ + .id = id orelse unreachable, + .range = .{ .begin = .{text.written().len} }, + }; + max_id = @max(id orelse unreachable, max_id); + id = null; + state = .initial; + break :fsm; + }, + }, + .placeholder => switch (c) { + '0'...'9' => { + const digit: usize = @intCast(c - '0'); + id = if (id) |id_| (id_ * 10) + digit else digit; + }, + ':' => { + const pos = snippet.len - iter.len; + if (id == null) + return invalid(snippet, pos, error.InvalidIdValue); + content_begin = .{text.written().len}; + state = .content; + }, + else => { + const pos = snippet.len - iter.len; + return invalid(snippet, pos, error.InvalidIdValue); + }, + }, + .content => switch (c) { + '\\' => { + state = .content_escape; + }, + '}' => { + const pos = snippet.len - iter.len; + if (id == null) + return invalid(snippet, pos, error.InvalidIdValue); + if (content_begin == null) + return invalid(snippet, pos, error.InvalidPlaceholderValue); + (try tabstops.addOne(allocator)).* = .{ + .id = id orelse unreachable, + .range = .{ + .begin = content_begin orelse unreachable, + .end = .{text.written().len}, + }, + }; + max_id = @max(id orelse unreachable, max_id); + id = null; + content_begin = null; + state = .initial; + }, + else => try text.writer.writeByte(c), + }, + .content_escape => { + try text.writer.writeByte(c); + state = .content; + }, + } + } + + if (state != .initial) { + const pos = snippet.len - iter.len; + if (id == null) + return invalid(snippet, pos, error.UnexpectedEndOfDocument); + } + + var result: std.ArrayList([]Range) = .empty; + defer result.deinit(allocator); + var n: usize = 1; + while (n <= max_id) : (n += 1) { + var tabstop: std.ArrayList(Range) = .empty; + errdefer tabstop.deinit(allocator); + for (tabstops.items) |item| if (item.id == n) { + (try tabstop.addOne(allocator)).* = item.range; + }; + (try result.addOne(allocator)).* = try tabstop.toOwnedSlice(allocator); + } + return .{ + .text = try text.toOwnedSlice(), + .tabstops = try result.toOwnedSlice(allocator), + }; +} + +fn invalid(snippet: []const u8, pos: usize, e: Error) Error { + log.err("invalid snippet: {s}", .{snippet}); + log.err("{t} at pos {d}", .{ e, pos }); + return e; +} + +pub const Error = error{ + WriteFailed, + OutOfMemory, + InvalidIdValue, + InvalidPlaceholderValue, + UnexpectedEndOfDocument, +}; + +const log = std.log.scoped(.snippet); +const std = @import("std"); From cbf26b3b6acad2dde30656d34b22194e874425cd Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 8 Dec 2025 21:17:02 +0100 Subject: [PATCH 2/5] refactor: use a structure for returning completion palette item values --- src/tui/mode/overlay/completion_palette.zig | 65 +++++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index 1af71a6..f4b84a4 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -45,18 +45,18 @@ pub fn load_entries(palette: *Type) !usize { max_description = 0; var max_label_len: usize = 0; for (palette.entries.items) |*item| { - const label_, const sort_text, _, const maybe_replace, _, const label_detail, const label_description, _, _ = get_values(item.cbor); - if (get_replace_selection(maybe_replace)) |replace| { + const values = get_values(item.cbor); + if (get_replace_selection(values.replace)) |replace| { if (palette.value.replace == null) palette.value.replace = replace; } - item.label = label_; - item.sort_text = sort_text; + item.label = values.label; + item.sort_text = values.sort_text; - var lines = std.mem.splitScalar(u8, label_description, '\n'); - const label_description_len = if (lines.next()) |desc| desc.len else label_description.len; + var lines = std.mem.splitScalar(u8, values.label_description, '\n'); + const label_description_len = if (lines.next()) |desc| desc.len else values.label_description.len; max_label_len = @max(max_label_len, item.label.len); - max_description = @max(max_description, label_description_len + label_detail.len); + max_description = @max(max_description, label_description_len + values.label_detail.len); } const less_fn = struct { @@ -102,17 +102,17 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T if (!(cbor.matchValue(&iter, cbor.extract_cbor(&item_cbor)) catch false)) return false; if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) return false; - const label_, _, const kind, _, _, const label_detail, const label_description, _, _ = get_values(item_cbor); - const icon_: []const u8 = kind_icon(@enumFromInt(kind)); + const values = get_values(item_cbor); + const icon_: []const u8 = kind_icon(@enumFromInt(values.kind)); const color: u24 = 0x0; return tui.render_symbol( &button.plane, - label_, + values.label, icon_, color, - label_detail, - label_description, + values.label_detail, + values.label_description, matches_cbor, button.active, selected, @@ -121,7 +121,21 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T ); } -fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8, Buffer.Selection, []const u8, []const u8, []const u8, []const u8, []const u8 } { +const Values = struct { + label: []const u8, + sort_text: []const u8, + kind: u8, + replace: Buffer.Selection, + additionalTextEdits: []const u8, + label_detail: []const u8, + label_description: []const u8, + detail: []const u8, + documentation: []const u8, + insertTextFormat: usize, + textEdit_newText: []const u8, +}; + +fn get_values(item_cbor: []const u8) Values { var label_: []const u8 = ""; var label_detail: []const u8 = ""; var label_description: []const u8 = ""; @@ -158,7 +172,19 @@ fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8, Buffer cbor.extract(&replace.end.col), // replace.end.col cbor.extract_cbor(&additionalTextEdits), }) catch false; - return .{ label_, sort_text, kind, replace, additionalTextEdits, label_detail, label_description, detail, documentation }; + return .{ + .label = label_, + .sort_text = sort_text, + .kind = kind, + .replace = replace, + .additionalTextEdits = additionalTextEdits, + .label_detail = label_detail, + .label_description = label_description, + .detail = detail, + .documentation = documentation, + .insertTextFormat = insertTextFormat, + .textEdit_newText = textEdit_newText, + }; } const TextEdit = struct { newText: []const u8 = &.{}, insert: ?Range = null, replace: ?Range = null }; @@ -184,16 +210,17 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { const button = button_ orelse return cancel(palette); - const label_, _, _, const replace, _, _, _, const detail, const documentation = get_values(button.opts.label); + const values = get_values(button.opts.label); const editor = tui.get_active_editor() orelse return error.NotFound; - editor.get_primary().selection = get_replace_selection(replace); + editor.get_primary().selection = get_replace_selection(values.replace); const mv = tui.mainview() orelse return; - try mv.set_info_content(label_, .replace); + try mv.set_info_content(values.label, .replace); try mv.set_info_content(" ", .append); // blank line - try mv.set_info_content(detail, .append); + try mv.set_info_content(values.detail, .append); + try mv.set_info_content(values.textEdit_newText, .append); try mv.set_info_content(" ", .append); // blank line - try mv.set_info_content(documentation, .append); + try mv.set_info_content(values.documentation, .append); } pub fn cancel(palette: *Type) !void { From 075ec84cf0bba27fbac1fa20e0eb35443310b20b Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 8 Dec 2025 21:18:04 +0100 Subject: [PATCH 3/5] refactor: call snippet parser when inserting completion items --- src/tui/mode/overlay/completion_palette.zig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index f4b84a4..a616bdf 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -201,11 +201,20 @@ fn get_replace_selection(replace: Buffer.Selection) ?Buffer.Selection { } fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { - const label_, _, _, _, _, _, _, _, _ = get_values(button.opts.label); - tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "insert_chars", .{label_} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + const values = get_values(button.opts.label); + switch (values.insertTextFormat) { + 2 => { + const snippet = @import("snippet").parse(menu.*.opts.ctx.allocator, values.textEdit_newText) catch return; + defer snippet.deinit(menu.*.opts.ctx.allocator); + tp.self_pid().send(.{ "cmd", "insert_chars", .{snippet.text} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + }, + else => { + tp.self_pid().send(.{ "cmd", "insert_chars", .{values.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + }, + } const mv = tui.mainview() orelse return; mv.cancel_info_content() catch {}; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { From 3da09cac5587216e0981ba1dbe817974c28b3db5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 8 Dec 2025 21:24:02 +0100 Subject: [PATCH 4/5] fix: snippet parser should not output '$' --- src/snippet.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/snippet.zig b/src/snippet.zig index d2bddea..2f6f8e0 100644 --- a/src/snippet.zig +++ b/src/snippet.zig @@ -44,7 +44,6 @@ pub fn parse(allocator: std.mem.Allocator, snippet: []const u8) Error!Snippet { }, '$' => { state = .tabstop; - try text.writer.writeByte(c); }, else => try text.writer.writeByte(c), }, From e7118763a1790300f428edb82486a59ba0ef4e00 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 8 Dec 2025 21:31:24 +0100 Subject: [PATCH 5/5] refactor: remove snippet from info panel --- src/tui/mode/overlay/completion_palette.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index a616bdf..5b141c5 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -227,7 +227,7 @@ pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { try mv.set_info_content(values.label, .replace); try mv.set_info_content(" ", .append); // blank line try mv.set_info_content(values.detail, .append); - try mv.set_info_content(values.textEdit_newText, .append); + // try mv.set_info_content(values.textEdit_newText, .append); try mv.set_info_content(" ", .append); // blank line try mv.set_info_content(values.documentation, .append); }