Skip to content

Commit 40a6fee

Browse files
Migrate collection item and argument scopes (#2081)
Note that we are here just migrating markdown and yaml collection. For languages like typescript I propose that the insertion delimiter is either `, ` for an inline collection or `,\n` for an vertical collection. When the edit is constructed by our destination `\n` gets replaced by `{indent}\n` or `\n{indent}` depending if it's before or after. In short I don't think we actually should use leading or trailing for the insertion delimiters and the destination can take care of that on its own. To get the correct insertion delimiter we could have a conditional insertion delimiter predicate that actually uses the collection and not the item itself to determine if it's a vertical collection/insertion delimiter. `insertionDelimiter = @list.range.isSingleLine ? ", " : ",\n"` ``` ( (array (_) @collectionItem ) @list (#conditional-insertion-delimiter! @collectionItem @list ", " ",\n") ) ``` Edit: Just went ahead and migrated typescript argument using the above predicate. Personally I think it turned out quite nicely. Will fix the second task #585 ## 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 811447f commit 40a6fee

38 files changed

+696
-131
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,37 @@ class InsertionDelimiter extends QueryPredicateOperator<InsertionDelimiter> {
214214
}
215215
}
216216

217+
/**
218+
* A predicate operator that sets the insertion delimiter of {@link nodeInfo} to
219+
* either {@link insertionDelimiterConsequence} or
220+
* {@link insertionDelimiterAlternative} depending on whether
221+
* {@link conditionNodeInfo} is single or multiline, respectively. For example,
222+
*
223+
* ```scm
224+
* (#single-or-multi-line-delimiter! @foo @bar ", " ",\n")
225+
* ```
226+
*
227+
* will set the insertion delimiter of the `@foo` capture to `", "` if the
228+
* `@bar` capture is a single line and `",\n"` otherwise.
229+
*/
230+
class SingleOrMultilineDelimiter extends QueryPredicateOperator<SingleOrMultilineDelimiter> {
231+
name = "single-or-multi-line-delimiter!" as const;
232+
schema = z.tuple([q.node, q.node, q.string, q.string]);
233+
234+
run(
235+
nodeInfo: MutableQueryCapture,
236+
conditionNodeInfo: MutableQueryCapture,
237+
insertionDelimiterConsequence: string,
238+
insertionDelimiterAlternative: string,
239+
) {
240+
nodeInfo.insertionDelimiter = conditionNodeInfo.range.isSingleLine
241+
? insertionDelimiterConsequence
242+
: insertionDelimiterAlternative;
243+
244+
return true;
245+
}
246+
}
247+
217248
export const queryPredicateOperators = [
218249
new Log(),
219250
new NotType(),
@@ -224,5 +255,6 @@ export const queryPredicateOperators = [
224255
new ShrinkToMatch(),
225256
new AllowMultiple(),
226257
new InsertionDelimiter(),
258+
new SingleOrMultilineDelimiter(),
227259
new HasMultipleChildrenOfType(),
228260
];

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { patternMatchers as ruby } from "./ruby";
2222
import rust from "./rust";
2323
import scala from "./scala";
2424
import { patternMatchers as scss } from "./scss";
25-
import { patternMatchers as typescript } from "./typescript";
2625

2726
export function getNodeMatcher(
2827
languageId: string,
@@ -59,8 +58,6 @@ export const languageMatchers: Record<
5958
clojure,
6059
go,
6160
java,
62-
javascript: typescript,
63-
javascriptreact: typescript,
6461
latex,
6562
markdown,
6663
php,
@@ -69,8 +66,6 @@ export const languageMatchers: Record<
6966
scala,
7067
scss,
7168
rust,
72-
typescript,
73-
typescriptreact: typescript,
7469
};
7570

7671
function matcherIncludeSiblings(matcher: NodeMatcher): NodeMatcher {

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

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
import { Range, SimpleScopeTypeType, TextEditor } from "@cursorless/common";
1+
import { SimpleScopeTypeType, TextEditor } from "@cursorless/common";
22
import type { SyntaxNode } from "web-tree-sitter";
3-
import {
4-
NodeFinder,
5-
NodeMatcherAlternative,
6-
SelectionWithContext,
7-
} from "../typings/Types";
8-
import { getMatchesInRange } from "../util/getMatchesInRange";
3+
import { NodeFinder, NodeMatcherAlternative } from "../typings/Types";
94
import { leadingSiblingNodeFinder, patternFinder } from "../util/nodeFinders";
105
import { createPatternMatchers, matcher } from "../util/nodeMatchers";
11-
import {
12-
extendUntilNextMatchingSiblingOrLast,
13-
selectWithLeadingDelimiter,
14-
} from "../util/nodeSelectors";
6+
import { extendUntilNextMatchingSiblingOrLast } from "../util/nodeSelectors";
157
import { shrinkRangeToFitContent } from "../util/selectionUtils";
168

179
const HEADING_MARKER_TYPES = [
@@ -69,48 +61,9 @@ function sectionMatcher(...patterns: string[]) {
6961
return matcher(leadingSiblingNodeFinder(finder), sectionExtractor);
7062
}
7163

72-
const itemLeadingDelimiterExtractor = selectWithLeadingDelimiter(
73-
"list_marker_parenthesis",
74-
"list_marker_dot",
75-
"list_marker_star",
76-
"list_marker_minus",
77-
"list_marker_plus",
78-
);
79-
80-
function excludeTrailingNewline(editor: TextEditor, range: Range) {
81-
const matches = getMatchesInRange(/\r?\n\s*$/g, editor, range);
82-
83-
if (matches.length > 0) {
84-
return new Range(range.start, matches[0].start);
85-
}
86-
87-
return range;
88-
}
89-
90-
function itemExtractor(
91-
editor: TextEditor,
92-
node: SyntaxNode,
93-
): SelectionWithContext {
94-
const { selection } = itemLeadingDelimiterExtractor(editor, node);
95-
const line = editor.document.lineAt(selection.start);
96-
const leadingRange = new Range(line.range.start, selection.start);
97-
const indent = editor.document.getText(leadingRange);
98-
99-
return {
100-
context: {
101-
containingListDelimiter: `\n${indent}`,
102-
leadingDelimiterRange: leadingRange,
103-
},
104-
selection: excludeTrailingNewline(editor, selection).toSelection(
105-
selection.isReversed,
106-
),
107-
};
108-
}
109-
11064
const nodeMatchers: Partial<
11165
Record<SimpleScopeTypeType, NodeMatcherAlternative>
11266
> = {
113-
collectionItem: matcher(patternFinder("list_item.paragraph!"), itemExtractor),
11467
section: sectionMatcher("atx_heading"),
11568
sectionLevelOne: sectionMatcher("atx_heading.atx_h1_marker"),
11669
sectionLevelTwo: sectionMatcher("atx_heading.atx_h2_marker"),

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

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,20 @@ export class EveryScopeStage implements ModifierStage {
7373
if (scopes == null) {
7474
// If target had no explicit range, or was contained by a single target
7575
// instance, expand to iteration scope before overlapping
76-
scopes = this.getDefaultIterationRange(
77-
scopeHandler,
78-
this.scopeHandlerFactory,
79-
target,
80-
).flatMap((iterationRange) =>
81-
getScopesOverlappingRange(scopeHandler, editor, iterationRange),
82-
);
76+
try {
77+
scopes = this.getDefaultIterationRange(
78+
scopeHandler,
79+
this.scopeHandlerFactory,
80+
target,
81+
).flatMap((iterationRange) =>
82+
getScopesOverlappingRange(scopeHandler, editor, iterationRange),
83+
);
84+
} catch (error) {
85+
if (!(error instanceof NoContainingScopeError)) {
86+
throw error;
87+
}
88+
scopes = [];
89+
}
8390
}
8491

8592
if (scopes.length === 0) {

packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from "@cursorless/common";
99
import { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
1010
import { Target } from "../../../typings/target.types";
11-
import { getInsertionDelimiter } from "../../../util/nodeSelectors";
1211
import { getRangeLength } from "../../../util/rangeUtils";
1312
import { ModifierStage } from "../../PipelineStages.types";
1413
import { ScopeTypeTarget } from "../../targets";
@@ -109,25 +108,34 @@ export class ItemStage implements ModifierStage {
109108
itemInfo: ItemInfo,
110109
removalRange?: Range,
111110
) {
112-
const delimiter = getInsertionDelimiter(
113-
target.editor,
111+
const insertionDelimiter = getInsertionDelimiter(
114112
itemInfo.leadingDelimiterRange,
115113
itemInfo.trailingDelimiterRange,
116-
", ",
117114
);
118115
return new ScopeTypeTarget({
119116
scopeTypeType: this.modifier.scopeType.type as SimpleScopeTypeType,
120117
editor: target.editor,
121118
isReversed: target.isReversed,
122119
contentRange: itemInfo.contentRange,
123-
delimiter,
120+
insertionDelimiter,
124121
leadingDelimiterRange: itemInfo.leadingDelimiterRange,
125122
trailingDelimiterRange: itemInfo.trailingDelimiterRange,
126123
removalRange,
127124
});
128125
}
129126
}
130127

128+
function getInsertionDelimiter(
129+
leadingDelimiterRange?: Range,
130+
trailingDelimiterRange?: Range,
131+
): string {
132+
return (leadingDelimiterRange != null &&
133+
!leadingDelimiterRange.isSingleLine) ||
134+
(trailingDelimiterRange != null && !trailingDelimiterRange.isSingleLine)
135+
? ",\n"
136+
: ", ";
137+
}
138+
131139
/** Filter item infos by content range and domain intersection */
132140
function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] {
133141
return itemInfos.filter(

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
7070
true,
7171
);
7272

73+
const rawPrefixRange = getRelatedRange(
74+
match,
75+
scopeTypeType,
76+
"prefix",
77+
true,
78+
);
79+
const prefixRange =
80+
rawPrefixRange != null
81+
? new Range(rawPrefixRange.start, contentRange.start)
82+
: undefined;
83+
7384
return {
7485
editor,
7586
domain,
@@ -80,11 +91,12 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler {
8091
editor,
8192
isReversed,
8293
contentRange,
94+
prefixRange,
8395
removalRange,
8496
leadingDelimiterRange,
8597
trailingDelimiterRange,
8698
interiorRange,
87-
delimiter: insertionDelimiter,
99+
insertionDelimiter,
88100
}),
89101
],
90102
};

packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/LegacyContainingSyntaxScopeStage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class LegacyContainingSyntaxScopeStage implements ModifierStage {
8282
contentRange: contentSelection,
8383
removalRange: removalRange,
8484
interiorRange: interiorRange,
85-
delimiter: containingListDelimiter,
85+
insertionDelimiter: containingListDelimiter,
8686
leadingDelimiterRange,
8787
trailingDelimiterRange,
8888
});

packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import {
1010
EditNewActionType,
1111
Target,
1212
} from "../../typings/target.types";
13+
import { union } from "../../util/rangeUtils";
1314

1415
export class DestinationImpl implements Destination {
1516
public readonly contentRange: Range;
1617
private readonly isLineDelimiter: boolean;
1718
private readonly isBefore: boolean;
1819
private readonly indentationString: string;
20+
private readonly insertionPrefix?: string;
1921

2022
constructor(
2123
public readonly target: Target,
@@ -24,12 +26,15 @@ export class DestinationImpl implements Destination {
2426
) {
2527
this.contentRange = getContentRange(target.contentRange, insertionMode);
2628
this.isBefore = insertionMode === "before";
27-
// It's only considered a line if the delimiter is only new line symbols
28-
this.isLineDelimiter = /^(\n)+$/.test(target.insertionDelimiter);
29+
this.isLineDelimiter = target.insertionDelimiter.includes("\n");
2930
this.indentationString =
3031
indentationString ?? this.isLineDelimiter
3132
? getIndentationString(target.editor, target.contentRange)
3233
: "";
34+
this.insertionPrefix =
35+
target.prefixRange != null
36+
? target.editor.document.getText(target.prefixRange)
37+
: undefined;
3338
}
3439

3540
get contentSelection(): Selection {
@@ -65,9 +70,10 @@ export class DestinationImpl implements Destination {
6570

6671
getEditNewActionType(): EditNewActionType {
6772
if (
68-
this.insertionDelimiter === "\n" &&
6973
this.insertionMode === "after" &&
70-
this.target.contentRange.isSingleLine
74+
this.target.contentRange.isSingleLine &&
75+
this.insertionDelimiter === "\n" &&
76+
this.insertionPrefix == null
7177
) {
7278
// If the target that we're wrapping is not a single line, then we
7379
// want to compute indentation based on the entire target. Otherwise,
@@ -110,43 +116,46 @@ export class DestinationImpl implements Destination {
110116

111117
private getEditRange() {
112118
const position = (() => {
113-
const contentPosition = this.isBefore
114-
? this.contentRange.start
115-
: this.contentRange.end;
119+
const insertionPosition = this.isBefore
120+
? union(this.target.contentRange, this.target.prefixRange).start
121+
: this.target.contentRange.end;
116122

117123
if (this.isLineDelimiter) {
118-
const line = this.editor.document.lineAt(contentPosition);
124+
const line = this.editor.document.lineAt(insertionPosition);
119125
const nonWhitespaceCharacterIndex = this.isBefore
120126
? line.firstNonWhitespaceCharacterIndex
121127
: line.lastNonWhitespaceCharacterIndex;
122128

123-
// Use the full line to include indentation
124-
if (contentPosition.character === nonWhitespaceCharacterIndex) {
129+
// Use the full line with included indentation and trailing whitespaces
130+
if (insertionPosition.character === nonWhitespaceCharacterIndex) {
125131
return this.isBefore ? line.range.start : line.range.end;
126132
}
127133
}
128134

129-
return contentPosition;
135+
return insertionPosition;
130136
})();
131137

132138
return new Range(position, position);
133139
}
134140

135141
private getEditText(text: string) {
136-
const insertionText = this.indentationString + text;
142+
const insertionText =
143+
this.indentationString + (this.insertionPrefix ?? "") + text;
137144

138145
return this.isBefore
139146
? insertionText + this.insertionDelimiter
140147
: this.insertionDelimiter + insertionText;
141148
}
142149

143150
private updateRange(range: Range, text: string) {
144-
const baseStartOffset = this.editor.document.offsetAt(range.start);
151+
const baseStartOffset =
152+
this.editor.document.offsetAt(range.start) +
153+
this.indentationString.length +
154+
(this.insertionPrefix?.length ?? 0);
155+
145156
const startIndex = this.isBefore
146-
? baseStartOffset + this.indentationString.length
147-
: baseStartOffset +
148-
this.getLengthOfInsertionDelimiter() +
149-
this.indentationString.length;
157+
? baseStartOffset
158+
: baseStartOffset + this.getLengthOfInsertionDelimiter();
150159

151160
const endIndex = startIndex + text.length;
152161

@@ -160,11 +169,10 @@ export class DestinationImpl implements Destination {
160169
// Went inserting a new line with eol `CRLF` each `\n` will be converted to
161170
// `\r\n` and therefore the length is doubled.
162171
if (this.editor.document.eol === "CRLF") {
163-
// This function is only called when inserting after a range. Therefore we
164-
// only care about leading new lines in the insertion delimiter.
165-
const match = this.insertionDelimiter.match(/^\n+/);
166-
const nlCount = match?.[0].length ?? 0;
167-
return this.insertionDelimiter.length + nlCount;
172+
const matches = this.insertionDelimiter.match(/\n/g);
173+
if (matches != null) {
174+
return this.insertionDelimiter.length + matches.length;
175+
}
168176
}
169177
return this.insertionDelimiter.length;
170178
}

0 commit comments

Comments
 (0)