summaryrefslogtreecommitdiff
path: root/src/syntax.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:35:27 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commite87e16547a17864b2a0cd574c1aa42d44125bc6c (patch)
tree45a35aec572bc141c008bc6c17c7a2d5acf6a653 /src/syntax.zig
parent79cdf76fd2dd0ba03622bfe742b525b770d51684 (diff)
downloadpardes-e87e16547a17864b2a0cd574c1aa42d44125bc6c.tar.gz
pardes-e87e16547a17864b2a0cd574c1aa42d44125bc6c.zip
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 <[email protected]>
Diffstat (limited to 'src/syntax.zig')
-rw-r--r--src/syntax.zig90
1 files changed, 90 insertions, 0 deletions
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 {