diff --git a/src/Project.zig b/src/Project.zig index c83dec0..920e3dc 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1362,6 +1362,7 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: var documentation: []const u8 = ""; var documentation_kind: []const u8 = ""; var sortText: []const u8 = ""; + var insertText: []const u8 = ""; var insertTextFormat: usize = 0; var textEdit: TextEdit = .{}; var additionalTextEdits: [32]TextEdit = undefined; @@ -1402,6 +1403,8 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: try cbor.skipValue(&iter); } } + } else if (std.mem.eql(u8, field_name, "insertText")) { + if (!(try cbor.matchValue(&iter, cbor.extract(&insertText)))) return invalid_field("insertText"); } else if (std.mem.eql(u8, field_name, "sortText")) { if (!(try cbor.matchValue(&iter, cbor.extract(&sortText)))) return invalid_field("sortText"); } else if (std.mem.eql(u8, field_name, "insertTextFormat")) { @@ -1437,6 +1440,7 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: documentation, documentation_kind, sortText, + insertText, insertTextFormat, textEdit.newText, insert.start.line, diff --git a/src/snippet.zig b/src/snippet.zig index 2f6f8e0..f8cd437 100644 --- a/src/snippet.zig +++ b/src/snippet.zig @@ -70,7 +70,7 @@ pub fn parse(allocator: std.mem.Allocator, snippet: []const u8) Error!Snippet { max_id = @max(id orelse unreachable, max_id); id = null; state = .initial; - break :fsm; + continue :fsm .initial; }, }, .placeholder => switch (c) { @@ -136,8 +136,16 @@ pub fn parse(allocator: std.mem.Allocator, snippet: []const u8) Error!Snippet { for (tabstops.items) |item| if (item.id == n) { (try tabstop.addOne(allocator)).* = item.range; }; - (try result.addOne(allocator)).* = try tabstop.toOwnedSlice(allocator); + if (tabstop.items.len > 0) + (try result.addOne(allocator)).* = try tabstop.toOwnedSlice(allocator); } + var tabstop: std.ArrayList(Range) = .empty; + errdefer tabstop.deinit(allocator); + for (tabstops.items) |item| if (item.id == 0) { + (try tabstop.addOne(allocator)).* = item.range; + }; + if (tabstop.items.len > 0) + (try result.addOne(allocator)).* = try tabstop.toOwnedSlice(allocator); return .{ .text = try text.toOwnedSlice(), .tabstops = try result.toOwnedSlice(allocator), diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 00d056f..5a2a3be 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -18,6 +18,7 @@ const Cell = @import("renderer").Cell; const input = @import("input"); const command = @import("command"); const EventHandler = @import("EventHandler"); +const snippet = @import("snippet"); const scrollbar_v = @import("scrollbar_v.zig"); const editor_gutter = @import("editor_gutter.zig"); @@ -308,6 +309,7 @@ pub const Editor = struct { cursels: CurSel.List = .empty, cursels_saved: CurSel.List = .empty, + cursels_tabstops: std.ArrayList([]CurSel) = .empty, selection_mode: SelectMode = .char, selection_drag_initial: ?Selection = null, target_column: ?Cursor = null, @@ -432,6 +434,34 @@ pub const Editor = struct { return count; } + fn cancel_all_tabstops(self: *Self) void { + for (self.cursels_tabstops.items) |ts_list| self.allocator.free(ts_list); + self.cursels_tabstops.clearRetainingCapacity(); + } + + fn pop_tabstop(self: *Self) bool { + if (self.cursels_tabstops.items.len == 0) return false; + + const tabstops = self.cursels_tabstops.toOwnedSlice(self.allocator) catch return false; + defer { + self.allocator.free(tabstops[0]); + self.allocator.free(tabstops); + } + + self.cancel_all_matches(); + self.cancel_all_selections(); + self.cursels.clearRetainingCapacity(); + + for (tabstops[0]) |cursel| { + (self.cursels.addOne(self.allocator) catch return false).* = cursel; + if (builtin.mode == .Debug) + self.logger.print("pop tabstop 1 of {}", .{tabstops.len}); + } + for (tabstops[1..]) |tabstop| (self.cursels_tabstops.addOne(self.allocator) catch return false).* = tabstop; + + return true; + } + pub fn write_state(self: *const Self, writer: *std.Io.Writer) !void { try cbor.writeArrayHeader(writer, 10); try cbor.writeValue(writer, self.file_path orelse ""); @@ -544,6 +574,7 @@ pub const Editor = struct { self.diagnostics.deinit(self.allocator); self.completions.deinit(self.allocator); if (self.syntax) |syn| syn.destroy(tui.query_cache()); + self.cancel_all_tabstops(); self.cursels.deinit(self.allocator); self.matches.deinit(self.allocator); self.handlers.deinit(); @@ -2167,6 +2198,8 @@ pub const Editor = struct { cursel.nudge_insert(nudge); for (self.matches.items) |*match_| if (match_.*) |*match| match.nudge_insert(nudge); + for (self.cursels_tabstops.items) |tabstop| for (tabstop) |*cursel| + cursel.nudge_insert(nudge); } fn nudge_delete(self: *Self, nudge: Selection, exclude: *const CurSel, _: usize) void { @@ -2179,6 +2212,11 @@ pub const Editor = struct { if (!match.nudge_delete(nudge)) { self.matches.items[i] = null; }; + for (self.cursels_tabstops.items) |tabstop| for (tabstop) |*cursel| + if (!cursel.nudge_delete(nudge)) { + self.cancel_all_tabstops(); + break; + }; } pub fn delete_selection(self: *Self, root: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { @@ -3934,11 +3972,12 @@ pub const Editor = struct { } pub fn indent(self: *Self, ctx: Context) Result { + if (self.pop_tabstop()) return; const b = try self.buf_for_update(); const root = try self.with_cursels_mut_repeat(b.root, indent_cursel, b.allocator, ctx); try self.update_buf(root); } - pub const indent_meta: Meta = .{ .description = "Indent current line", .arguments = &.{.integer} }; + pub const indent_meta: Meta = .{ .description = "Indent current line (or pop tabstop)", .arguments = &.{.integer} }; fn unindent_cursor(self: *Self, root: Buffer.Root, cursor: *Cursor, cursor_protect: ?*Cursor, allocator: Allocator) error{Stop}!Buffer.Root { var newroot = root; @@ -4166,6 +4205,7 @@ pub const Editor = struct { pub const move_buffer_end_meta: Meta = .{ .description = "Move cursor to end of file" }; pub fn cancel(self: *Self, _: Context) Result { + self.cancel_all_tabstops(); self.cancel_all_selections(); self.cancel_all_matches(); @import("keybind").clear_integer_argument(); @@ -4618,10 +4658,52 @@ pub const Editor = struct { } pub const select_prev_sibling_meta: Meta = .{ .description = "Move selection to previous AST sibling node" }; - pub fn insert_chars(self: *Self, ctx: Context) Result { - var chars: []const u8 = undefined; - if (!try ctx.args.match(.{tp.extract(&chars)})) - return error.InvalidInsertCharsArgument; + pub fn insert_snippet(self: *Self, snippet_text: []const u8) Result { + self.logger.print("snippet: {s}", .{snippet_text}); + const value = try snippet.parse(self.allocator, snippet_text); + defer value.deinit(self.allocator); + + const root_ = try self.buf_root(); + const primary = self.get_primary(); + const cursor = if (primary.selection) |sel| sel.begin else primary.cursor; + const eol_mode = try self.buf_eol_mode(); + var cursor_pos: usize = 0; + _ = try root_.get_range(.{ + .begin = .{ .row = 0, .col = 0 }, + .end = cursor, + }, null, &cursor_pos, null, self.metrics); + + try self.insert_cursels(value.text); + const root = try self.buf_root(); + + if (self.count_cursels() > 1) + return; + + self.cancel_all_tabstops(); + for (value.tabstops) |ts| { + var cursels: std.ArrayList(CurSel) = .empty; + for (ts) |placeholder| { + const ts_begin_pos = cursor_pos + placeholder.begin.@"0"; + const ts_begin = root.byte_offset_to_line_and_col(ts_begin_pos, self.metrics, eol_mode); + const ts_end = if (placeholder.end) |end| blk: { + const ts_end_pos = cursor_pos + end.@"0"; + break :blk root.byte_offset_to_line_and_col(ts_end_pos, self.metrics, eol_mode); + } else null; + const p = (try cursels.addOne(self.allocator)); + p.* = if (ts_end) |ts_end_| .{ + .cursor = ts_end_, + .selection = .{ .begin = ts_begin, .end = ts_end_ }, + } else .{ + .cursor = ts_begin, + }; + if (p.selection) |sel| self.add_match_from_selection(sel); + } + (try self.cursels_tabstops.addOne(self.allocator)).* = try cursels.toOwnedSlice(self.allocator); + } + _ = self.pop_tabstop(); + } + + pub fn insert_cursels(self: *Self, chars: []const u8) Result { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { @@ -4630,6 +4712,13 @@ pub const Editor = struct { try self.update_buf(root); self.clamp(); } + + pub fn insert_chars(self: *Self, ctx: Context) Result { + var chars: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&chars)})) + return error.InvalidInsertCharsArgument; + return self.insert_cursels(chars); + } pub const insert_chars_meta: Meta = .{ .arguments = &.{.string} }; pub fn insert_line(self: *Self, _: Context) Result { @@ -5452,6 +5541,13 @@ pub const Editor = struct { (self.matches.addOne(self.allocator) catch return).* = match; } + fn add_match_from_selection(self: *Self, sel: Selection) void { + var match: Match = Match.from_selection(sel); + if (match.end.eql(self.get_primary().cursor)) + match.has_selection = true; + (self.matches.addOne(self.allocator) catch return).* = match; + } + fn find_selection_match(self: *const Self, sel: Selection) ?*Match { for (self.matches.items) |*match_| if (match_.*) |*match| { if (match.to_selection().eql(sel)) diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index 5b141c5..a0ee283 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -4,6 +4,7 @@ const tp = @import("thespian"); const root = @import("soft_root").root; const command = @import("command"); const Buffer = @import("Buffer"); +const builtin = @import("builtin"); const tui = @import("../../tui.zig"); pub const Type = @import("palette.zig").Create(@This()); @@ -46,9 +47,9 @@ pub fn load_entries(palette: *Type) !usize { var max_label_len: usize = 0; for (palette.entries.items) |*item| { const values = get_values(item.cbor); - if (get_replace_selection(values.replace)) |replace| { - if (palette.value.replace == null) palette.value.replace = replace; - } + if (palette.value.replace == null) if (get_replace_selection(values.replace)) |replace| { + palette.value.replace = replace; + }; item.label = values.label; item.sort_text = values.sort_text; @@ -131,6 +132,7 @@ const Values = struct { label_description: []const u8, detail: []const u8, documentation: []const u8, + insertText: []const u8, insertTextFormat: usize, textEdit_newText: []const u8, }; @@ -143,6 +145,7 @@ fn get_values(item_cbor: []const u8) Values { var documentation: []const u8 = ""; var sort_text: []const u8 = ""; var kind: u8 = 0; + var insertText: []const u8 = ""; var insertTextFormat: usize = 0; var textEdit_newText: []const u8 = ""; var replace: Buffer.Selection = .{}; @@ -160,6 +163,7 @@ fn get_values(item_cbor: []const u8) Values { cbor.extract(&documentation), // documentation cbor.any, // documentation_kind cbor.extract(&sort_text), // sortText + cbor.extract(&insertText), // insertText cbor.extract(&insertTextFormat), // insertTextFormat cbor.extract(&textEdit_newText), // textEdit_newText cbor.any, // insert.begin.row @@ -183,6 +187,7 @@ fn get_values(item_cbor: []const u8) Values { .detail = detail, .documentation = documentation, .insertTextFormat = insertTextFormat, + .insertText = insertText, .textEdit_newText = textEdit_newText, }; } @@ -192,25 +197,26 @@ const Range = struct { start: Position, end: Position }; const Position = struct { line: usize, character: usize }; fn get_replace_selection(replace: Buffer.Selection) ?Buffer.Selection { - return if (tui.get_active_editor()) |edt| - replace.from_pos(edt.buf_root() catch return null, edt.metrics) - else if (replace.empty()) + return if (replace.empty()) null + else if (tui.get_active_editor()) |edt| + replace.from_pos(edt.buf_root() catch return null, edt.metrics) else replace; } fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { const values = get_values(button.opts.label); + const editor = tui.get_active_editor() orelse return; + const text = if (values.insertText.len > 0) + values.insertText + else if (values.textEdit_newText.len > 0) + values.textEdit_newText + else + values.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); - }, + 2 => editor.insert_snippet(text) catch |e| menu.*.opts.ctx.logger.err(module_name, e), + else => editor.insert_cursels(text) catch |e| menu.*.opts.ctx.logger.err(module_name, e), } const mv = tui.mainview() orelse return; mv.cancel_info_content() catch {}; @@ -227,7 +233,12 @@ 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); + if (builtin.mode == .Debug) { + try mv.set_info_content("newText:", .append); // blank line + try mv.set_info_content(values.textEdit_newText, .append); + try mv.set_info_content("insertText:", .append); // blank line + try mv.set_info_content(values.insertText, .append); + } try mv.set_info_content(" ", .append); // blank line try mv.set_info_content(values.documentation, .append); }