Compare commits

...

15 commits

Author SHA1 Message Date
4a44838b88
refactor: nudge tabstops 2025-12-09 20:16:38 +01:00
b472300b3d
refactor: remove render_tabstops 2025-12-09 20:16:16 +01:00
94109da73e
feat: add editor.pop_tabstop 2025-12-09 20:05:34 +01:00
a897e6bf87
fix: support for $0 in snippets 2025-12-09 20:05:06 +01:00
50db9082d8
refactor: attempt to render tabstops 2025-12-09 19:21:20 +01:00
025ef9c768
refactor: add editor.insert_snippet 2025-12-09 19:20:37 +01:00
c462e3abda
refactor: split editor.insert_chars into insert_chars and insert_cursels 2025-12-09 19:19:39 +01:00
098d925358
refactor: add editor.add_match_from_selection function 2025-12-09 19:18:26 +01:00
8152de3df9
refactor: prefer completion insertText over newText or label 2025-12-09 19:17:30 +01:00
387a3416c3
refactor: add cursels_tabstops member to editor 2025-12-09 18:56:58 +01:00
c71ddb2900
refactor: add newText and insertText to info panel in debug builds 2025-12-09 18:55:47 +01:00
62bc86d2db
refactor: add insertText to completion_palette get_values function 2025-12-09 18:54:04 +01:00
b22337a2b3
refactor: extract insertText from completion responses 2025-12-09 18:52:46 +01:00
19751e7fd4
fix: incorrect use of a labeled switch in snippet 2025-12-09 18:52:01 +01:00
288e23e8b0
fix: completion with no replacements causes OOM 2025-12-09 18:32:41 +01:00
4 changed files with 141 additions and 22 deletions

View file

@ -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,

View file

@ -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),

View file

@ -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))

View file

@ -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);
}