Skip to content

Commit 1e5e0f0

Browse files
AndreasArvidssonpokeypre-commit-ci-lite[bot]
authored
Talon language support (#1555)
Resolves #817 **Condition** - [x] context match - [x] every context match **Command** - [x] command - [x] every command **Statement** - [x] command line - [x] command block - [x] settings block - [x] script action call - [x] script key action call - [x] script sleep action call - [x] script assignment - [x] key binding - [x] tag binding - [x] gamepad binding - [x] parrot binding - [x] face binding - [x] every statement in file - [x] every statement in command block - [x] every statement in settings block **Name / Collection key** - [x] context match left - [x] command left - [x] settings block left - [x] setting left - [x] tag() left - [x] key() left - [x] gamepad() left - [x] face() left - [x] parrot() left - [x] every name/key in context block - [x] every name/key in file **Value** - [x] context match right - [x] command right - [x] settings block right - [x] setting right - [x] tag() right - [x] key() right - [x] gamepad() right - [x] face() right - [x] parrot() right - [x] every value in context block - [x] every value in settings block - [x] every value in file **Interior** - [x] command right - [ ] settings block right - [x] every command interior in file **Function call** - [x] action - [x] key action - [x] sleep action - [x] every call in command block **Function callee** - [x] action - [x] key action - [x] sleep action - [x] every callee in command block **Argument or parameter** - [x] action - [x] key action - [x] sleep action - [x] every argument in action **Update Tree sitter patterns** - [x] Exclude leading/trailing whitespace from `implicit_string` Waiting for release of wenkokke/tree-sitter-talon#23 - [x] Removed trailing parentheses `key(` and `sleep(` actions ~~Waiting for release of wenkokke/tree-sitter-talon#26 Instead solved with a pattern predicate ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] Look into failures with new tree-sitter-talon. Do they occur on shipped version? - [ ] "every value bat" in aaa: "bbb" ccc: "ddd" - [x] wenkokke/tree-sitter-talon#29 - [x] `"take key"` targeting `settings()` - [x] `"take key"` targeting `tag()` - [x] Rollback whitespace removal - [x] If the above are all fine on shipped tree-sitter-talon, let's just file follow-up to upgrade tree-sitter-talon later - [ ] 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) - [x] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 11dff61 commit 1e5e0f0

File tree

96 files changed

+2696
-33
lines changed

Some content is hidden

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

96 files changed

+2696
-33
lines changed

cursorless-talon/src/modifiers/scopes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
"paragraph": "namedParagraph",
6565
"subparagraph": "subParagraph",
6666
"environment": "environment",
67+
# Talon
68+
"command": "command",
6769
# Text-based scope types
6870
"char": "character",
6971
"word": "word",

packages/common/src/extensionDependencies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export const extensionDependencies = [
33
"ms-toolsai.jupyter",
44
"scalameta.metals",
55
"ms-python.python",
6+
"mrob95.vscode-talonscript",
67
];

packages/common/src/types/command/PartialTargetDescriptor.types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export type SimpleScopeTypeType =
134134
| "xmlElement"
135135
| "xmlEndTag"
136136
| "xmlStartTag"
137+
| "notebookCell"
137138
// Latex scope types
138139
| "part"
139140
| "chapter"
@@ -154,7 +155,8 @@ export type SimpleScopeTypeType =
154155
| "nonWhitespaceSequence"
155156
| "boundedNonWhitespaceSequence"
156157
| "url"
157-
| "notebookCell";
158+
// Talon
159+
| "command";
158160

159161
export interface SimpleScopeType {
160162
type: SimpleScopeTypeType;

packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/modifiers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export const scopeSpokenForms = {
8484
namedParagraph: "paragraph",
8585
subParagraph: "subparagraph",
8686
environment: "environment",
87+
// Talon
88+
command: "command",
8789
// Text-based scope types
8890
character: "char",
8991
word: "word",

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Range } from "@cursorless/common";
1+
import { Range, TextDocument } from "@cursorless/common";
22
import { Point } from "web-tree-sitter";
33

44
/**
@@ -35,6 +35,9 @@ export interface QueryCapture {
3535
* captures with the same name and domain into a single scope with multiple
3636
* content ranges. */
3737
readonly allowMultiple: boolean;
38+
39+
/** The insertion delimiter to use if any */
40+
readonly insertionDelimiter: string | undefined;
3841
}
3942

4043
/**
@@ -57,8 +60,10 @@ export interface MutableQueryCapture extends QueryCapture {
5760
*/
5861
readonly node: Omit<SimpleSyntaxNode, "startPosition" | "endPosition">;
5962

63+
readonly document: TextDocument;
6064
range: Range;
6165
allowMultiple: boolean;
66+
insertionDelimiter: string | undefined;
6267
}
6368

