summaryrefslogtreecommitdiff
path: root/src/syntax.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/syntax.zig')
-rw-r--r--src/syntax.zig237
1 files changed, 237 insertions, 0 deletions
diff --git a/src/syntax.zig b/src/syntax.zig
index 9930db20..8ed00bfa 100644
--- a/src/syntax.zig
+++ b/src/syntax.zig
@@ -3,6 +3,7 @@ const config = @import("pardes_config");
const tracy = @import("tracy.zig");
const grammar_manifest = @import("grammar_manifest.zig");
const look = @import("look.zig");
+const regexp = @import("regexp.zig");
const c_heap = @import("c_heap");
pub const enabled = config.syntax_highlighting;
@@ -27,6 +28,10 @@ const Spec = struct {
exts: []const []const u8,
language: *const fn () callconv(.c) *const ts.Language,
query_src: []const u8,
+ /// helix's textobjects.scm for the grammar (vendor/queries), "" for none
+ objects_src: []const u8 = "",
+ objects: ?*ts.Query = null,
+ objects_failed: bool = false,
selected: ?Selected = null,
capture_styles: [256]u8 = undefined,
};
@@ -131,6 +136,7 @@ fn initSpecs() [specCount()]Spec {
.exts = g.exts,
.language = @extern(LanguageFn, .{ .name = "tree_sitter_" ++ g.name }),
.query_src = querySrc(g),
+ .objects_src = @field(ts_queries, g.name ++ "_textobjects"),
};
i += 1;
}
@@ -173,6 +179,9 @@ pub fn stop() void {
selected.query.destroy();
}
spec.selected = null;
+ if (spec.objects) |q| q.destroy();
+ spec.objects = null;
+ spec.objects_failed = false;
}
ts_set_allocator(null, null, null, null);
syntax_started = false;
@@ -310,6 +319,234 @@ 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;
+}
+
+
+/// helix's textobjects, which its `]f`, `mif` and kin select: the name a
+/// query's captures begin with (`function.around`, `test.inside`).
+pub const Object = @import("modal.zig").Normal.Object;
+
+fn objectName(o: Object) []const u8 {
+ return if (o == .xml_element) "xml-element" else @tagName(o);
+}
+
+/// The grammar's textobject query for `path`, compiled the first time; null
+/// when the grammar has none.
+fn objectQuery(path: []const u8) ?*ts.Query {
+ const ext = std.fs.path.extension(path);
+ for (&specs) |*spec| for (spec.exts) |choice| {
+ if (!std.ascii.eqlIgnoreCase(ext, choice)) continue;
+ if (spec.objects) |q| return q;
+ if (spec.objects_src.len == 0 or spec.objects_failed) return null;
+ var error_offset: u32 = 0;
+ spec.objects = ts.Query.create(spec.language(), spec.objects_src, &error_offset) catch {
+ spec.objects_failed = true;
+ return null;
+ };
+ return spec.objects;
+ };
+ return null;
+}
+
+fn captureId(q: *const ts.Query, name: []const u8) ?u32 {
+ for (0..q.captureCount()) |i| if (std.mem.eql(u8, q.captureNameForId(@intCast(i)) orelse "", name)) return @intCast(i);
+ return null;
+}
+
+/// A match's `capture`, as one span over all its nodes (helix's grouped
+/// capture), when the match's `#eq?` and `#match?` hold.
+fn matchSpan(q: *const ts.Query, match: anytype, capture: u32, text: []const u8) ?Span {
+ var span: ?Span = null;
+ for (match.captures) |c| if (c.index == capture) {
+ const s: Span = .{ .from = c.node.startByte(), .to = c.node.endByte() };
+ span = if (span) |old| .{ .from = @min(old.from, s.from), .to = @max(old.to, s.to) } else s;
+ };
+ var found = span orelse return null;
+ // a node that takes its line's newline (this zig grammar's comments)
+ // is the line without it, as helix's grammars have it
+ if (found.to > found.from + 1 and found.to <= text.len and text[found.to - 1] == '\n') found.to -= 1;
+ const steps = q.predicatesForPattern(match.pattern_index);
+ var i: usize = 0;
+ while (i < steps.len) {
+ var end = i;
+ while (end < steps.len and steps[end].type != .done) end += 1;
+ const pred = steps[i..end];
+ i = end + 1;
+ if (pred.len != 3 or pred[0].type != .string or pred[1].type != .capture) continue;
+ const op = q.stringValueForId(pred[0].value_id) orelse continue;
+ const subject = for (match.captures) |c| {
+ if (c.index == pred[1].value_id) break text[@min(c.node.startByte(), text.len)..@min(c.node.endByte(), text.len)];
+ } else continue;
+ const want = if (pred[2].type == .string) q.stringValueForId(pred[2].value_id) orelse continue else for (match.captures) |c| {
+ if (c.index == pred[2].value_id) break text[@min(c.node.startByte(), text.len)..@min(c.node.endByte(), text.len)];
+ } else continue;
+ if (std.mem.eql(u8, op, "eq?")) {
+ if (!std.mem.eql(u8, subject, want)) return null;
+ } else if (std.mem.eql(u8, op, "match?")) {
+ var re = regexp.Regex.compile(want) catch continue;
+ if ((re.find(subject, 0, subject.len, subject.len) catch null) == null) return null;
+ }
+ }
+ return found;
+}
+
+/// `mi`/`ma`: the smallest `object.inside` (or `.around`) holding byte `at`
+/// (helix textobject_treesitter). None without a query or such an object.
+pub fn objectAt(path: []const u8, tree_ptr: *anyopaque, text: []const u8, object: Object, around: bool, at: usize) ?Span {
+ if (!enabled) return null;
+ const q = objectQuery(path) orelse return null;
+ var buf: [32]u8 = undefined;
+ const want = std.fmt.bufPrint(&buf, "{s}.{s}", .{ objectName(object), if (around) "around" else "inside" }) catch return null;
+ const capture = captureId(q, want) orelse return null;
+ const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr));
+ const cursor = ts.QueryCursor.create();
+ defer cursor.destroy();
+ cursor.exec(q, tree.rootNode());
+ var best: ?Span = null;
+ while (cursor.nextMatch()) |match| {
+ const s = matchSpan(q, match, capture, text) orelse continue;
+ if (!(s.from <= at and at < s.to)) continue;
+ if (best == null or s.to - s.from < best.?.to - best.?.from) best = s;
+ }
+ const s = best orelse return null;
+ if (s.from >= text.len or s.to >= text.len) return null;
+ return s;
+}
+
+/// `]f`/`[f` and kin: the next object starting after byte `at`, or the
+/// previous one ending before it (helix goto_treesitter_object), from the
+/// first of `.movement`, `.around`, `.inside` the query names.
+pub fn objectNext(path: []const u8, tree_ptr: *anyopaque, text: []const u8, object: Object, forward: bool, at: usize) ?Span {
+ if (!enabled) return null;
+ const q = objectQuery(path) orelse return null;
+ var buf: [32]u8 = undefined;
+ const capture = for ([_][]const u8{ "movement", "around", "inside" }) |kind| {
+ const want = std.fmt.bufPrint(&buf, "{s}.{s}", .{ objectName(object), kind }) catch return null;
+ if (captureId(q, want)) |id| break id;
+ } else return null;
+ const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr));
+ const cursor = ts.QueryCursor.create();
+ defer cursor.destroy();
+ cursor.exec(q, tree.rootNode());
+ var best: ?Span = null;
+ while (cursor.nextMatch()) |match| {
+ const s = matchSpan(q, match, capture, text) orelse continue;
+ if (forward) {
+ if (s.from <= at) continue;
+ if (best == null or s.from < best.?.from or (s.from == best.?.from and s.to > best.?.to)) best = s;
+ } else {
+ if (s.to >= at) continue;
+ if (best == null or s.to > best.?.to or (s.to == best.?.to and s.from < best.?.from)) best = s;
+ }
+ }
+ const s = best orelse return null;
+ if (s.from >= text.len or s.to >= text.len) return null;
+ return s;
+}
+
+test "every vendored textobject query compiles against its grammar" {
+ if (!enabled) return;
+ var failed: usize = 0;
+ for (&specs) |*spec| {
+ if (spec.objects_src.len == 0) continue;
+ var error_offset: u32 = 0;
+ const q = ts.Query.create(spec.language(), spec.objects_src, &error_offset) catch {
+ std.debug.print("textobjects for {s} do not compile (offset {d})\n", .{ spec.name, error_offset });
+ failed += 1;
+ continue;
+ };
+ q.destroy();
+ }
+ try std.testing.expectEqual(@as(usize, 0), failed);
+}
+
/// 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 {