Skip to content

Commit

Permalink
Add Cursorless support for tree-sitter query .scm files (#1448)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pokey authored Aug 22, 2023
1 parent 9e9f71f commit 2bc7a94
Show file tree
Hide file tree
Showing 58 changed files with 1,611 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/common/src/extensionDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const extensionDependencies = [
"scalameta.metals",
"ms-python.python",
"mrob95.vscode-talonscript",
"jrieken.vscode-tree-sitter-query",
];
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const testCases: TestCase[] = [
},

{
name: "should show error for capture with multiple start",
name: "should allow capture with multiple start",
captures: [
{
name: "@foo.start",
Expand All @@ -145,8 +145,8 @@ const testCases: TestCase[] = [
range: new Range(0, 2, 0, 3),
},
],
isValid: false,
expectedErrorMessageIds: ["TreeSitterQuery.checkCaptures.duplicate"],
isValid: true,
expectedErrorMessageIds: [],
},

{
Expand All @@ -157,7 +157,7 @@ const testCases: TestCase[] = [
range: new Range(0, 0, 0, 0),
},
{
name: "@foo.start",
name: "@foo",
range: new Range(0, 1, 0, 2),
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ export function checkCaptureStartEnd(

let shownError = false;

if (captures.length === 2) {
const startRange = captures.find(({ name }) => name.endsWith(".start"))
?.range;
const endRange = captures.find(({ name }) => name.endsWith(".end"))?.range;
if (startRange != null && endRange != null) {
if (startRange.end.isBeforeOrEqual(endRange.start)) {
// Found just a start and endpoint in the right order, so we're good
return true;
}

const lastStart = captures
.filter(({ name }) => name.endsWith(".start"))
.map(({ range: { end } }) => end)
.sort((a, b) => a.compareTo(b))
.at(-1);
const firstEnd = captures
.filter(({ name }) => name.endsWith(".end"))
.map(({ range: { start } }) => start)
.sort((a, b) => a.compareTo(b))
.at(0);
if (lastStart != null && firstEnd != null) {
if (lastStart.isAfter(firstEnd)) {
showError(
messages,
"TreeSitterQuery.checkCaptures.badOrder",
Expand All @@ -63,7 +65,7 @@ export function checkCaptureStartEnd(
shownError = true;
}

if (regularCount > 1 || startCount > 1 || endCount > 1) {
if (regularCount > 1) {
// Found duplicate captures
showError(
messages,
Expand All @@ -75,15 +77,5 @@ export function checkCaptureStartEnd(
shownError = true;
}

if (!shownError) {
// I don't think it's possible to get here, but just in case, show a generic
// error message
showError(
messages,
"TreeSitterQuery.checkCaptures.unexpected",
`Unexpected captures: ${captures}`,
);
}

return false;
return !shownError;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ class IsNthChild extends QueryPredicateOperator<IsNthChild> {
}
}

/**
* A predicate operator that returns true if the node has more than 1 child of
* type {@link type} (inclusive). For example, `(has-multiple-children-of-type?
* @foo bar)` will accept the match if the `@foo` capture has 2 or more children
* of type `bar`.
*/
class HasMultipleChildrenOfType extends QueryPredicateOperator<HasMultipleChildrenOfType> {
name = "has-multiple-children-of-type?" as const;
schema = z.tuple([q.node, q.string]);

run({ node }: MutableQueryCapture, type: string) {
const count = node.children.filter((n) => n.type === type).length;
return count > 1;
}
}

class ChildRange extends QueryPredicateOperator<ChildRange> {
name = "child-range!" as const;
schema = z.union([
Expand Down Expand Up @@ -170,4 +186,5 @@ export const queryPredicateOperators = [
new ShrinkToMatch(),
new AllowMultiple(),
new InsertionDelimiter(),
new HasMultipleChildrenOfType(),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
languageId: scm
command:
version: 6
spokenForm: bring name to air
action:
name: replaceWithTarget
source:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
destination:
type: primitive
insertionMode: to
target:
type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: a}
usePrePhraseSnapshot: true
initialState:
documentContents: |-
(aaa) @bbb @ccc @ddd
(eee) @fff
selections:
- anchor: {line: 1, character: 0}
active: {line: 1, character: 0}
marks:
default.a:
start: {line: 0, character: 1}
end: {line: 0, character: 4}
finalState:
documentContents: |-
(aaa) @fff
(eee) @fff
selections:
- anchor: {line: 1, character: 0}
active: {line: 1, character: 0}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
languageId: scm
command:
version: 6
spokenForm: change every name
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: everyScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: (aaa) @bbb @ccc @ddd
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: (aaa) @ @ @
selections:
- anchor: {line: 0, character: 7}
active: {line: 0, character: 7}
- anchor: {line: 0, character: 9}
active: {line: 0, character: 9}
- anchor: {line: 0, character: 11}
active: {line: 0, character: 11}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: scm
command:
version: 6
spokenForm: change name
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: (aaa) @bbb @ccc @ddd
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: (aaa) @
selections:
- anchor: {line: 0, character: 7}
active: {line: 0, character: 7}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: scm
command:
version: 6
spokenForm: change name
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: "eee: (aaa) @bbb @ccc @ddd"
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "eee: (aaa) @"
selections:
- anchor: {line: 0, character: 12}
active: {line: 0, character: 12}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: scm
command:
version: 6
spokenForm: change name
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: "eee: _ @bbb @ccc @ddd"
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "eee: _ @"
selections:
- anchor: {line: 0, character: 8}
active: {line: 0, character: 8}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
languageId: scm
command:
version: 6
spokenForm: change value
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: value}
usePrePhraseSnapshot: true
initialState:
documentContents: |-
(
aaa: (bbb) @ccc @ddd
eee: "fff" @ggg
hhh: (iii)
jjj: [(kkk)] @lll
mmm: ((nnn) (ooo))* @ppp
qqq: _ @rrr
)
selections:
- anchor: {line: 1, character: 4}
active: {line: 1, character: 4}
- anchor: {line: 2, character: 4}
active: {line: 2, character: 4}
- anchor: {line: 3, character: 4}
active: {line: 3, character: 4}
- anchor: {line: 4, character: 4}
active: {line: 4, character: 4}
- anchor: {line: 5, character: 4}
active: {line: 5, character: 4}
- anchor: {line: 6, character: 4}
active: {line: 6, character: 4}
marks: {}
finalState:
documentContents: |-
(
aaa: @ccc @ddd
eee: @ggg
hhh:
jjj: @lll
mmm: @ppp
qqq: @rrr
)
selections:
- anchor: {line: 1, character: 9}
active: {line: 1, character: 9}
- anchor: {line: 2, character: 9}
active: {line: 2, character: 9}
- anchor: {line: 3, character: 9}
active: {line: 3, character: 9}
- anchor: {line: 4, character: 9}
active: {line: 4, character: 9}
- anchor: {line: 5, character: 9}
active: {line: 5, character: 9}
- anchor: {line: 6, character: 9}
active: {line: 6, character: 9}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
languageId: scm
command:
version: 6
spokenForm: chuck key
action:
name: remove
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: collectionKey}
usePrePhraseSnapshot: true
initialState:
documentContents: |-
(
aaa: (bbb) @ccc
)
selections:
- anchor: {line: 1, character: 19}
active: {line: 1, character: 19}
marks: {}
finalState:
documentContents: |-
(
(bbb) @ccc
)
selections:
- anchor: {line: 1, character: 14}
active: {line: 1, character: 14}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: scm
command:
version: 6
spokenForm: chuck name
action:
name: remove
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: (aaa) @bbb @ccc @ddd
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
marks: {}
finalState:
documentContents: "(aaa) "
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: scm
command:
version: 6
spokenForm: chuck name
action:
name: remove
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: name}
usePrePhraseSnapshot: true
initialState:
documentContents: (aaa) @bbb @ccc @ddd
selections:
- anchor: {line: 0, character: 20}
active: {line: 0, character: 20}
marks: {}
finalState:
documentContents: "(aaa) @bbb @ccc "
selections:
- anchor: {line: 0, character: 16}
active: {line: 0, character: 16}
Loading

0 comments on commit 2bc7a94

Please sign in to comment.