Skip to content

Commit 2bc7a94

Browse files
authored
Add Cursorless support for tree-sitter query .scm files (#1448)
This PR allows us to say things like `"take state"` when we're working on `.scm` files. - Depends on #1763 - Depends on #1800 ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [x] Add trailing delimiter to "key" - [x] File issue for "bring to name" when there are multiple names - [ ] 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
1 parent 9e9f71f commit 2bc7a94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1611
-26
lines changed

packages/common/src/extensionDependencies.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export const extensionDependencies = [
44
"scalameta.metals",
55
"ms-python.python",
66
"mrob95.vscode-talonscript",
7+
"jrieken.vscode-tree-sitter-query",
78
];

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const testCases: TestCase[] = [
130130
},
131131

132132
{
133-
name: "should show error for capture with multiple start",
133+
name: "should allow capture with multiple start",
134134
captures: [
135135
{
136136
name: "@foo.start",
@@ -145,8 +145,8 @@ const testCases: TestCase[] = [
145145
range: new Range(0, 2, 0, 3),
146146
},
147147
],
148-
isValid: false,
149-
expectedErrorMessageIds: ["TreeSitterQuery.checkCaptures.duplicate"],
148+
isValid: true,
149+
expectedErrorMessageIds: [],
150150
},
151151

152152
{
@@ -157,7 +157,7 @@ const testCases: TestCase[] = [
157157
range: new Range(0, 0, 0, 0),
158158
},
159159
{
160-
name: "@foo.start",
160+
name: "@foo",
161161
range: new Range(0, 1, 0, 2),
162162
},
163163
{

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

+14-22
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ export function checkCaptureStartEnd(
2727

2828
let shownError = false;
2929

30-
if (captures.length === 2) {
31-
const startRange = captures.find(({ name }) => name.endsWith(".start"))
32-
?.range;
33-
const endRange = captures.find(({ name }) => name.endsWith(".end"))?.range;
34-
if (startRange != null && endRange != null) {
35-
if (startRange.end.isBeforeOrEqual(endRange.start)) {
36-
// Found just a start and endpoint in the right order, so we're good
37-
return true;
38-
}
39-
30+
const lastStart = captures
31+
.filter(({ name }) => name.endsWith(".start"))
32+
.map(({ range: { end } }) => end)
33+
.sort((a, b) => a.compareTo(b))
34+
.at(-1);
35+
const firstEnd = captures
36+
.filter(({ name }) => name.endsWith(".end"))
37+
.map(({ range: { start } }) => start)
38+
.sort((a, b) => a.compareTo(b))
39+
.at(0);
40+
if (lastStart != null && firstEnd != null) {
41+
if (lastStart.isAfter(firstEnd)) {
4042
showError(
4143
messages,
4244
"TreeSitterQuery.checkCaptures.badOrder",
@@ -63,7 +65,7 @@ export function checkCaptureStartEnd(
6365
shownError = true;
6466
}
6567

66-
if (regularCount > 1 || startCount > 1 || endCount > 1) {
68+
if (regularCount > 1) {
6769
// Found duplicate captures
6870
showError(
6971
messages,
@@ -75,15 +77,5 @@ export function checkCaptureStartEnd(
7577
shownError = true;
7678
}
7779

78-
if (!shownError) {
79-
// I don't think it's possible to get here, but just in case, show a generic
80-
// error message
81-
showError(
82-
messages,
83-
"TreeSitterQuery.checkCaptures.unexpected",
84-
`Unexpected captures: ${captures}`,
85-
);
86-
}
87-
88-
return false;
80+
return !shownError;
8981
}

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

+17
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ class IsNthChild extends QueryPredicateOperator<IsNthChild> {
5757
}
5858
}
5959

60+
/**
61+
* A predicate operator that returns true if the node has more than 1 child of
62+
* type {@link type} (inclusive). For example, `(has-multiple-children-of-type?
63+
* @foo bar)` will accept the match if the `@foo` capture has 2 or more children
64+
* of type `bar`.
65+
*/
66+
class HasMultipleChildrenOfType extends QueryPredicateOperator<HasMultipleChildrenOfType> {
67+
name = "has-multiple-children-of-type?" as const;
68+
schema = z.tuple([q.node, q.string]);
69+
70+
run({ node }: MutableQueryCapture, type: string) {
71+
const count = node.children.filter((n) => n.type === type).length;
72+
return count > 1;
73+
}
74+
}
75+
6076
class ChildRange extends QueryPredicateOperator<ChildRange> {
6177
name = "child-range!" as const;
6278
schema = z.union([
@@ -170,4 +186,5 @@ export const queryPredicateOperators = [
170186
new ShrinkToMatch(),
171187
new AllowMultiple(),
172188
new InsertionDelimiter(),
189+
new HasMultipleChildrenOfType(),
173190
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: bring name to air
5+
action:
6+
name: replaceWithTarget
7+
source:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
destination:
13+
type: primitive
14+
insertionMode: to
15+
target:
16+
type: primitive
17+
mark: {type: decoratedSymbol, symbolColor: default, character: a}
18+
usePrePhraseSnapshot: true
19+
initialState:
20+
documentContents: |-
21+
(aaa) @bbb @ccc @ddd
22+
(eee) @fff
23+
selections:
24+
- anchor: {line: 1, character: 0}
25+
active: {line: 1, character: 0}
26+
marks:
27+
default.a:
28+
start: {line: 0, character: 1}
29+
end: {line: 0, character: 4}
30+
finalState:
31+
documentContents: |-
32+
(aaa) @fff
33+
(eee) @fff
34+
selections:
35+
- anchor: {line: 1, character: 0}
36+
active: {line: 1, character: 0}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: change every name
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: everyScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: (aaa) @bbb @ccc @ddd
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks: {}
19+
finalState:
20+
documentContents: (aaa) @ @ @
21+
selections:
22+
- anchor: {line: 0, character: 7}
23+
active: {line: 0, character: 7}
24+
- anchor: {line: 0, character: 9}
25+
active: {line: 0, character: 9}
26+
- anchor: {line: 0, character: 11}
27+
active: {line: 0, character: 11}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: change name
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: (aaa) @bbb @ccc @ddd
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks: {}
19+
finalState:
20+
documentContents: (aaa) @
21+
selections:
22+
- anchor: {line: 0, character: 7}
23+
active: {line: 0, character: 7}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: change name
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: "eee: (aaa) @bbb @ccc @ddd"
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks: {}
19+
finalState:
20+
documentContents: "eee: (aaa) @"
21+
selections:
22+
- anchor: {line: 0, character: 12}
23+
active: {line: 0, character: 12}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: change name
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: "eee: _ @bbb @ccc @ddd"
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks: {}
19+
finalState:
20+
documentContents: "eee: _ @"
21+
selections:
22+
- anchor: {line: 0, character: 8}
23+
active: {line: 0, character: 8}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: change value
5+
action:
6+
name: clearAndSetSelection
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: value}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: |-
15+
(
16+
aaa: (bbb) @ccc @ddd
17+
eee: "fff" @ggg
18+
hhh: (iii)
19+
jjj: [(kkk)] @lll
20+
mmm: ((nnn) (ooo))* @ppp
21+
qqq: _ @rrr
22+
)
23+
selections:
24+
- anchor: {line: 1, character: 4}
25+
active: {line: 1, character: 4}
26+
- anchor: {line: 2, character: 4}
27+
active: {line: 2, character: 4}
28+
- anchor: {line: 3, character: 4}
29+
active: {line: 3, character: 4}
30+
- anchor: {line: 4, character: 4}
31+
active: {line: 4, character: 4}
32+
- anchor: {line: 5, character: 4}
33+
active: {line: 5, character: 4}
34+
- anchor: {line: 6, character: 4}
35+
active: {line: 6, character: 4}
36+
marks: {}
37+
finalState:
38+
documentContents: |-
39+
(
40+
aaa: @ccc @ddd
41+
eee: @ggg
42+
hhh:
43+
jjj: @lll
44+
mmm: @ppp
45+
qqq: @rrr
46+
)
47+
selections:
48+
- anchor: {line: 1, character: 9}
49+
active: {line: 1, character: 9}
50+
- anchor: {line: 2, character: 9}
51+
active: {line: 2, character: 9}
52+
- anchor: {line: 3, character: 9}
53+
active: {line: 3, character: 9}
54+
- anchor: {line: 4, character: 9}
55+
active: {line: 4, character: 9}
56+
- anchor: {line: 5, character: 9}
57+
active: {line: 5, character: 9}
58+
- anchor: {line: 6, character: 9}
59+
active: {line: 6, character: 9}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: chuck key
5+
action:
6+
name: remove
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: collectionKey}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: |-
15+
(
16+
aaa: (bbb) @ccc
17+
)
18+
selections:
19+
- anchor: {line: 1, character: 19}
20+
active: {line: 1, character: 19}
21+
marks: {}
22+
finalState:
23+
documentContents: |-
24+
(
25+
(bbb) @ccc
26+
)
27+
selections:
28+
- anchor: {line: 1, character: 14}
29+
active: {line: 1, character: 14}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: chuck name
5+
action:
6+
name: remove
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: (aaa) @bbb @ccc @ddd
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks: {}
19+
finalState:
20+
documentContents: "(aaa) "
21+
selections:
22+
- anchor: {line: 0, character: 0}
23+
active: {line: 0, character: 0}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: scm
2+
command:
3+
version: 6
4+
spokenForm: chuck name
5+
action:
6+
name: remove
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: name}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: (aaa) @bbb @ccc @ddd
15+
selections:
16+
- anchor: {line: 0, character: 20}
17+
active: {line: 0, character: 20}
18+
marks: {}
19+
finalState:
20+
documentContents: "(aaa) @bbb @ccc "
21+
selections:
22+
- anchor: {line: 0, character: 16}
23+
active: {line: 0, character: 16}

0 commit comments

Comments
 (0)