Skip to content

Commit d861d83

Browse files
Use parse tree
1 parent 423cd0f commit d861d83

File tree

15 files changed

+140
-97
lines changed

15 files changed

+140
-97
lines changed

cursorless-talon/src/spoken_forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]):
160160
"private.switchStatementSubject",
161161
"textFragment",
162162
"disqualifyDelimiter",
163+
"pairDelimiter",
163164
],
164165
default_list_name="scope_type",
165166
),
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"server"
2+
'server'
3+
"""server"""
4+
'''server'''
5+
r" r"
6+
r' r'
7+
r""" r"""
8+
r''' r'''
9+
---
10+
11+
[#1 Content] =
12+
[#1 Domain] = 4:0-4:2
13+
>--<
14+
4| r" r"
15+
16+
[#1 Removal] = 4:0-4:3
17+
>---<
18+
4| r" r"
19+
20+
[#1 Trailing delimiter] = 4:2-4:3
21+
>-<
22+
4| r" r"
23+
24+
[#1 Insertion delimiter] = " "
25+
26+
27+
[#2 Content] =
28+
[#2 Domain] = 5:0-5:2
29+
>--<
30+
5| r' r'
31+
32+
[#2 Removal] = 5:0-5:3
33+
>---<
34+
5| r' r'
35+
36+
[#2 Trailing delimiter] = 5:2-5:3
37+
>-<
38+
5| r' r'
39+
40+
[#2 Insertion delimiter] = " "
41+
42+
43+
[#3 Content] =
44+
[#3 Domain] = 6:0-6:4
45+
>----<
46+
6| r""" r"""
47+
48+
[#3 Removal] = 6:0-6:5
49+
>-----<
50+
6| r""" r"""
51+
52+
[#3 Trailing delimiter] = 6:4-6:5
53+
>-<
54+
6| r""" r"""
55+
56+
[#3 Insertion delimiter] = " "
57+
58+
59+
[#4 Content] =
60+
[#4 Domain] = 7:0-7:4
61+
>----<
62+
7| r''' r'''
63+
64+
[#4 Removal] = 7:0-7:5
65+
>-----<
66+
7| r''' r'''
67+
68+
[#4 Trailing delimiter] = 7:4-7:5
69+
>-<
70+
7| r''' r'''
71+
72+
[#4 Insertion delimiter] = " "

packages/common/src/scopeSupportFacets/python.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const pythonScopeSupport: LanguageScopeSupportFacetMap = {
1414
namedFunction: supported,
1515
anonymousFunction: supported,
1616
disqualifyDelimiter: supported,
17+
pairDelimiter: supported,
1718

1819
"argument.actual": supported,
1920
"argument.actual.iteration": supported,

packages/common/src/scopeSupportFacets/scopeSupportFacetInfos.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ export const scopeSupportFacetInfos: Record<
298298
"Used to disqualify a token from being treated as a surrounding pair delimiter. This will usually be operators containing `>` or `<`, eg `<`, `<=`, `->`, etc",
299299
scopeType: "disqualifyDelimiter",
300300
},
301+
pairDelimiter: {
302+
description:
303+
"A pair delimiter, eg parentheses, brackets, braces, quotes, etc",
304+
scopeType: "pairDelimiter",
305+
},
301306

302307
"branch.if": {
303308
description: "An if/elif/else branch",

packages/common/src/scopeSupportFacets/scopeSupportFacets.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const scopeSupportFacets = [
8080
"textFragment.string.multiLine",
8181

8282
"disqualifyDelimiter",
83+
"pairDelimiter",
8384

8485
"branch.if",
8586
"branch.if.iteration",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export const simpleScopeTypeTypes = [
205205
// Private scope types
206206
"textFragment",
207207
"disqualifyDelimiter",
208+
"pairDelimiter",
208209
] as const;
209210

210211
export function isSimpleScopeType(

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ class HasMultipleChildrenOfType extends QueryPredicateOperator<HasMultipleChildr
6262
}
6363
}
6464

65+
/**
66+
* A predicate operator that returns true if the nodes text matched the regular expression
67+
*/
68+
class Match extends QueryPredicateOperator<Match> {
69+
name = "match?" as const;
70+
schema = z.tuple([q.node, q.string]);
71+
72+
run(nodeInfo: MutableQueryCapture, pattern: string) {
73+
const { document, range } = nodeInfo;
74+
const regex = new RegExp(pattern, "ds");
75+
const text = document.getText(range);
76+
return regex.test(text);
77+
}
78+
}
79+
6580
class ChildRange extends QueryPredicateOperator<ChildRange> {
6681
name = "child-range!" as const;
6782
schema = z.union([
@@ -277,4 +292,5 @@ export const queryPredicateOperators = [
277292
new InsertionDelimiter(),
278293
new SingleOrMultilineDelimiter(),
279294
new HasMultipleChildrenOfType(),
295+
new Match(),
280296
];

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/delimiterMaps.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ interface Options {
1414
* salient example is strings.
1515
*/
1616
isSingleLine?: boolean;
17-
18-
/**
19-
* The prefixes that can be used before the left side of the delimiter, eg "r".
20-
* Note that the empty string is always considered an acceptable prefix
21-
*/
22-
prefixes?: string[];
2317
}
2418

2519
type DelimiterMap = Record<
@@ -47,36 +41,6 @@ const delimiterToText: DelimiterMap = Object.freeze({
4741
squareBrackets: ["[", "]"],
4842
});
4943

50-
// https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
51-
const pythonPrefixes = [
52-
// string prefixes
53-
"r",
54-
"u",
55-
"R",
56-
"U",
57-
"f",
58-
"F",
59-
"fr",
60-
"Fr",
61-
"fR",
62-
"FR",
63-
"rf",
64-
"rF",
65-
"Rf",
66-
"RF",
67-
// byte prefixes
68-
"b",
69-
"B",
70-
"br",
71-
"Br",
72-
"bR",
73-
"BR",
74-
"rb",
75-
"rB",
76-
"Rb",
77-
"RB",
78-
];
79-
8044
// FIXME: Probably remove these as part of
8145
// https://github.com/cursorless-dev/cursorless/issues/1812#issuecomment-1691493746
8246
const delimiterToTextOverrides: Record<string, Partial<DelimiterMap>> = {
@@ -95,10 +59,8 @@ const delimiterToTextOverrides: Record<string, Partial<DelimiterMap>> = {
9559
},
9660

9761
python: {
98-
singleQuotes: ["'", "'", { isSingleLine: true, prefixes: pythonPrefixes }],
99-
doubleQuotes: ['"', '"', { isSingleLine: true, prefixes: pythonPrefixes }],
100-
tripleSingleQuotes: ["'''", "'''", { prefixes: pythonPrefixes }],
101-
tripleDoubleQuotes: ['"""', '"""', { prefixes: pythonPrefixes }],
62+
tripleSingleQuotes: ["'''", "'''"],
63+
tripleDoubleQuotes: ['"""', '"""'],
10264
},
10365

10466
ruby: {

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterOccurrences.ts

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ export function getDelimiterOccurrences(
3131
const disqualifyDelimiters = new OneWayRangeFinder(
3232
getSortedCaptures(languageDefinition, document, "disqualifyDelimiter"),
3333
);
34-
// We need a tree for text fragments since they can be nested
34+
const pairDelimiters = new OneWayRangeFinder(
35+
getSortedCaptures(languageDefinition, document, "pairDelimiter"),
36+
);
3537
const textFragments = new OneWayNestedRangeFinder(
3638
getSortedCaptures(languageDefinition, document, "textFragment"),
3739
);
3840

39-
const delimiterTextToDelimiterInfoMap =
40-
getDelimiterTextToDelimiterInfoMap(individualDelimiters);
41+
const delimiterTextToDelimiterInfoMap = Object.fromEntries(
42+
individualDelimiters.map((individualDelimiter) => [
43+
individualDelimiter.text,
44+
individualDelimiter,
45+
]),
46+
);
4147

4248
const regexMatches = matchAllIterator(
4349
document.getText(),
@@ -48,28 +54,37 @@ export function getDelimiterOccurrences(
4854

4955
for (const match of regexMatches) {
5056
const text = match[0];
51-
const range = new Range(
57+
const matchRange = new Range(
5258
document.positionAt(match.index!),
5359
document.positionAt(match.index! + text.length),
5460
);
5561

56-
const delimiter = disqualifyDelimiters.getContaining(range);
57-
const isDisqualified = delimiter != null && !delimiter.hasError();
62+
const disqualifiedDelimiter = ifNoErrors(
63+
disqualifyDelimiters.getContaining(matchRange),
64+
);
5865

59-
if (!isDisqualified) {
60-
const textFragmentRange =
61-
textFragments.getSmallestContaining(range)?.range;
62-
results.push({
63-
delimiterInfo: delimiterTextToDelimiterInfoMap[text],
64-
textFragmentRange,
65-
range,
66-
});
66+
if (disqualifiedDelimiter != null) {
67+
continue;
6768
}
69+
70+
results.push({
71+
delimiterInfo: delimiterTextToDelimiterInfoMap[text],
72+
textFragmentRange: ifNoErrors(
73+
textFragments.getSmallestContaining(matchRange),
74+
)?.range,
75+
range:
76+
ifNoErrors(pairDelimiters.getContaining(matchRange))?.range ??
77+
matchRange,
78+
});
6879
}
6980

7081
return results;
7182
}
7283

84+
function ifNoErrors(capture?: QueryCapture): QueryCapture | undefined {
85+
return capture != null && !capture.hasError() ? capture : undefined;
86+
}
87+
7388
function getSortedCaptures(
7489
languageDefinition: LanguageDefinition | undefined,
7590
document: TextDocument,
@@ -79,23 +94,3 @@ function getSortedCaptures(
7994
items.sort((a, b) => a.range.start.compareTo(b.range.start));
8095
return items;
8196
}
82-
83-
function getDelimiterTextToDelimiterInfoMap(
84-
individualDelimiters: IndividualDelimiter[],
85-
): Record<string, IndividualDelimiter> {
86-
return Object.fromEntries(
87-
individualDelimiters.flatMap((individualDelimiter) => {
88-
const results = [[individualDelimiter.text, individualDelimiter]];
89-
for (const prefix of individualDelimiter.prefixes) {
90-
const prefixText = prefix + individualDelimiter.text;
91-
const prefixDelimiter: IndividualDelimiter = {
92-
...individualDelimiter,
93-
text: prefixText,
94-
side: "left",
95-
};
96-
results.push([prefixText, prefixDelimiter]);
97-
}
98-
return results;
99-
}),
100-
);
101-
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/getDelimiterRegex.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,11 @@ export function getDelimiterRegex(individualDelimiters: IndividualDelimiter[]) {
1212
// Create a regex which is a disjunction of all possible left / right
1313
// delimiter texts
1414
const individualDelimiterDisjunct = uniq(
15-
individualDelimiters.flatMap((delimiter) => {
16-
const text = escapeRegExp(delimiter.text);
17-
const result = [text];
18-
for (const prefix of delimiter.prefixes) {
19-
// If the prefix is only alpha character, we need to make sure that there is no preceding alpha characters.
20-
if (alphaRegex.test(prefix)) {
21-
result.push(`(?<!\\w)${prefix}${text}`);
22-
} else {
23-
result.push(`${escapeRegExp(prefix)}${text}`);
24-
}
25-
}
26-
return result;
27-
}),
28-
).join("|");
15+
individualDelimiters.map(({ text }) => text),
16+
)
17+
.map(escapeRegExp)
18+
.join("|");
2919

3020
// Then make sure that we don't allow preceding `\`
3121
return new RegExp(`(?<!\\\\)(${individualDelimiterDisjunct})`, "gu");
3222
}
33-
34-
const alphaRegex = /^\w+$/;

0 commit comments

Comments
 (0)