summaryrefslogtreecommitdiff
path: root/src/syntax.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/syntax.zig')
-rw-r--r--src/syntax.zig144
1 files changed, 144 insertions, 0 deletions
diff --git a/src/syntax.zig b/src/syntax.zig
index 89fc4198..3a83800b 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;
@@ -427,6 +436,141 @@ pub fn walkNodes(tree_ptr: *anyopaque, walk: NodeWalk, from: usize, to: usize, a
}
+/// 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;
+ };
+ const found = span orelse return null;
+ 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 {