From 60016a3d039f9cd17604683503edcb1e36cb0eee Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 11:52:42 +0200 Subject: [PATCH 1/6] feat: improve expand_selection by selecting top selection matching node --- src/tui/editor.zig | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 6932495..6e43585 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -170,9 +170,8 @@ 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 = .{ + fn selection_from_range(range: syntax.Range, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return .{ .begin = .{ .row = range.start_point.row, .col = try root.pos_to_width(range.start_point.row, range.start_point.column, metrics), @@ -182,7 +181,23 @@ pub const CurSel = struct { .col = try root.pos_to_width(range.end_point.row, range.end_point.column, metrics), }, }; - self.cursor = self.selection.?.end; + } + + fn selection_from_node(node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return selection_from_range(node.getRange(), root, metrics); + } + + fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { + const sel = try selection_from_node(node, root, metrics); + self.selection = sel; + self.cursor = sel.end; + } + + fn select_parent_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!syntax.Node { + const parent = node.getParent(); + if (parent.isNull()) return error.NotFound; + try self.select_node(parent, root, metrics); + return parent; } fn write(self: *const Self, writer: Buffer.MetaWriter) !void { @@ -4297,19 +4312,36 @@ pub const Editor = struct { return node; } - fn select_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { + fn top_node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + var node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return node; + var parent = node.getParent(); + if (parent.isNull()) return node; + const node_sel = CurSel.selection_from_node(node, root, metrics) catch return node; + var parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + while (parent_sel.eql(node_sel)) { + node = parent; + parent = parent.getParent(); + parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + } + return node; + } + + fn select_top_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { cursel.disable_selection(root, self.metrics); const sel = (try cursel.enable_selection(root, self.metrics)).*; - return cursel.select_node(try self.node_at_selection(sel, root, metrics), root, metrics); + return cursel.select_node(try self.top_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 = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + var node = try self.top_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); + var node_sel = try CurSel.selection_from_node(node, root, metrics); + if (!node_sel.eql(sel)) return cursel.select_node(node, root, metrics); + node = try cursel.select_parent_node(node, root, metrics); + while (cursel.selection.?.eql(sel)) + node = try cursel.select_parent_node(node, root, metrics); } pub fn expand_selection(self: *Self, _: Context) Result { @@ -4320,7 +4352,7 @@ pub const Editor = struct { 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.select_top_node_at_cursor(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } @@ -4363,7 +4395,7 @@ pub const Editor = struct { fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_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; @@ -4372,7 +4404,7 @@ pub const Editor = struct { fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_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; @@ -4398,7 +4430,7 @@ pub const Editor = struct { fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_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; @@ -4407,7 +4439,7 @@ pub const Editor = struct { fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_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; From 99dc805817033d5eddd1830697335491f5ef6114 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:25:13 +0200 Subject: [PATCH 2/6] feat: add flow mode keybinds for unnamed AST sibling movement --- src/keybind/builtin/flow.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 8f0a85a..e975af7 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -132,6 +132,10 @@ ["alt+shift+right", "expand_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], + ["alt+]", "shrink_selection", true], + ["alt+[", "expand_selection"], + ["alt+{", "select_prev_sibling", true], + ["alt+}", "select_next_sibling", true], ["alt+shift+e", "move_parent_node_end"], ["alt+shift+b", "move_parent_node_start"], ["alt+a", "select_all_siblings"], From 30af629a1ab770b36340e585eea8ddd59b973401 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:50:14 +0200 Subject: [PATCH 3/6] refactor: expose CurSel.to_selection method --- src/tui/editor.zig | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 6e43585..92ba6ba 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -102,37 +102,41 @@ pub const CurSel = struct { } pub fn enable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return switch (tui.get_selection_style()) { - .normal => self.enable_selection_normal(), - .inclusive => try self.enable_selection_inclusive(root, metrics), - }; + self.selection = try self.to_selection(root, metrics); + return if (self.selection) |*sel| sel else unreachable; } pub fn enable_selection_normal(self: *Self) *Selection { - return if (self.selection) |*sel| - sel - else cod: { - self.selection = Selection.from_cursor(&self.cursor); - break :cod &self.selection.?; + self.selection = self.to_selection_normal(); + return if (self.selection) |*sel| sel else unreachable; + } + + pub fn to_selection(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return switch (tui.get_selection_style()) { + .normal => self.to_selection_normal(), + .inclusive => try self.to_selection_inclusive(root, metrics), }; } - fn enable_selection_inclusive(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return if (self.selection) |*sel| + fn to_selection_normal(self: *const Self) Selection { + return if (self.selection) |sel| sel else Selection.from_cursor(&self.cursor); + } + + fn to_selection_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return if (self.selection) |sel| sel else cod: { - self.selection = Selection.from_cursor(&self.cursor); - try self.selection.?.end.move_right(root, metrics); - try self.cursor.move_right(root, metrics); - break :cod &self.selection.?; + var sel = Selection.from_cursor(&self.cursor); + try sel.end.move_right(root, metrics); + break :cod sel; }; } - fn to_inclusive_cursor(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { - var res = self.cursor; + fn to_cursor_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { + var cursor = self.cursor; if (self.selection) |sel| if (!sel.is_reversed()) - try res.move_left(root, metrics); - return res; + try cursor.move_left(root, metrics); + return cursor; } pub fn disable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) void { @@ -1207,7 +1211,7 @@ pub const Editor = struct { fn get_rendered_cursor(self: *Self, style: anytype, cursel: anytype) !Cursor { return switch (style) { .normal => cursel.cursor, - .inclusive => try cursel.to_inclusive_cursor(try self.buf_root(), self.metrics), + .inclusive => try cursel.to_cursor_inclusive(try self.buf_root(), self.metrics), }; } From 8100e7d52b55fd670c39c4ce469900df057b4f01 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:58:10 +0200 Subject: [PATCH 4/6] refactor: improve const correctness in AST navigation functions --- src/tui/editor.zig | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 92ba6ba..088cdf8 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4298,7 +4298,7 @@ pub const Editor = struct { } pub const selections_reverse_meta: Meta = .{ .description = "Reverse selection" }; - fn node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + fn node_at_selection(self: *const 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 = .{ @@ -4316,7 +4316,7 @@ pub const Editor = struct { return node; } - fn top_node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + fn top_node_at_selection(self: *const Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { var node = try self.node_at_selection(sel, root, metrics); if (node.isNull()) return node; var parent = node.getParent(); @@ -4331,10 +4331,9 @@ pub const Editor = struct { return node; } - fn select_top_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - cursel.disable_selection(root, self.metrics); - const sel = (try cursel.enable_selection(root, self.metrics)).*; - return cursel.select_node(try self.top_node_at_selection(sel, root, metrics), root, metrics); + fn top_node_at_cursel(self: *const Self, cursel: *const CurSel, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + const sel = try cursel.to_selection(root, metrics); + return try self.top_node_at_selection(sel, root, metrics); } fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { @@ -4356,7 +4355,7 @@ pub const Editor = struct { try if (cursel.selection) |_| self.expand_selection_to_parent_node(root, cursel, self.metrics) else - self.select_top_node_at_cursor(root, cursel, self.metrics); + cursel.select_node(try self.top_node_at_cursel(cursel, root, self.metrics), root, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } From 1ef77601e36e0521b7b4aba4a405646fccd51f4d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 12:58:42 +0200 Subject: [PATCH 5/6] feat: allow next/previous sibling functions to work with no selection --- src/tui/editor.zig | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 088cdf8..8252578 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4397,8 +4397,7 @@ pub const Editor = struct { pub const shrink_selection_meta: 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 = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4406,8 +4405,7 @@ pub const Editor = struct { } fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, 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; @@ -4421,19 +4419,17 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - 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); + 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: 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 = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4441,8 +4437,7 @@ pub const Editor = struct { } fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.top_node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, 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; @@ -4456,11 +4451,10 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - 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); + 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(); } From 52996ed57dd1f9d0a763f2019f4b120951022f86 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 22 Sep 2025 13:07:03 +0200 Subject: [PATCH 6/6] feat: make AST keybindings more intuitive --- src/keybind/builtin/flow.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index e975af7..c139a3a 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -128,14 +128,14 @@ ["alt+shift+s", "filter", "sort", "-u"], ["alt+shift+v", "paste"], ["alt+shift+i", "add_cursors_to_line_ends"], - ["alt+shift+left", "shrink_selection"], - ["alt+shift+right", "expand_selection"], + ["alt+shift+left", "expand_selection"], + ["alt+shift+right", "shrink_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], - ["alt+]", "shrink_selection", true], - ["alt+[", "expand_selection"], - ["alt+{", "select_prev_sibling", true], - ["alt+}", "select_next_sibling", true], + ["alt+{", "expand_selection"], + ["alt+}", "shrink_selection", true], + ["alt+[", "select_prev_sibling", true], + ["alt+]", "select_next_sibling", true], ["alt+shift+e", "move_parent_node_end"], ["alt+shift+b", "move_parent_node_start"], ["alt+a", "select_all_siblings"],