diff --git a/src/Project.zig b/src/Project.zig index 178979e..06c9e75 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1587,14 +1587,18 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: insertText, insertTextFormat, textEdit.newText, - insert.start.line, - insert.start.character, - insert.end.line, - insert.end.character, - replace.start.line, - replace.start.character, - replace.end.line, - replace.end.character, + if (textEdit.insert == null) null else .{ + insert.start.line, + insert.start.character, + insert.end.line, + insert.end.character, + }, + if (textEdit.replace == null) null else .{ + replace.start.line, + replace.start.character, + replace.end.line, + replace.end.character, + }, additionalTextEdits[0..additionalTextEdits_len], }, }) catch |e| { diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 35f2e36..4e36a8e 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -398,9 +398,7 @@ ["ctrl+up", "palette_menu_up"], ["ctrl+down", "palette_menu_down"], ["ctrl+enter", "palette_menu_activate"], - ["tab", "palette_menu_complete"], - ["ctrl+backspace", "overlay_delete_word_left"], - ["backspace", "overlay_delete_backwards"] + ["tab", "palette_menu_complete"] ] }, "overlay/dropdown": { @@ -413,9 +411,7 @@ ["up", "palette_menu_up"], ["down", "palette_menu_down"], ["enter", "palette_menu_activate"], - ["tab", "palette_menu_complete"], - ["ctrl+backspace", "overlay_delete_word_left"], - ["backspace", "overlay_delete_backwards"] + ["tab", "palette_menu_complete"] ] }, "mini/numeric": { diff --git a/src/tui/editor.zig b/src/tui/editor.zig index d19dfba..d045f42 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4981,7 +4981,9 @@ pub const Editor = struct { primary.selection = s; break :blk s; } else primary.selection orelse Selection.from_cursor(&primary.cursor); - self.replicate_selection(sel); + if (self.has_secondary_cursors()) + self.replicate_selection(sel); + primary.selection = sel; switch (insertTextFormat) { 2 => try self.insert_snippet(text), @@ -4991,7 +4993,9 @@ pub const Editor = struct { pub fn update_completion_cursels(self: *Self, sel: Selection, text: []const u8) Result { const b = self.buf_for_update() catch return; - self.replicate_selection(sel); + if (self.has_secondary_cursors()) + self.replicate_selection(sel); + self.get_primary().selection = sel; var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { root = if (text.len > 0) @@ -6608,7 +6612,8 @@ pub const Editor = struct { return self.completions.data.items.len > 0; } - pub fn get_completion_replacement_selection(self: *Self, replace: Selection) ?Selection { + pub fn get_completion_replacement_selection(self: *Self, insert_: ?Selection, replace_: ?Selection) ?Selection { + const replace = replace_ orelse insert_ orelse return null; var sel = replace.from_pos(self.buf_root() catch return null, self.metrics); sel.normalize(); const cursor = self.get_primary().cursor; diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index 5eba3ef..5807bf8 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -30,6 +30,7 @@ pub const Entry = struct { }; pub const ValueType = struct { + editor: *ed.Editor = undefined, start: ed.CurSel = .{}, cursor: ed.Cursor = .{}, view: ed.View = .{}, @@ -40,6 +41,13 @@ pub const defaultValue: ValueType = .{}; var max_description: usize = 0; +pub fn init(self: *Type) error{ Stop, OutOfMemory }!void { + try self.value.commands.init(self); + self.value.editor = tui.get_active_editor() orelse return error.Stop; + self.value.cursor = self.value.editor.get_primary().cursor; + self.value.view = self.value.editor.view; +} + pub fn load_entries(self: *Type) !usize { max_description = 0; var max_label_len: usize = 0; @@ -47,9 +55,9 @@ pub fn load_entries(self: *Type) !usize { var existing: std.StringHashMapUnmanaged(void) = .empty; defer existing.deinit(self.allocator); - const editor = tui.get_active_editor() orelse return error.NotFound; - self.value.start = editor.get_primary().*; - var iter: []const u8 = editor.completions.data.items; + self.value.start = self.value.editor.get_primary().*; + self.value.replace = null; + var iter: []const u8 = self.value.editor.completions.data.items; while (iter.len > 0) { var cbor_item: []const u8 = undefined; if (!try cbor.matchValue(&iter, cbor.extract_cbor(&cbor_item))) return error.BadCompletion; @@ -59,7 +67,7 @@ pub fn load_entries(self: *Type) !usize { if (existing.contains(dup_text)) continue; try existing.put(self.allocator, dup_text, {}); - if (self.value.replace == null) if (get_replace_selection(values.replace)) |replace| { + if (self.value.replace == null) if (get_replace_selection(self.value.editor, values)) |replace| { self.value.replace = replace; }; const item = try self.entries.addOne(self.allocator); @@ -100,56 +108,39 @@ pub fn handle_event(self: *Type, _: tp.pid_ref, m: tp.message) tp.result { try m.match(.{ "E", "pos", tp.more }) or try m.match(.{ "E", "close" })) { - const editor = tui.get_active_editor() orelse return; - if (!self.value.cursor.eql(editor.get_primary().cursor) or !self.value.view.eql(editor.view)) { + const cursor = self.value.editor.get_primary().cursor; + if (self.value.cursor.row != cursor.row or + self.value.cursor.col > cursor.col or + !self.value.view.eql(self.value.editor.view)) + { tp.self_pid().send(.{ "cmd", "palette_menu_cancel" }) catch |e| self.logger.err(module_name, e); + } else { + update_query(self, cursor) catch |e| self.logger.err(module_name, e); } } } -pub fn initial_query(self: *Type, allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { - try self.value.commands.init(self); - const editor = tui.get_active_editor() orelse return allocator.dupe(u8, ""); - self.value.cursor = editor.get_primary().cursor; - self.value.view = editor.view; +pub fn initial_query(self: *Type, allocator: std.mem.Allocator) error{ Stop, OutOfMemory }![]const u8 { + return get_query_text(self, self.value.cursor, allocator); +} + +fn get_query_text(self: *Type, cursor: ed.Cursor, allocator: std.mem.Allocator) error{ Stop, OutOfMemory }![]const u8 { return if (self.value.replace) |replace| blk: { - const sel: Buffer.Selection = .{ .begin = replace.begin, .end = self.value.start.cursor }; - break :blk editor.get_selection(sel, allocator) catch break :blk allocator.dupe(u8, ""); + const sel: Buffer.Selection = .{ .begin = replace.begin, .end = cursor }; + break :blk try self.value.editor.get_selection(sel, allocator); } else allocator.dupe(u8, ""); } -pub fn update_query(self: *Type, query: []const u8) void { - const editor = tui.get_active_editor() orelse return; - const sel = get_insert_selection(self, editor.get_primary().cursor); - editor.update_completion_cursels(sel, query) catch |e| self.logger.err(module_name, e); - - const primary = editor.get_primary(); - self.value.cursor = primary.cursor; - self.value.view = editor.view; - if (self.value.replace) |*s| s.* = .{ .begin = s.begin, .end = self.value.cursor }; +fn update_query(self: *Type, cursor: ed.Cursor) error{OutOfMemory}!void { + const query = get_query_text(self, cursor, self.allocator) catch |e| switch (e) { + error.Stop => return, + else => |e_| return e_, + }; + defer self.allocator.free(query); tp.self_pid().send(.{ "cmd", "completion" }) catch |e| self.logger.err(module_name, e); return; } -pub fn delete_word_empty(self: *Type) void { - cancel(self) catch return; - tp.self_pid().send(.{ "cmd", "delete_word_left" }) catch |e| self.logger.err(module_name, e); -} - -pub fn delete_empty(self: *Type) void { - cancel(self) catch return; - tp.self_pid().send(.{ "cmd", "smart_delete_backward" }) catch |e| self.logger.err(module_name, e); -} - -fn get_insert_selection(self: *Type, cursor: ed.Cursor) ed.Selection { - return if (self.value.replace) |sel| - sel - else if (self.value.start.selection) |sel| - sel - else - .{ .begin = self.value.start.cursor, .end = cursor }; -} - pub fn clear_entries(self: *Type) void { self.entries.clearRetainingCapacity(); } @@ -164,7 +155,7 @@ pub fn add_menu_entry(self: *Type, entry: *Entry, matches: ?[]const usize) !void self.items += 1; } -pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.Theme, selected: bool) bool { +pub fn on_render_menu(self: *Type, button: *Type.ButtonType, theme: *const Widget.Theme, selected: bool) bool { var item_cbor: []const u8 = undefined; var matches_cbor: []const u8 = undefined; @@ -177,7 +168,7 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T const color: u24 = 0x0; if (tui.config().enable_terminal_cursor) blk: { - const cursor = (tui.get_active_editor() orelse break :blk).get_primary_abs() orelse break :blk; + const cursor = self.value.editor.get_primary_abs() orelse break :blk; tui.rdr().cursor_enable(@intCast(cursor.row), @intCast(cursor.col), tui.get_cursor_shape()) catch {}; } @@ -199,11 +190,12 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T ); } -const Values = struct { +pub const Values = struct { label: []const u8, sort_text: []const u8, kind: CompletionItemKind, - replace: Buffer.Selection, + insert: ?Buffer.Selection, + replace: ?Buffer.Selection, additionalTextEdits: []const u8, label_detail: []const u8, label_description: []const u8, @@ -214,7 +206,7 @@ const Values = struct { textEdit_newText: []const u8, }; -fn get_values(item_cbor: []const u8) Values { +pub fn get_values(item_cbor: []const u8) Values { var label_: []const u8 = ""; var label_detail: []const u8 = ""; var label_description: []const u8 = ""; @@ -225,7 +217,8 @@ fn get_values(item_cbor: []const u8) Values { var insertText: []const u8 = ""; var insertTextFormat: usize = 0; var textEdit_newText: []const u8 = ""; - var replace: Buffer.Selection = .{}; + var insert_cbor: []const u8 = &.{}; + var replace_cbor: []const u8 = &.{}; var additionalTextEdits: []const u8 = &.{}; _ = cbor.match(item_cbor, .{ cbor.any, // file_path @@ -243,21 +236,16 @@ fn get_values(item_cbor: []const u8) Values { cbor.extract(&insertText), // insertText cbor.extract(&insertTextFormat), // insertTextFormat cbor.extract(&textEdit_newText), // textEdit_newText - cbor.any, // insert.begin.row - cbor.any, // insert.begin.col - cbor.any, // insert.end.row - cbor.any, // insert.end.col - cbor.extract(&replace.begin.row), // replace.begin.row - cbor.extract(&replace.begin.col), // replace.begin.col - cbor.extract(&replace.end.row), // replace.end.row - cbor.extract(&replace.end.col), // replace.end.col + cbor.extract_cbor(&insert_cbor), + cbor.extract_cbor(&replace_cbor), cbor.extract_cbor(&additionalTextEdits), }) catch false; return .{ .label = label_, .sort_text = sort_text, .kind = @enumFromInt(kind), - .replace = replace, + .insert = get_range(insert_cbor), + .replace = get_range(replace_cbor), .additionalTextEdits = additionalTextEdits, .label_detail = label_detail, .label_description = label_description, @@ -269,37 +257,53 @@ fn get_values(item_cbor: []const u8) Values { }; } +fn get_range(range_cbor: []const u8) ?Buffer.Selection { + var range: Buffer.Selection = .{}; + return if (cbor.match(range_cbor, tp.null_) catch false) + null + else if (cbor.match(range_cbor, .{ + cbor.extract(&range.begin.row), + cbor.extract(&range.begin.col), + cbor.extract(&range.end.row), + cbor.extract(&range.end.col), + }) catch false) + range + else + null; +} + const TextEdit = struct { newText: []const u8 = &.{}, insert: ?Range = null, replace: ?Range = null }; 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 (replace.empty()) - null - else if (tui.get_active_editor()) |edt| - edt.get_completion_replacement_selection(replace) - else - replace; +pub fn get_replace_selection(editor: *ed.Editor, values: Values) ?Buffer.Selection { + return editor.get_completion_replacement_selection(values.insert, values.replace); } pub fn complete(self: *Type, _: ?*Type.ButtonType) !void { self.menu.activate_selected(); } +fn get_insert_selection(self: *Type, values: Values, cursor: ed.Cursor) ed.Selection { + return if (values.replace) |sel| + sel + else if (self.value.start.selection) |sel| + sel + else + .{ .begin = self.value.start.cursor, .end = cursor }; +} + fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { const self = menu.*.opts.ctx; const values = get_values(button.opts.label); - const editor = tui.get_active_editor() orelse return; - const sel = get_insert_selection(self, editor.get_primary().cursor); + const sel = get_insert_selection(self, values, self.value.editor.get_primary().cursor); const text = if (values.insertText.len > 0) values.insertText else if (values.textEdit_newText.len > 0) values.textEdit_newText else values.label; - editor.insert_completion(sel, text, values.insertTextFormat) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - self.value.cursor = editor.get_primary().cursor; - self.value.view = editor.view; + self.value.editor.insert_completion(sel, text, values.insertTextFormat) 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| self.logger.err(module_name, e); diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index bddc60a..5eaca9a 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -13,6 +13,10 @@ const ed = @import("../../editor.zig"); const module_name = @typeName(@This()); const Widget = @import("../../Widget.zig"); +const Values = @import("completion_dropdown.zig").Values; +const get_values = @import("completion_dropdown.zig").get_values; +const get_replace_selection = @import("completion_dropdown.zig").get_replace_selection; + pub const label = "Select completion"; pub const name = "completion"; pub const description = "completions"; @@ -53,7 +57,7 @@ pub fn load_entries(palette: *Type) !usize { if (existing.contains(dup_text)) continue; try existing.put(palette.allocator, dup_text, {}); - if (palette.value.replace == null) if (get_replace_selection(values.replace)) |replace| { + if (palette.value.replace == null) if (get_replace_selection(editor, values)) |replace| { palette.value.replace = replace; }; const item = (try palette.entries.addOne(palette.allocator)); @@ -135,89 +139,6 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T ); } -const Values = struct { - label: []const u8, - sort_text: []const u8, - kind: CompletionItemKind, - replace: Buffer.Selection, - additionalTextEdits: []const u8, - label_detail: []const u8, - label_description: []const u8, - detail: []const u8, - documentation: []const u8, - insertText: []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 = ""; - var detail: []const u8 = ""; - 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 = .{}; - var additionalTextEdits: []const u8 = &.{}; - _ = cbor.match(item_cbor, .{ - cbor.any, // file_path - cbor.any, // row - cbor.any, // col - cbor.any, // is_incomplete - cbor.extract(&label_), // label - cbor.extract(&label_detail), // label_detail - cbor.extract(&label_description), // label_description - cbor.extract(&kind), // kind - cbor.extract(&detail), // detail - 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 - cbor.any, // insert.begin.col - cbor.any, // insert.end.row - cbor.any, // insert.end.col - cbor.extract(&replace.begin.row), // replace.begin.row - cbor.extract(&replace.begin.col), // replace.begin.col - cbor.extract(&replace.end.row), // replace.end.row - cbor.extract(&replace.end.col), // replace.end.col - cbor.extract_cbor(&additionalTextEdits), - }) catch false; - return .{ - .label = label_, - .sort_text = sort_text, - .kind = @enumFromInt(kind), - .replace = replace, - .additionalTextEdits = additionalTextEdits, - .label_detail = label_detail, - .label_description = label_description, - .detail = detail, - .documentation = documentation, - .insertTextFormat = insertTextFormat, - .insertText = insertText, - .textEdit_newText = textEdit_newText, - }; -} - -const TextEdit = struct { newText: []const u8 = &.{}, insert: ?Range = null, replace: ?Range = null }; -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 (replace.empty()) - null - else if (tui.get_active_editor()) |edt| - edt.get_completion_replacement_selection(replace) - else - replace; -} - pub fn complete(palette: *Type, _: ?*Type.ButtonType) !void { palette.menu.activate_selected(); } @@ -241,7 +162,7 @@ pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { const button = button_ orelse return cancel(palette); const values = get_values(button.opts.label); const editor = tui.get_active_editor() orelse return error.NotFound; - editor.get_primary().selection = get_replace_selection(values.replace); + editor.get_primary().selection = get_replace_selection(editor, values); const mv = tui.mainview() orelse return; try mv.set_info_content(values.label, .replace); diff --git a/src/tui/mode/overlay/dropdown.zig b/src/tui/mode/overlay/dropdown.zig index e90e885..d637d17 100644 --- a/src/tui/mode/overlay/dropdown.zig +++ b/src/tui/mode/overlay/dropdown.zig @@ -84,15 +84,14 @@ pub fn Create(options: type) type { .mode = try keybind.mode(switch (tui.config().dropdown_keybinds) { .standard => "overlay/dropdown", .noninvasive => "overlay/dropdown-noninvasive", - }, allocator, .{ - .insert_command = "overlay_insert_bytes", - }), + }, allocator, .{}), .placement = if (@hasDecl(options, "placement")) options.placement else .top_center, }; try self.commands.init(self); self.mode.event_handler = EventHandler.to_owned(self); self.mode.name = options.name; if (self.menu.scrollbar) |scrollbar| scrollbar.style_factory = scrollbar_style; + if (@hasDecl(options, "init")) try options.init(self); self.longest_hint = if (@hasDecl(options, "load_entries_with_args")) try options.load_entries_with_args(self, ctx) else @@ -267,6 +266,11 @@ pub fn Create(options: type) type { return false; } + pub fn update_query(self: *Self, query: []const u8) !void { + try self.query.appendSlice(self.allocator, query); + return self.start_query(0); + } + fn start_query(self: *Self, n: usize) !void { self.items = 0; self.menu.reset_items(); @@ -353,51 +357,6 @@ pub fn Create(options: type) type { return matches.items.len; } - fn delete_word(self: *Self) !void { - if (self.query.items.len == 0 and @hasDecl(options, "delete_word_empty")) { - options.delete_word_empty(self); - return; - } - if (std.mem.lastIndexOfAny(u8, self.query.items, "/\\. -_")) |pos| { - self.query.shrinkRetainingCapacity(pos); - } else { - self.query.shrinkRetainingCapacity(0); - } - if (@hasDecl(options, "update_query")) - options.update_query(self, self.query.items); - return self.start_query(0); - } - - fn delete_code_point(self: *Self) !void { - if (self.query.items.len > 0) { - self.query.shrinkRetainingCapacity(self.query.items.len - tui.egc_last(self.query.items).len); - if (@hasDecl(options, "update_query")) - options.update_query(self, self.query.items); - } else { - if (@hasDecl(options, "delete_empty")) - options.delete_empty(self); - } - try self.start_query(0); - } - - fn insert_code_point(self: *Self, c: u32) !void { - var buf: [6]u8 = undefined; - const bytes = try input.ucs32_to_utf8(&[_]u32{c}, &buf); - try self.query.appendSlice(self.allocator, buf[0..bytes]); - if (@hasDecl(options, "update_query")) - options.update_query(self, self.query.items); - // std.log.debug("insert_code_point: '{s}'", .{self.query.items}); - return self.start_query(0); - } - - fn insert_bytes(self: *Self, bytes: []const u8) !void { - try self.query.appendSlice(self.allocator, bytes); - if (@hasDecl(options, "update_query")) - options.update_query(self, self.query.items); - // std.log.debug("insert_bytes: '{s}'", .{self.query.items}); - return self.start_query(0); - } - fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { try command.executeName(name_, ctx); } @@ -550,32 +509,6 @@ pub fn Create(options: type) type { } pub const palette_menu_cancel_meta: Meta = .{}; - pub fn overlay_delete_word_left(self: *Self, _: Ctx) Result { - self.delete_word() catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - pub const overlay_delete_word_left_meta: Meta = .{ .description = "Delete word to the left" }; - - pub fn overlay_delete_backwards(self: *Self, _: Ctx) Result { - self.delete_code_point() catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - pub const overlay_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; - - pub fn overlay_insert_code_point(self: *Self, ctx: Ctx) Result { - var egc: u32 = 0; - if (!try ctx.args.match(.{tp.extract(&egc)})) - return error.InvalidPaletteInsertCodePointArgument; - self.insert_code_point(egc) catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - pub const overlay_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; - - pub fn overlay_insert_bytes(self: *Self, ctx: Ctx) Result { - var bytes: []const u8 = undefined; - if (!try ctx.args.match(.{tp.extract(&bytes)})) - return error.InvalidPaletteInsertBytesArgument; - self.insert_bytes(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - pub const overlay_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; - pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { tui.set_next_style(widget_type); const padding = tui.get_widget_style(widget_type).padding;