6469
/**

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export class TreeSitterQuery {
7878
captures: captures.map(({ name, node }) => ({
7979
name,
8080
node,
81+
document,
8182
range: getNodeRange(node),
83+
insertionDelimiter: undefined,
8284
allowMultiple: false,
8385
})),
8486
}),
@@ -112,6 +114,9 @@ export class TreeSitterQuery {
112114
.map(({ range }) => range)
113115
.reduce((accumulator, range) => range.union(accumulator)),
114116
allowMultiple: captures.some((capture) => capture.allowMultiple),
117+
insertionDelimiter: captures.find(
118+
(capture) => capture.insertionDelimiter != null,
119+
)?.insertionDelimiter,
115120
};
116121
});
117122

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import assert = require("assert");
55

66
interface TestCase {
77
name: string;
8-
captures: Omit<QueryCapture, "allowMultiple">[];
8+
captures: Omit<QueryCapture, "allowMultiple" | "insertionDelimiter">[];
99
isValid: boolean;
1010
expectedErrorMessageIds: string[];
1111
}
@@ -192,6 +192,7 @@ suite("checkCaptureStartEnd", () => {
192192
testCase.captures.map((capture) => ({
193193
...capture,
194194
allowMultiple: false,
195+
insertionDelimiter: undefined,
195196
})),
196197
messages,
197198
);

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Range } from "@cursorless/common";
12
import z from "zod";
23
import { makeRangeFromPositions } from "../../util/nodeSelectors";
34
import { MutableQueryCapture } from "./QueryCapture";
@@ -18,6 +19,17 @@ class NotType extends QueryPredicateOperator<NotType> {
1819
}
1920
}
2021

22+
/**
23+
* A predicate operator that returns true if the nodes range is not empty.
24+
*/
25+
class NotEmpty extends QueryPredicateOperator<NotEmpty> {
26+
name = "not-empty?" as const;
27+
schema = z.tuple([q.node]);
28+
run({ range }: MutableQueryCapture) {
29+
return !range.isEmpty;
30+
}
31+
}
32+
2133
/**
2234
* A predicate operator that returns true if the node's parent is not of the
2335
* given type. For example, `(not-parent-type? @foo string)` will reject the
@@ -81,6 +93,47 @@ class ChildRange extends QueryPredicateOperator<ChildRange> {
8193
}
8294
}
8395

96+
/**
97+
* A predicate operator that modifies the range of the match to shrink to regex
98+
* match. For example, `(#shrink-to-match! @foo "\\S+")` will modify the range
99+
* of the `@foo` capture to exclude whitespace.
100+
*
101+
* If convenient, you can use a special capture group called `keep` to indicate
102+
* the part of the match that should be kept. For example,
103+
*
104+
* ```
105+
* (#shrink-to-match! @foo "^\s+(?<keep>.*)$")
106+
* ```
107+
*
108+
* will modify the range of the `@foo` capture to skip any leading whitespace.
109+
*/
110+
class ShrinkToMatch extends QueryPredicateOperator<ShrinkToMatch> {
111+
name = "shrink-to-match!" as const;
112+
schema = z.tuple([q.node, q.string]);
113+
114+
run(nodeInfo: MutableQueryCapture, pattern: string) {
115+
const { document, range } = nodeInfo;
116+
const text = document.getText(range);
117+
const match = text.match(new RegExp(pattern, "ds"));
118+
119+
if (match?.index == null) {
120+
throw Error(`No match for pattern '${pattern}'`);
121+
}
122+
123+
const [startOffset, endOffset] =
124+
match.indices?.groups?.keep ?? match.indices![0];
125+
126+
const baseOffset = document.offsetAt(range.start);
127+
128+
nodeInfo.range = new Range(
129+
document.positionAt(baseOffset + startOffset),
130+
document.positionAt(baseOffset + endOffset),
131+
);
132+
133+
return true;
134+
}
135+
}
136+
84137
class AllowMultiple extends QueryPredicateOperator<AllowMultiple> {
85138
name = "allow-multiple!" as const;
86139
schema = z.tuple([q.node]);
@@ -92,10 +145,29 @@ class AllowMultiple extends QueryPredicateOperator<AllowMultiple> {
92145
}
93146
}
94147

