diff --git a/packages/cursorless-engine/src/languages/LanguageDefinition.ts b/packages/cursorless-engine/src/languages/LanguageDefinition.ts index 0504719592..fd63ba8748 100644 --- a/packages/cursorless-engine/src/languages/LanguageDefinition.ts +++ b/packages/cursorless-engine/src/languages/LanguageDefinition.ts @@ -103,10 +103,7 @@ export class LanguageDefinition { for (const match of matches) { for (const capture of match.captures) { const name = capture.name as T; - if (result[name] == null) { - result[name] = []; - } - result[name]!.push(capture); + (result[name] ??= []).push(capture); } } diff --git a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts index 8d4a0412c6..a872ced16e 100644 --- a/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts +++ b/packages/cursorless-engine/src/languages/TreeSitterQuery/TreeSitterQuery.ts @@ -61,8 +61,8 @@ export class TreeSitterQuery { matches( document: TextDocument, - start?: Position, - end?: Position, + start: Position, + end: Position, ): QueryMatch[] { return this.getMatches(document, start, end, undefined); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts index e356957b2d..5bb3625dac 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemTextualScopeHandler.ts @@ -1,10 +1,10 @@ -import { - type Direction, - type Position, - Range, - type ScopeType, - type TextEditor, +import type { + Direction, + Position, + ScopeType, + TextEditor, } from "@cursorless/common"; +import { Range } from "@cursorless/common"; import { shrinkRangeToFitContent } from "../../../../util/selectionUtils"; import { BaseScopeHandler } from "../BaseScopeHandler"; import { compareTargetScopes } from "../compareTargetScopes"; @@ -48,7 +48,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { const cacheKey = "CollectionItemTextualScopeHandler_" + isEveryScope; if (!scopeHandlerCache.isValid(cacheKey, editor.document)) { - const scopes = this.getsScopes(editor, direction, isEveryScope); + const scopes = this.getScopes(editor, isEveryScope); scopeHandlerCache.update(cacheKey, editor.document, scopes); } @@ -59,29 +59,72 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { yield* scopes; } - private getsScopes( - editor: TextEditor, - direction: Direction, - isEveryScope: boolean, - ) { - const separatorRanges = getSeparatorOccurrences(editor.document); + private getScopes(editor: TextEditor, isEveryScope: boolean) { const interiorRanges = getInteriorRanges( this.scopeHandlerFactory, this.languageId, editor, "collectionBoundary", ); - const interiorRangeFinder = new OneWayNestedRangeFinder(interiorRanges); - const stringRanges = getInteriorRanges( - this.scopeHandlerFactory, - this.languageId, + const scopes: TargetScope[] = []; + + const usedInteriors = this.addSeparatorRanges( editor, - "string", + isEveryScope, + interiorRanges, + scopes, ); - const stringRangeFinder = new OneWayRangeFinder(stringRanges); - const scopes: TargetScope[] = []; + + // Add interior ranges without a delimiter in them. eg: `[foo]` + for (const interior of interiorRanges) { + if (!usedInteriors.has(interior.range) && !interior.range.isEmpty) { + const range = shrinkRangeToFitContent(editor, interior.range); + if (!range.isEmpty) { + scopes.push( + createTargetScope(isEveryScope, editor, interior.range, range), + ); + } + } + } + + return scopes; + } + + private addSeparatorRanges( + editor: TextEditor, + isEveryScope: boolean, + interiorRanges: { range: Range }[], + scopes: TargetScope[], + ): Set { + const separatorRanges = getSeparatorOccurrences(editor.document); const usedInteriors = new Set(); + + if (separatorRanges.length === 0) { + return usedInteriors; + } + const iterationStatesStack: IterationState[] = []; + const interiorRangeFinder = new OneWayNestedRangeFinder(interiorRanges); + const stringRangeFinder = new OneWayRangeFinder( + getInteriorRanges( + this.scopeHandlerFactory, + this.languageId, + editor, + "string", + ), + ); + let previousLine = -1; + let previousLineRange: Range | undefined; + + const getLineRange = (line: number) => { + // Separators are processed in document order, so line numbers are + // monotonically increasing. + if (line !== previousLine || previousLineRange == null) { + previousLine = line; + previousLineRange = editor.document.lineAt(line).range; + } + return previousLineRange; + }; for (const separator of separatorRanges) { // Separators in a string are not considered @@ -109,8 +152,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { // The containing iteration range is either the interior or the line containing the separator const containingIterationRange = - containingInteriorRange ?? - editor.document.lineAt(separator.start.line).range; + containingInteriorRange ?? getLineRange(separator.start.line); // The current containing iteration range is the same as the previous one. Just append delimiter. if ( @@ -142,19 +184,7 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { this.addScopes(scopes, state); } - // Add interior ranges without a delimiter in them. eg: `[foo]` - for (const interior of interiorRanges) { - if (!usedInteriors.has(interior.range) && !interior.range.isEmpty) { - const range = shrinkRangeToFitContent(editor, interior.range); - if (!range.isEmpty) { - scopes.push( - createTargetScope(isEveryScope, editor, interior.range, range), - ); - } - } - } - - return scopes; + return usedInteriors; } private addScopes(scopes: TargetScope[], state: IterationState) { @@ -164,41 +194,68 @@ export class CollectionItemTextualScopeHandler extends BaseScopeHandler { return; } - const itemRanges: Range[] = []; - - for (let i = 0; i < delimiters.length; ++i) { - const current = delimiters[i]; - - const previous = delimiters[i - 1]?.end ?? iterationRange.start; - itemRanges.push(new Range(previous, current.start)); - } - - const lastDelimiter = delimiters[delimiters.length - 1]; - itemRanges.push(new Range(lastDelimiter.end, iterationRange.end)); - - const trimmedRanges = itemRanges.map((range) => - shrinkRangeToFitContent(editor, range), + const firstDelimiter = delimiters[0]; + let previousRange: Range | undefined; + let currentRange = shrinkRangeToFitContent( + editor, + new Range(iterationRange.start, firstDelimiter.start), ); + let previousDelimiterEnd = firstDelimiter.end; + + for (let i = 1; i < delimiters.length; ++i) { + const nextRange = shrinkRangeToFitContent( + editor, + new Range(previousDelimiterEnd, delimiters[i].start), + ); - for (let i = 0; i < trimmedRanges.length; ++i) { - // Handle trailing delimiter - if ( - i === trimmedRanges.length - 1 && - editor.document.getText(trimmedRanges[i]).trim() === "" - ) { - continue; - } scopes.push( createTargetScope( isEveryScope, editor, iterationRange, - trimmedRanges[i], - trimmedRanges[i - 1], - trimmedRanges[i + 1], + currentRange, + previousRange, + nextRange, ), ); + + previousRange = currentRange; + currentRange = nextRange; + previousDelimiterEnd = delimiters[i].end; + } + + const trailingRange = shrinkRangeToFitContent( + editor, + new Range(previousDelimiterEnd, iterationRange.end), + ); + + // Emit the item before the final delimiter, using the trailing range as + // nextRange so delimiter metadata remains correct. + scopes.push( + createTargetScope( + isEveryScope, + editor, + iterationRange, + currentRange, + previousRange, + trailingRange, + ), + ); + + // Handle trailing delimiter. + if (editor.document.getText(trailingRange).trim() === "") { + return; } + + scopes.push( + createTargetScope( + isEveryScope, + editor, + iterationRange, + trailingRange, + currentRange, + ), + ); } }