From b0a1b80acbcb5f7483789ea8545912f8eb3edd85 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 25 Nov 2024 18:51:37 +0100 Subject: [PATCH] feat: add expand/shrink_selection and select_next/prev_sibling commands --- src/buffer/Buffer.zig | 2 +- src/keybind/static/input/flow.zig | 2 + src/syntax/src/syntax.zig | 6 ++ src/tui/editor.zig | 167 ++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index 31ec237..69e0fa7 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -759,7 +759,7 @@ const Node = union(enum) { return if (found) ctx.result else error.NotFound; } - pub fn pos_to_width(self: *const Node, line: usize, pos: usize, metrics_: Metrics) !usize { + pub fn pos_to_width(self: *const Node, line: usize, pos: usize, metrics_: Metrics) error{NotFound}!usize { const do = struct { result: usize = 0, pos: usize, diff --git a/src/keybind/static/input/flow.zig b/src/keybind/static/input/flow.zig index fafe566..53c5c52 100644 --- a/src/keybind/static/input/flow.zig +++ b/src/keybind/static/input/flow.zig @@ -142,6 +142,8 @@ fn map_press(self: *Self, keypress: input.Key, egc: input.Key, modifiers: input. 'S' => self.cmd("filter", command.fmt(.{"sort"})), 'V' => self.cmd("paste", .{}), 'X' => self.cmd("open_command_palette", .{}), + input.key.home => self.cmd("select_prev_sibling", .{}), + input.key.end => self.cmd("select_next_sibling", .{}), input.key.left => self.cmd("jump_back", .{}), input.key.right => self.cmd("jump_forward", .{}), input.key.up => self.cmd("pull_up", .{}), diff --git a/src/syntax/src/syntax.zig b/src/syntax/src/syntax.zig index 9f9a21f..0f8ff97 100644 --- a/src/syntax/src/syntax.zig +++ b/src/syntax/src/syntax.zig @@ -184,3 +184,9 @@ pub fn highlights_at_point(self: *const Self, ctx: anytype, comptime cb: CallBac } return; } + +pub fn node_at_point_range(self: *const Self, range: Range) error{Stop}!treez.Node { + const tree = self.tree orelse return error.Stop; + const root_node = tree.getRootNode(); + return treez.Node.externs.ts_node_descendant_for_point_range(root_node, range.start_point, range.end_point); +} diff --git a/src/tui/editor.zig b/src/tui/editor.zig index b2234bb..f9bb554 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -111,6 +111,21 @@ pub const CurSel = struct { return sel; } + fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { + const range = node.getRange(); + self.selection = .{ + .begin = .{ + .row = range.start_point.row, + .col = try root.pos_to_width(range.start_point.row, range.start_point.column, metrics), + }, + .end = .{ + .row = range.end_point.row, + .col = try root.pos_to_width(range.end_point.row, range.end_point.column, metrics), + }, + }; + self.cursor = self.selection.?.end; + } + fn write(self: *const Self, writer: Buffer.MetaWriter) !void { try self.cursor.write(writer); if (self.selection) |sel| { @@ -2924,6 +2939,158 @@ pub const Editor = struct { } pub const selections_reverse_meta = .{ .description = "Reverse selection" }; + fn node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + const syn = self.syntax orelse return error.Stop; + const node = try syn.node_at_point_range(.{ + .start_point = .{ + .row = @intCast(sel.begin.row), + .column = @intCast(try root.get_line_width_to_pos(sel.begin.row, sel.begin.col, metrics)), + }, + .end_point = .{ + .row = @intCast(sel.end.row), + .column = @intCast(try root.get_line_width_to_pos(sel.end.row, sel.end.col, metrics)), + }, + .start_byte = 0, + .end_byte = 0, + }); + if (node.isNull()) return error.Stop; + return node; + } + + fn select_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + cursel.selection = null; + const sel = cursel.enable_selection().*; + return cursel.select_node(try self.node_at_selection(sel, root, metrics), root, metrics); + } + + fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return error.Stop; + const parent = node.getParent(); + if (parent.isNull()) return error.Stop; + return cursel.select_node(parent, root, metrics); + } + + pub fn expand_selection(self: *Self, _: Context) Result { + try self.send_editor_jump_source(); + const root = try self.buf_root(); + const cursel = self.get_primary(); + cursel.check_selection(); + try if (cursel.selection) |_| + self.expand_selection_to_parent_node(root, cursel, self.metrics) + else + self.select_node_at_cursor(root, cursel, self.metrics); + self.clamp(); + try self.send_editor_jump_destination(); + } + pub const expand_selection_meta = .{ .description = "Expand selection to AST parent node" }; + + fn shrink_selection_to_child_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull() or node.getChildCount() == 0) return error.Stop; + const child = node.getChild(0); + if (child.isNull()) return error.Stop; + return cursel.select_node(child, root, metrics); + } + + fn shrink_selection_to_named_child_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull() or node.getNamedChildCount() == 0) return error.Stop; + const child = node.getNamedChild(0); + if (child.isNull()) return error.Stop; + return cursel.select_node(child, root, metrics); + } + + pub fn shrink_selection(self: *Self, ctx: Context) Result { + var unnamed: bool = false; + _ = ctx.args.match(.{tp.extract(&unnamed)}) catch false; + try self.send_editor_jump_source(); + const root = try self.buf_root(); + const cursel = self.get_primary(); + cursel.check_selection(); + if (cursel.selection) |_| + try if (unnamed) + self.shrink_selection_to_child_node(root, cursel, self.metrics) + else + self.shrink_selection_to_named_child_node(root, cursel, self.metrics); + self.clamp(); + try self.send_editor_jump_destination(); + } + pub const shrink_selection_meta = .{ .description = "Shrink selection to first AST child node" }; + + fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return error.Stop; + const sibling = syntax.Node.externs.ts_node_next_sibling(node); + if (sibling.isNull()) return error.Stop; + return cursel.select_node(sibling, root, metrics); + } + + fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return error.Stop; + const sibling = syntax.Node.externs.ts_node_next_named_sibling(node); + if (sibling.isNull()) return error.Stop; + return cursel.select_node(sibling, root, metrics); + } + + pub fn select_next_sibling(self: *Self, ctx: Context) Result { + var unnamed: bool = false; + _ = ctx.args.match(.{tp.extract(&unnamed)}) catch false; + try self.send_editor_jump_source(); + const root = try self.buf_root(); + const cursel = self.get_primary(); + cursel.check_selection(); + if (cursel.selection) |_| + try if (unnamed) + self.select_next_sibling_node(root, cursel, self.metrics) + else + self.select_next_named_sibling_node(root, cursel, self.metrics); + self.clamp(); + try self.send_editor_jump_destination(); + } + pub const select_next_sibling_meta = .{ .description = "Move selection to next AST sibling node" }; + + fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return error.Stop; + const sibling = syntax.Node.externs.ts_node_prev_sibling(node); + if (sibling.isNull()) return error.Stop; + return cursel.select_node(sibling, root, metrics); + } + + fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + const sel = cursel.enable_selection().*; + const node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return error.Stop; + const sibling = syntax.Node.externs.ts_node_prev_named_sibling(node); + if (sibling.isNull()) return error.Stop; + return cursel.select_node(sibling, root, metrics); + } + + pub fn select_prev_sibling(self: *Self, ctx: Context) Result { + var unnamed: bool = false; + _ = ctx.args.match(.{tp.extract(&unnamed)}) catch false; + try self.send_editor_jump_source(); + const root = try self.buf_root(); + const cursel = self.get_primary(); + cursel.check_selection(); + if (cursel.selection) |_| + try if (unnamed) + self.select_prev_sibling_node(root, cursel, self.metrics) + else + self.select_prev_named_sibling_node(root, cursel, self.metrics); + self.clamp(); + try self.send_editor_jump_destination(); + } + pub const select_prev_sibling_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)}))