148+
/**
149+
* A predicate operator that sets the insertion delimiter of the match. For
150+
* example, `(#insertion-delimiter! @foo ", ")` will set the insertion delimiter
151+
* of the `@foo` capture to `", "`.
152+
*/
153+
class InsertionDelimiter extends QueryPredicateOperator<InsertionDelimiter> {
154+
name = "insertion-delimiter!" as const;
155+
schema = z.tuple([q.node, q.string]);
156+
157+
run(nodeInfo: MutableQueryCapture, insertionDelimiter: string) {
158+
nodeInfo.insertionDelimiter = insertionDelimiter;
159+
160+
return true;
161+
}
162+
}
163+
95164
export const queryPredicateOperators = [
96165
new NotType(),
166+
new NotEmpty(),
97167
new NotParentType(),
98168
new IsNthChild(),
99169
new ChildRange(),
170+
new ShrinkToMatch(),
100171
new AllowMultiple(),
172+
new InsertionDelimiter(),
101173
];

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

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Range } from "@cursorless/common";
2-
import { MutableQueryCapture } from "./QueryCapture";
1+
import { Range, TextDocument } from "@cursorless/common";
32
import { SyntaxNode } from "web-tree-sitter";
3+
import { MutableQueryCapture } from "./QueryCapture";
44
import { rewriteStartOfEndOf } from "./rewriteStartOfEndOf";
55
import assert = require("assert");
66

@@ -50,24 +50,21 @@ const testCases: TestCase[] = [
5050
},
5151
];
5252

53+
function fillOutCapture(capture: NameRange): MutableQueryCapture {
54+
return {
55+
...capture,
56+
allowMultiple: false,
57+
insertionDelimiter: undefined,
58+
document: null as unknown as TextDocument,
59+
node: null as unknown as SyntaxNode,
60+
};
61+
}
62+
5363
suite("rewriteStartOfEndOf", () => {
5464
for (const testCase of testCases) {
5565
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-
);
66+
const actual = rewriteStartOfEndOf(testCase.captures.map(fillOutCapture));
67+
assert.deepStrictEqual(actual, testCase.expected.map(fillOutCapture));
7168
});
7269
}
7370
});

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SimpleScopeType, TextEditor } from "@cursorless/common";
1+
import { Range, SimpleScopeType, TextEditor } from "@cursorless/common";
22
import { TreeSitterQuery } from "../../../../languages/TreeSitterQuery";
33
import { QueryMatch } from "../../../../languages/TreeSitterQuery/QueryCapture";
44
import ScopeTypeTarget from "../../../targets/ScopeTypeTarget";
@@ -48,25 +48,19 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
4848
return undefined;
4949
}
5050

51-
const { range: contentRange, allowMultiple } = capture;
51+
const { range: contentRange, allowMultiple, insertionDelimiter } = capture;
5252

5353
const domain =
5454
getRelatedRange(match, scopeTypeType, "domain", true) ?? contentRange;
5555

5656
const removalRange = getRelatedRange(match, scopeTypeType, "removal", true);
5757

58-
const leadingDelimiterRange = getRelatedRange(
59-
match,
60-
scopeTypeType,
61-
"leading",
62-
true,
58+
const leadingDelimiterRange = dropEmptyRange(
59+
getRelatedRange(match, scopeTypeType, "leading", true),
6360
);
6461

65-
const trailingDelimiterRange = getRelatedRange(
66-
match,
67-
scopeTypeType,
68-
"trailing",
69-
true,
62+
const trailingDelimiterRange = dropEmptyRange(
63+
getRelatedRange(match, scopeTypeType, "trailing", true),
7064
);
7165

7266
const interiorRange = getRelatedRange(
@@ -90,9 +84,13 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
9084
leadingDelimiterRange,
9185
trailingDelimiterRange,
9286
interiorRange,
93-
// FIXME: Add delimiter text
87+
delimiter: insertionDelimiter,
9488
}),
9589
],
9690
};
9791
}
9892
}
93+
94+
function dropEmptyRange(range?: Range) {
95+
return range != null && !range.isEmpty ? range : undefined;
96+
}

0 commit comments

Comments
 (0)