diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index ed576da..30ddc9c 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -165,6 +165,7 @@ ["shift+f11", "toggle_highlight_columns"], ["ctrl+f11", "toggle_inspector_view"], ["f12", "goto_definition"], + ["ctrl+.", "completion"], ["f34", "toggle_whitespace_mode"], ["escape", "cancel"], ["enter", "smart_insert_line"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 03b5900..d90d6cc 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -351,6 +351,9 @@ pub const Editor = struct { diag_hints: usize = 0, completions: std.ArrayListUnmanaged(u8) = .empty, + completion_row: usize = 0, + completion_col: usize = 0, + completion_is_complete: bool = true, enable_auto_save: bool, enable_format_on_save: bool, @@ -5678,10 +5681,13 @@ pub const Editor = struct { } pub fn add_completion(self: *Self, row: usize, col: usize, is_incomplete: bool, msg: tp.message) Result { + if (!(row == self.completion_row and col == self.completion_col)) { + self.completions.clearRetainingCapacity(); + self.completion_row = row; + self.completion_col = col; + } try self.completions.appendSlice(self.allocator, msg.buf); - _ = row; - _ = col; - _ = is_incomplete; + self.completion_is_complete = is_incomplete; } pub fn select(self: *Self, ctx: Context) Result { diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 197fbcf..288963a 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -827,8 +827,11 @@ const cmds = struct { tp.more, })) return error.InvalidAddDiagnosticArgument; file_path = project_manager.normalize_file_path(file_path); - if (self.get_active_editor()) |editor| if (std.mem.eql(u8, file_path, editor.file_path orelse "")) - try editor.add_completion(row, col, is_incomplete, ctx.args); + if (self.get_active_editor()) |editor| { + if (std.mem.eql(u8, file_path, editor.file_path orelse "")) + try editor.add_completion(row, col, is_incomplete, ctx.args); + try tui.open_overlay(@import("mode/overlay/completion_palette.zig").Type); + } } pub const add_completion_meta: Meta = .{ .arguments = &.{ diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig new file mode 100644 index 0000000..eac3856 --- /dev/null +++ b/src/tui/mode/overlay/completion_palette.zig @@ -0,0 +1,175 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const root = @import("root"); +const command = @import("command"); + +const tui = @import("../../tui.zig"); +pub const Type = @import("palette.zig").Create(@This()); +const module_name = @typeName(@This()); +const Widget = @import("../../Widget.zig"); + +pub const label = "Select completion"; +pub const name = "completion"; +pub const description = "completions"; +pub const icon = "󱎸 "; + +pub const Entry = struct { + label: []const u8, + sort_text: []const u8, + cbor: []const u8, +}; + +pub fn load_entries(palette: *Type) !usize { + const editor = tui.get_active_editor() orelse return error.NotFound; + var iter: []const u8 = editor.completions.items; + while (iter.len > 0) { + var cbor_item: []const u8 = undefined; + if (!try cbor.matchValue(&iter, cbor.extract_cbor(&cbor_item))) return error.BadCompletion; + (try palette.entries.addOne()).* = .{ .cbor = cbor_item, .label = undefined, .sort_text = undefined }; + } + + var max_label_len: usize = 0; + for (palette.entries.items) |*item| { + const label_, const sort_text, _ = get_values(item.cbor); + item.label = label_; + item.sort_text = sort_text; + max_label_len = @max(max_label_len, item.label.len); + } + + const less_fn = struct { + fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { + const lhs_str = if (lhs.sort_text.len > 0) lhs.sort_text else lhs.label; + const rhs_str = if (rhs.sort_text.len > 0) rhs.sort_text else rhs.label; + return std.mem.order(u8, lhs_str, rhs_str) == .lt; + } + }.less_fn; + std.mem.sort(Entry, palette.entries.items, {}, less_fn); + + return if (max_label_len > label.len + 3) 0 else label.len + 3 - max_label_len; +} + +pub fn clear_entries(palette: *Type) void { + palette.entries.clearRetainingCapacity(); +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.allocator); + defer value.deinit(); + const writer = value.writer(); + try writer.writeAll(entry.cbor); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { + var item_cbor: []const u8 = undefined; + var matches_cbor: []const u8 = undefined; + + var iter = button.opts.label; + 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 = get_values(item_cbor); + const icon_: []const u8 = kind_icon(@enumFromInt(kind)); + const color: u24 = 0x0; + const indicator: []const u8 = &.{}; + + return tui.render_file_item(&button.plane, label_, icon_, color, indicator, matches_cbor, button.active, selected, button.hover, theme); +} + +fn get_values(item_cbor: []const u8) struct { []const u8, []const u8, u8 } { + var label_: []const u8 = ""; + var sort_text: []const u8 = ""; + var kind: u8 = 0; + _ = cbor.match(item_cbor, .{ + cbor.any, // file_path + cbor.any, // row + cbor.any, // col + cbor.any, // is_incomplete + cbor.extract(&label_), // label + cbor.any, // label_detail + cbor.any, // label_description + cbor.extract(&kind), // kind + cbor.any, // detail + cbor.any, // documentation + cbor.any, // documentation_kind + cbor.extract(&sort_text), // sortText + cbor.any, // insertTextFormat + cbor.any, // textEdit_newText + cbor.any, // insert.begin.row + cbor.any, // insert.begin.col + cbor.any, // insert.end.row + cbor.any, // insert.end.col + cbor.any, // replace.begin.row + cbor.any, // replace.begin.col + cbor.any, // replace.end.row + cbor.any, // replace.end.col + }) catch false; + return .{ label_, sort_text, kind }; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) 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 CompletionItemKind = enum(u8) { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +}; + +fn kind_icon(kind: CompletionItemKind) []const u8 { + return switch (kind) { + .Text => "󰊄", + .Method => "", + .Function => "󰊕", + .Constructor => "", + .Field => "", + .Variable => "", + .Class => "", + .Interface => "", + .Module => "", + .Property => "", + .Unit => "󱔁", + .Value => "󱔁", + .Enum => "", + .Keyword => "", + .Snippet => "", + .Color => "", + .File => "", + .Reference => "※", + .Folder => "🗀", + .EnumMember => "", + .Constant => "", + .Struct => "", + .Event => "", + .Operator => "", + .TypeParameter => "", + }; +}