Skip to content

Commit 6c1f724

Browse files
josharianpokey
andauthored
replace treesitter #start-position with .startof (#1682)
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. ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]>
1 parent 85ce0c7 commit 6c1f724

File tree

7 files changed

+120
-55
lines changed

7 files changed

+120
-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
@@ -68,7 +68,9 @@ export function checkCaptureStartEnd(
6868
showError(
6969
messages,
7070
"TreeSitterQuery.checkCaptures.duplicate",
71-
`A capture with the same name may only appear once in a single pattern: ${captures}`,
71+
`A capture with the same name may only appear once in a single pattern: ${captures.map(
72+
({ name }) => name,
73+
)}`,
7274
);
7375
shownError = true;
7476
}

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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 = Pick<MutableQueryCapture, "name" | "range">;
8+
9+
interface TestCase {
10+
name: string;
11+
captures: NameRange[];
12+
expected: 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+
{ name: "@collectionKey.startOf", range: new Range(1, 2, 1, 3) },
21+
],
22+
expected: [
23+
{ name: "@value.iteration.start", range: new Range(1, 2, 1, 2) },
24+
{ name: "@collectionKey", range: new Range(1, 2, 1, 2) },
25+
],
26+
},
27+
28+
{
29+
name: "should rewrite endOf to start of range",
30+
captures: [
31+
{ name: "@value.iteration.start.endOf", range: new Range(1, 2, 1, 3) },
32+
{ name: "@collectionKey.endOf", range: new Range(1, 2, 1, 3) },
33+
],
34+
expected: [
35+
{ name: "@value.iteration.start", range: new Range(1, 3, 1, 3) },
36+
{ name: "@collectionKey", range: new Range(1, 3, 1, 3) },
37+
],
38+
},
39+
40+
{
41+
name: "should leave other captures alone",
42+
captures: [
43+
{ name: "@value.iteration.start", range: new Range(1, 2, 1, 3) },
44+
{ name: "@collectionKey", range: new Range(1, 2, 1, 3) },
45+
],
46+
expected: [
47+
{ name: "@value.iteration.start", range: new Range(1, 2, 1, 3) },
48+
{ name: "@collectionKey", range: new Range(1, 2, 1, 3) },
49+
],
50+
},
51+
];
52+
53+
suite("rewriteStartOfEndOf", () => {
54+
for (const testCase of testCases) {
55+
test(testCase.name, () => {
56+
const actual = rewriteStartOfEndOf(
57+
testCase.captures.map((capture) => ({
58+
...capture,
59+
allowMultiple: false,
60+
node: null as unknown as SyntaxNode,
61+
})),
62+
);
63+
assert.deepStrictEqual(
64+
actual,
65+
testCase.expected.map((capture) => ({
66+
...capture,
67+
allowMultiple: false,
68+
node: null as unknown as SyntaxNode,
69+
})),
70+
);
71+
});
72+
}
73+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { MutableQueryCapture } from "./QueryCapture";
2+
3+
/**
4+
* Modifies captures by applying any `.startOf` or `.endOf` suffixes. For
5+
* example, if we have a capture `@value.startOf`, we would rename it to
6+
* `@value` and adjust the range to be the start of the original range.
7+
*
8+
* @param captures A list of captures
9+
* @returns rewritten captures, with .startOf and .endOf removed
10+
*/
11+
export function rewriteStartOfEndOf(
12+
captures: MutableQueryCapture[],
13+
): MutableQueryCapture[] {
14+
return captures.map((capture) => {
15+
// Remove trailing .startOf and .endOf, adjusting ranges.
16+
if (capture.name.endsWith(".startOf")) {
17+
return {
18+
...capture,
19+
name: capture.name.replace(/\.startOf$/, ""),
20+
range: capture.range.start.toEmptyRange(),
21+
};
22+
}
23+
if (capture.name.endsWith(".endOf")) {
24+
return {
25+
...capture,
26+
name: capture.name.replace(/\.endOf$/, ""),
27+
range: capture.range.end.toEmptyRange(),
28+
};
29+
}
30+
return capture;
31+
});
32+
}

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.startOf @_.domain.end
8683
)

0 commit comments

Comments
 (0)