From e87e16547a17864b2a0cd574c1aa42d44125bc6c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 16:35:27 -0300 Subject: Alt-o, Alt-i and the other tree-sitter keys walk the file's syntax tree helix's expand/shrink, previous/next sibling, all siblings, all children and parent-node end/start (Alt-o/up, Alt-i/down, Alt-p/left, Alt-right, Alt-a, Alt-I, Alt-e, Alt-b; Alt-n stays the new terminal) walk a whole-file parse the file keeps (File.node_tree), made on the first such key after an edit and reused until the next, never per key. syntax.walkNodes follows helix's TreeCursor over every node, anonymous ones included, and Alt-i goes back to what Alt-o grew from while the selection still holds it. The harness runs without grammars, so the cases prove the plain-text no-op; a test replays eleven JSON walks whose results were taken from the installed hx. Co-Authored-By: Claude Opus 5.5 --- src/syntax.zig | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) (limited to 'src/syntax.zig') diff --git a/src/syntax.zig b/src/syntax.zig index 9c1a4f4b..89fc4198 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -337,6 +337,96 @@ pub fn supportsPath(path: []const u8) bool { return false; } +/// A whole file's parse, kept by the file for the keys that walk its nodes +/// (helix's Alt-o and kin). Opaque outside this file; null without a grammar. +pub fn parseTree(path: []const u8, content: []const u8) ?*anyopaque { + if (!enabled) return null; + const selected = (forExt(std.fs.path.extension(path)) catch return null) orelse return null; + const tree = selected.parser.parseString(content, null) orelse return null; + return @ptrCast(tree); +} + +pub fn freeTree(tree: *anyopaque) void { + if (!enabled) return; + const t: *ts.Tree = @ptrCast(@alignCast(tree)); + t.destroy(); +} + +/// Where a node walk goes from a range (helix object.rs and movement.rs). +pub const NodeWalk = @import("modal.zig").Normal.NodeWalk; +pub const Span = struct { from: usize, to: usize }; + +/// The spans `walk` takes the byte range [from, to) to, into `out`; `at` is +/// the range's cursor. None when the tree has nothing to say there. +pub fn walkNodes(tree_ptr: *anyopaque, walk: NodeWalk, from: usize, to: usize, at: usize, out: []Span) usize { + if (!enabled or out.len == 0) return 0; + const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr)); + const lo: u32 = @intCast(@min(from, std.math.maxInt(u32))); + const hi: u32 = @intCast(@min(to, std.math.maxInt(u32))); + if (walk == .parent_end or walk == .parent_start) { + var node = tree.rootNode().namedDescendantForByteRange(lo, hi) orelse return 0; + if (walk == .parent_end) { + out[0] = .{ .from = node.endByte(), .to = node.endByte() }; + return 1; + } + // already at the node's lo: its first ancestor that starts earlier + if (node.startByte() == at) { + const first = node.startByte(); + while (node.startByte() >= first or !node.isNamed()) node = node.parent() orelse break; + } + out[0] = .{ .from = node.startByte(), .to = node.startByte() }; + return 1; + } + var cursor = tree.walk(); + defer cursor.destroy(); + // helix TreeCursor.reset_to_byte_range: the smallest node holding the range + while (true) { + const node = cursor.node(); + if (lo < node.startByte() or hi > node.endByte()) { + _ = cursor.gotoParent(); + break; + } + if (cursor.gotoFirstChildForByte(lo) == null) break; + } + switch (walk) { + .expand => while (cursor.node().startByte() == lo and cursor.node().endByte() == hi) { + if (!cursor.gotoParent()) break; + }, + .shrink => _ = cursor.gotoFirstChild(), + .next_sibling => while (!cursor.gotoNextSibling()) { + if (!cursor.gotoParent()) break; + }, + .prev_sibling => while (!cursor.gotoPreviousSibling()) { + if (!cursor.gotoParent()) break; + }, + .all_siblings, .all_children => { + if (walk == .all_siblings) { + while (true) { + if (!cursor.gotoParent()) return 0; + if (cursor.node().childCount() > 1) break; + } + } + // the named children, or nothing to say + var n: usize = 0; + if (!cursor.gotoFirstChild()) return 0; + while (true) { + const child = cursor.node(); + if (child.isNamed() and n < out.len) { + out[n] = .{ .from = child.startByte(), .to = child.endByte() }; + n += 1; + } + if (!cursor.gotoNextSibling()) break; + } + return n; + }, + .parent_end, .parent_start => unreachable, + } + const node = cursor.node(); + out[0] = .{ .from = node.startByte(), .to = node.endByte() }; + return 1; +} + + /// Owned source analysis. Both slices use the allocator passed to analyzeSource. /// The parsed tree is released before returning; callers can cache this result. pub const SourceAnalysis = struct { -- cgit v1.3