Skip to content

Commit 2320d46

Browse files
Optimize performance for collection item (#3188)
1 parent 386bf8b commit 2320d46

3 files changed

Lines changed: 120 additions & 66 deletions

File tree

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,7 @@ export class LanguageDefinition {
103103
for (const match of matches) {
104104
for (const capture of match.captures) {
105105
const name = capture.name as T;
106-
if (result[name] == null) {
107-
result[name] = [];
108-
}
109-
result[name]!.push(capture);
106+
(result[name] ??= []).push(capture);
110107
}
111108
}
112109

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ export class TreeSitterQuery {
6161

6262
matches(
6363
document: TextDocument,
64-
start?: Position,
65-
end?: Position,
64+
start: Position,
65+
end: Position,
6666
): QueryMatch[] {
6767
return this.getMatches(document, start, end, undefined);
6868
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {
2-
type Direction,
3-
type Position,
4-
Range,
5-
type ScopeType,
6-
type TextEditor,
1+
import type {
2+
Direction,
3+
Position,
4+
ScopeType,
5+
TextEditor,
76
} from "@cursorless/common";
7+
import { Range } from "@cursorless/common";
88
import { shrinkRangeToFitContent } from "../../../../util/selectionUtils";
99
import { BaseScopeHandler } from "../BaseScopeHandler";
1010
import { compareTargetScopes } from "../compareTargetScopes";
@@ -48,7 +48,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
4848
const cacheKey = "CollectionItemTextualScopeHandler_" + isEveryScope;
4949

5050
if (!scopeHandlerCache.isValid(cacheKey, editor.document)) {
51-
const scopes = this.getsScopes(editor, direction, isEveryScope);
51+
const scopes = this.getScopes(editor, isEveryScope);
5252
scopeHandlerCache.update(cacheKey, editor.document, scopes);
5353
}
5454

@@ -59,29 +59,72 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
5959
yield* scopes;
6060
}
6161

62-
private getsScopes(
63-
editor: TextEditor,
64-
direction: Direction,
65-
isEveryScope: boolean,
66-
) {
67-
const separatorRanges = getSeparatorOccurrences(editor.document);
62+
private getScopes(editor: TextEditor, isEveryScope: boolean) {
6863
const interiorRanges = getInteriorRanges(
6964
this.scopeHandlerFactory,
7065
this.languageId,
7166
editor,
7267
"collectionBoundary",
7368
);
74-
const interiorRangeFinder = new OneWayNestedRangeFinder(interiorRanges);
75-
const stringRanges = getInteriorRanges(
76-
this.scopeHandlerFactory,
77-
this.languageId,
69+
const scopes: TargetScope[] = [];
70+
71+
const usedInteriors = this.addSeparatorRanges(
7872
editor,
79-
"string",
73+
isEveryScope,
74+
interiorRanges,
75+
scopes,
8076
);
81-
const stringRangeFinder = new OneWayRangeFinder(stringRanges);
82-
const scopes: TargetScope[] = [];
77+
78+
// Add interior ranges without a delimiter in them. eg: `[foo]`
79+
for (const interior of interiorRanges) {
80+
if (!usedInteriors.has(interior.range) && !interior.range.isEmpty) {
81+
const range = shrinkRangeToFitContent(editor, interior.range);
82+
if (!range.isEmpty) {
83+
scopes.push(
84+
createTargetScope(isEveryScope, editor, interior.range, range),
85+
);
86+
}
87+
}
88+
}
89+
90+
return scopes;
91+
}
92+
93+
private addSeparatorRanges(
94+
editor: TextEditor,
95+
isEveryScope: boolean,
96+
interiorRanges: { range: Range }[],
97+
scopes: TargetScope[],
98+
): Set<Range> {
99+
const separatorRanges = getSeparatorOccurrences(editor.document);
83100
const usedInteriors = new Set<Range>();
101+
102+
if (separatorRanges.length === 0) {
103+
return usedInteriors;
104+
}
105+
84106
const iterationStatesStack: IterationState[] = [];
107+
const interiorRangeFinder = new OneWayNestedRangeFinder(interiorRanges);
108+
const stringRangeFinder = new OneWayRangeFinder(
109+
getInteriorRanges(
110+
this.scopeHandlerFactory,
111+
this.languageId,
112+
editor,
113+
"string",
114+
),
115+
);
116+
let previousLine = -1;
117+
let previousLineRange: Range | undefined;
118+
119+
const getLineRange = (line: number) => {
120+
// Separators are processed in document order, so line numbers are
121+
// monotonically increasing.
122+
if (line !== previousLine || previousLineRange == null) {
123+
previousLine = line;
124+
previousLineRange = editor.document.lineAt(line).range;
125+
}
126+
return previousLineRange;
127+
};
85128

86129
for (const separator of separatorRanges) {
87130
// Separators in a string are not considered
@@ -109,8 +152,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
109152

110153
// The containing iteration range is either the interior or the line containing the separator
111154
const containingIterationRange =
112-
containingInteriorRange ??
113-
editor.document.lineAt(separator.start.line).range;
155+
containingInteriorRange ?? getLineRange(separator.start.line);
114156

115157
// The current containing iteration range is the same as the previous one. Just append delimiter.
116158
if (
@@ -142,19 +184,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
142184
this.addScopes(scopes, state);
143185
}
144186

145-
// Add interior ranges without a delimiter in them. eg: `[foo]`
146-
for (const interior of interiorRanges) {
147-
if (!usedInteriors.has(interior.range) && !interior.range.isEmpty) {
148-
const range = shrinkRangeToFitContent(editor, interior.range);
149-
if (!range.isEmpty) {
150-
scopes.push(
151-
createTargetScope(isEveryScope, editor, interior.range, range),
152-
);
153-
}
154-
}
155-
}
156-
157-
return scopes;
187+
return usedInteriors;
158188
}
159189

160190
private addScopes(scopes: TargetScope[], state: IterationState) {
@@ -164,41 +194,68 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler {
164194
return;
165195
}
166196

167-
const itemRanges: Range[] = [];
168-
169-
for (let i = 0; i < delimiters.length; ++i) {
170-
const current = delimiters[i];
171-
172-
const previous = delimiters[i - 1]?.end ?? iterationRange.start;
173-
itemRanges.push(new Range(previous, current.start));
174-
}
175-
176-
const lastDelimiter = delimiters[delimiters.length - 1];
177-
itemRanges.push(new Range(lastDelimiter.end, iterationRange.end));
178-
179-
const trimmedRanges = itemRanges.map((range) =>
180-
shrinkRangeToFitContent(editor, range),
197+
const firstDelimiter = delimiters[0];
198+
let previousRange: Range | undefined;
199+
let currentRange = shrinkRangeToFitContent(
200+
editor,
201+
new Range(iterationRange.start, firstDelimiter.start),
181202
);
203+
let previousDelimiterEnd = firstDelimiter.end;
204+
205+
for (let i = 1; i < delimiters.length; ++i) {
206+
const nextRange = shrinkRangeToFitContent(
207+
editor,
208+
new Range(previousDelimiterEnd, delimiters[i].start),
209+
);
182210

183-
for (let i = 0; i < trimmedRanges.length; ++i) {
184-
// Handle trailing delimiter
185-
if (
186-
i === trimmedRanges.length - 1 &&
187-
editor.document.getText(trimmedRanges[i]).trim() === ""
188-
) {
189-
continue;
190-
}
191211
scopes.push(
192212
createTargetScope(
193213
isEveryScope,
194214
editor,
195215
iterationRange,
196-
trimmedRanges[i],
197-
trimmedRanges[i - 1],
198-
trimmedRanges[i + 1],
216+
currentRange,
217+
previousRange,
218+
nextRange,
199219
),
200220
);
221+
222+
previousRange = currentRange;
223+
currentRange = nextRange;
224+
previousDelimiterEnd = delimiters[i].end;
225+
}
226+
227+
const trailingRange = shrinkRangeToFitContent(
228+
editor,
229+
new Range(previousDelimiterEnd, iterationRange.end),
230+
);
231+
232+
// Emit the item before the final delimiter, using the trailing range as
233+
// nextRange so delimiter metadata remains correct.
234+
scopes.push(
235+
createTargetScope(
236+
isEveryScope,
237+
editor,
238+
iterationRange,
239+
currentRange,
240+
previousRange,
241+
trailingRange,
242+
),
243+
);
244+
245+
// Handle trailing delimiter.
246+
if (editor.document.getText(trailingRange).trim() === "") {
247+
return;
201248
}
249+
250+
scopes.push(
251+
createTargetScope(
252+
isEveryScope,
253+
editor,
254+
iterationRange,
255+
trailingRange,
256+
currentRange,
257+
),
258+
);
202259
}
203260
}
204261

0 commit comments

Comments
 (0)