diff options
Diffstat (limited to 'src/syntax.zig')
| -rw-r--r-- | src/syntax.zig | 144 |
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 { |
