Skip to content

Commit a20f7d2

Browse files
committed
replace treesitter #start-position with .startof
This particular query pattern is going to be very common. It's worth having syntactic sugar for. In particular, this reduces the level of indentation required to express that a node's associated scope (iteration, trailing, leading, etc.) should start or end at the start or end of a different node. While we're here, make a related error message more helpful.
1 parent 80bacd9 commit a20f7d2

File tree

7 files changed

+107
-55
lines changed

7 files changed

+107
-55
lines changed

packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { parsePredicates } from "./parsePredicates";
88
import { predicateToString } from "./predicateToString";
99
import { groupBy, uniq } from "lodash";
1010
import { checkCaptureStartEnd } from "./checkCaptureStartEnd";
11+
import { rewriteStartOfEndOf } from "./rewriteStartOfEndOf";
1112

1213
/**
1314
* Wrapper around a tree-sitter query that provides a more convenient API, and
@@ -95,6 +96,7 @@ export class TreeSitterQuery {
9596
const captures: QueryCapture[] = Object.entries(
9697
groupBy(match.captures, ({ name }) => normalizeCaptureName(name)),
9798
).map(([name, captures]) => {
99+
captures = rewriteStartOfEndOf(captures);
98100
const capturesAreValid = checkCaptureStartEnd(
99101
captures,
100102
ide().messages,
@@ -123,7 +125,7 @@ export class TreeSitterQuery {
123125
}
124126

125127
function normalizeCaptureName(name: string): string {
126-
return name.replace(/\.(start|end)$/, "");
128+
return name.replace(/\.(start|end)(\.(startof|endof))?$/, "");
127129
}
128130

129131
function positionToPoint(start: Position): Point {

packages/cursorless-engine/src/languages/TreeSitterQuery/checkCaptureStartEnd.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ export function checkCaptureStartEnd(
6969
showError(
7070
messages,
7171
"TreeSitterQuery.checkCaptures.duplicate",
72-
`A capture with the same name may only appear once in a single pattern: ${captures}`,
72+
`A capture with the same name may only appear once in a single pattern: ${captures.map(
73+
({ name }) => name,
74+
)}`,
7375
);
7476
shownError = true;
7577
}

packages/cursorless-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Range } from "@cursorless/common";
21
import z from "zod";
32
import { makeRangeFromPositions } from "../../util/nodeSelectors";
43
import { MutableQueryCapture } from "./QueryCapture";
@@ -46,40 +45,6 @@ class IsNthChild extends QueryPredicateOperator<IsNthChild> {
4645
}
4746
}
4847

49-
/**
50-
* A predicate operator that modifies the range of the match to be a zero-width
51-
* range at the start of the node. For example, `(#start-position! @foo)` will
52-
* modify the range of the `@foo` capture to be a zero-width range at the start
53-
* of the `@foo` node.
54-
*/
55-
class StartPosition extends QueryPredicateOperator<StartPosition> {
56-
name = "start-position!" as const;
57-
schema = z.tuple([q.node]);
58-
59-
run(nodeInfo: MutableQueryCapture) {
60-
nodeInfo.range = new Range(nodeInfo.range.start, nodeInfo.range.start);
61-
62-
return true;
63-
}
64-
}
65-
66-
/**
67-
* A predicate operator that modifies the range of the match to be a zero-width
68-
* range at the end of the node. For example, `(#end-position! @foo)` will
69-
* modify the range of the `@foo` capture to be a zero-width range at the end of
70-
* the `@foo` node.
71-
*/
72-
class EndPosition extends QueryPredicateOperator<EndPosition> {
73-
name = "end-position!" as const;
74-
schema = z.tuple([q.node]);
75-
76-
run(nodeInfo: MutableQueryCapture) {
77-
nodeInfo.range = new Range(nodeInfo.range.end, nodeInfo.range.end);
78-
79-
return true;
80-
}
81-
}
82-
8348
class ChildRange extends QueryPredicateOperator<ChildRange> {
8449
name = "child-range!" as const;
8550
schema = z.union([
@@ -131,8 +96,6 @@ export const queryPredicateOperators = [
13196
new NotType(),
13297
new NotParentType(),
13398
new IsNthChild(),
134-
new StartPosition(),
135-
new EndPosition(),
13699
new ChildRange(),
137100
new AllowMultiple(),
138101
];
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Range } from "@cursorless/common";
2+
import { MutableQueryCapture } from "./QueryCapture";
3+
import { SyntaxNode } from "web-tree-sitter";
4+
import { rewriteStartOfEndOf } from "./rewriteStartOfEndOf";
5+
import assert = require("assert");
6+
7+
type NameRange = Omit<MutableQueryCapture, "allowMultiple" | "node">;
8+
9+
interface TestCase {
10+
name: string;
11+
captures: NameRange[];
12+
want: NameRange[];
13+
}
14+
15+
const testCases: TestCase[] = [
16+
{
17+
name: "should rewrite startof to start of range",
18+
captures: [
19+
{ name: "@value.iteration.start.startof", range: new Range(1, 2, 1, 3) },
20+
],
21+
want: [{ name: "@value.iteration.start", range: new Range(1, 2, 1, 2) }],
22+
},
23+
24+
{
25+
name: "should rewrite endof to start of range",
26+
captures: [
27+
{ name: "@value.iteration.start.endof", range: new Range(1, 2, 1, 3) },
28+
],
29+
want: [{ name: "@value.iteration.start", range: new Range(1, 3, 1, 3) }],
30+
},
31+
32+
{
33+
name: "should leave other captures alone",
34+
captures: [
35+
{ name: "@value.iteration.start", range: new Range(1, 2, 1, 3) },
36+
],
37+
want: [{ name: "@value.iteration.start", range: new Range(1, 2, 1, 3) }],
38+
},
39+
];
40+
41+
suite("rewriteStartOfEndOf", () => {
42+
for (const testCase of testCases) {
43+
test(testCase.name, () => {
44+
const got = rewriteStartOfEndOf(
45+
testCase.captures.map((capture) => ({
46+
...capture,
47+
allowMultiple: false,
48+
node: null as unknown as SyntaxNode,
49+
})),
50+
);
51+
assert.deepStrictEqual(
52+
got,
53+
testCase.want.map((capture) => ({
54+
...capture,
55+
allowMultiple: false,
56+
node: null as unknown as SyntaxNode,
57+
})),
58+
);
59+
});
60+
}
61+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MutableQueryCapture } from "./QueryCapture";
2+
3+
/**
4+
* Rewrite captures, absorbing .startof and .endof into ranges.
5+
*
6+
* @param captures A list of captures
7+
* @returns rewritten captures, with .startof and .endof removed
8+
*/
9+
export function rewriteStartOfEndOf(
10+
captures: MutableQueryCapture[],
11+
): MutableQueryCapture[] {
12+
// TODO: mutate instead of copy/create/return?
13+
return captures.map((capture) => {
14+
// Remove trailing .startof and .endof, adjusting ranges.
15+
if (capture.name.endsWith(".startof")) {
16+
return {
17+
...capture,
18+
name: capture.name.replace(/\.startof$/, ""),
19+
range: capture.range.start.toEmptyRange(),
20+
};
21+
}
22+
if (capture.name.endsWith(".endof")) {
23+
return {
24+
...capture,
25+
name: capture.name.replace(/\.endof$/, ""),
26+
range: capture.range.end.toEmptyRange(),
27+
};
28+
}
29+
return capture;
30+
});
31+
}

queries/javascript.core.scm

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,11 @@
9797
;; Treat interior of all bodies as iteration scopes for `name`, eg
9898
;;!! function foo() { }
9999
;;! ***
100-
(
101-
(_
102-
body: (_
103-
.
104-
"{" @name.iteration.start
105-
"}" @name.iteration.end
106-
.
107-
)
100+
(_
101+
body: (_
102+
.
103+
"{" @name.iteration.start.endof
104+
"}" @name.iteration.end.startof
105+
.
108106
)
109-
(#end-position! @name.iteration.start)
110-
(#start-position! @name.iteration.end)
111107
)

queries/javascript.jsx.scm

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,7 @@
7777
;;!! <>foo</>
7878
;;! {} {}
7979
;;! -- ---
80-
(
81-
(jsx_fragment
82-
"<" @_.domain.start
83-
">" @name @_.domain.end
84-
)
85-
(#start-position! @name)
80+
(jsx_fragment
81+
"<" @_.domain.start
82+
">" @name.start.startof @_.domain.end
8683
)

0 commit comments

Comments
 (0)