diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts index b1f0af59101..8903f4b5251 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts @@ -57,6 +57,6 @@ export class ContainingScopeStage implements ModifierStage { throw new NoContainingScopeError(this.modifier.scopeType.type); } - return [containingScope]; + return containingScope; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index c0fa052f937..33424fa031a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -73,14 +73,12 @@ export class EveryScopeStage implements ModifierStage { if (scopes == null) { // If target had no explicit range, or was contained by a single target // instance, expand to iteration scope before overlapping - scopes = getScopesOverlappingRange( + scopes = this.getDefaultIterationRange( scopeHandler, - editor, - this.getDefaultIterationRange( - scopeHandler, - this.scopeHandlerFactory, - target, - ), + this.scopeHandlerFactory, + target, + ).flatMap((iterationRange) => + getScopesOverlappingRange(scopeHandler, editor, iterationRange), ); } @@ -88,14 +86,14 @@ export class EveryScopeStage implements ModifierStage { throw new NoContainingScopeError(scopeType.type); } - return scopes.map((scope) => scope.getTarget(isReversed)); + return scopes.flatMap((scope) => scope.getTargets(isReversed)); } getDefaultIterationRange( scopeHandler: ScopeHandler, scopeHandlerFactory: ScopeHandlerFactory, target: Target, - ): Range { + ): Range[] { const iterationScopeHandler = scopeHandlerFactory.create( scopeHandler.iterationScopeType, target.editor.document.languageId, @@ -116,7 +114,7 @@ export class EveryScopeStage implements ModifierStage { ); } - return iterationScopeTarget.contentRange; + return iterationScopeTarget.map((target) => target.contentRange); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index fca69fcee86..ab2c7573bff 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -64,7 +64,7 @@ export default class RelativeExclusiveScopeStage implements ModifierStage { // When we hit offset, that becomes proximal scope if (desiredScopeCount === 1) { // Just yield it if we only want 1 scope - return [scope.getTarget(isReversed)]; + return scope.getTargets(isReversed); } proximalScope = scope; @@ -73,7 +73,7 @@ export default class RelativeExclusiveScopeStage implements ModifierStage { if (scopeCount === offset + desiredScopeCount - 1) { // Then make a range when we get the desired number of scopes - return [constructScopeRangeTarget(isReversed, proximalScope!, scope)]; + return constructScopeRangeTarget(isReversed, proximalScope!, scope); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index 196d328632e..73528db44c7 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -74,12 +74,10 @@ export class RelativeInclusiveScopeStage implements ModifierStage { throw new OutOfRangeError(); } - return [ - constructScopeRangeTarget( - isReversed, - scopes[0], - scopes[scopes.length - 1], - ), - ]; + return constructScopeRangeTarget( + isReversed, + scopes[0], + scopes[scopes.length - 1], + ); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts index 3928e0df37f..170b8c157d5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -17,13 +17,20 @@ export function constructScopeRangeTarget( isReversed: boolean, scope1: TargetScope, scope2: TargetScope, -): Target { +): Target[] { if (scope1 === scope2) { - return scope1.getTarget(isReversed); + return scope1.getTargets(isReversed); } - const target1 = scope1.getTarget(isReversed); - const target2 = scope2.getTarget(isReversed); + const targets1 = scope1.getTargets(isReversed); + const targets2 = scope2.getTargets(isReversed); + + if (targets1.length !== 1 || targets2.length !== 1) { + throw Error("Scope range targets must be single-target"); + } + + const [target1] = targets1; + const [target2] = targets2; const isScope2After = target2.contentRange.start.isAfterOrEqual( target1.contentRange.start, @@ -33,10 +40,7 @@ export function constructScopeRangeTarget( ? [target1, target2] : [target2, target1]; - return startTarget.createContinuousRangeTarget( - isReversed, - endTarget, - true, - true, - ); + return [ + startTarget.createContinuousRangeTarget(isReversed, endTarget, true, true), + ]; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts index e86ced150fe..24aa3f6c6ad 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/getContainingScopeTarget.ts @@ -17,7 +17,7 @@ export function getContainingScopeTarget( target: Target, scopeHandler: ScopeHandler, ancestorIndex: number = 0, -): Target | undefined { +): Target[] | undefined { const { isReversed, editor, @@ -46,7 +46,7 @@ export function getContainingScopeTarget( return undefined; } - return scope.getTarget(isReversed); + return scope.getTargets(isReversed); } const startScope = expandFromPosition( @@ -62,7 +62,7 @@ export function getContainingScopeTarget( } if (startScope.domain.contains(end)) { - return startScope.getTarget(isReversed); + return startScope.getTargets(isReversed); } const endScope = expandFromPosition( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts index 19ec9a93156..28d9fb46369 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/BaseScopeHandler.test.ts @@ -128,7 +128,7 @@ suite("BaseScopeHandler", () => { const inputScopes = testCase.scopes.map((scope) => ({ editor, domain: toRange(scope.start, scope.end), - getTarget: () => undefined as any, + getTargets: () => undefined as any, })); assert.deepStrictEqual( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts index c5686050002..c6deb9a3e5b 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CharacterScopeHandler.ts @@ -33,13 +33,14 @@ export default class CharacterScopeHandler extends NestedScopeHandler { (range) => ({ editor, domain: range, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new PlainTarget({ editor, contentRange: range, isReversed, isToken: false, }), + ], }), ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/DocumentScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/DocumentScopeHandler.ts index e97c515d805..6df7da3907e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/DocumentScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/DocumentScopeHandler.ts @@ -23,12 +23,13 @@ export default class DocumentScopeHandler extends BaseScopeHandler { yield { editor, domain: contentRange, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new DocumentTarget({ editor, isReversed, contentRange, }), + ], }; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts index d1841595024..feebab75514 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/IdentifierScopeHandler.ts @@ -21,12 +21,13 @@ export default class IdentifierScopeHandler extends NestedScopeHandler { (range) => ({ editor, domain: range, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new TokenTarget({ editor, contentRange: range, isReversed, }), + ], }), ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts index e1a01a6fdd8..23241713971 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/LineScopeHandler.ts @@ -40,7 +40,7 @@ function lineNumberToScope( return { editor, domain: range, - getTarget: (isReversed) => createLineTarget(editor, isReversed, range), + getTargets: (isReversed) => [createLineTarget(editor, isReversed, range)], }; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts index 1c9419f3292..cfb62dcc48f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ParagraphScopeHandler.ts @@ -85,11 +85,12 @@ function createScope(editor: TextEditor, domain: Range): TargetScope { return { editor, domain, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new ParagraphTarget({ editor, isReversed, contentRange: fitRangeToLineContent(editor, domain), }), + ], }; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts index aa95a4dc5cb..eb7f8d54116 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TokenScopeHandler.ts @@ -22,12 +22,13 @@ export default class TokenScopeHandler extends NestedScopeHandler { (range) => ({ editor, domain: range, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new TokenTarget({ editor, contentRange: range, isReversed, }), + ], }), ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts index 1f80f850f17..921fcf813a6 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/BaseTreeSitterScopeHandler.ts @@ -13,6 +13,7 @@ import { ContainmentPolicy, ScopeIteratorRequirements, } from "../scopeHandler.types"; +import { mergeAdjacentBy } from "./mergeAdjacentBy"; /** Base scope handler to use for both tree-sitter scopes and their iteration scopes */ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler { @@ -36,11 +37,26 @@ export abstract class BaseTreeSitterScopeHandler extends BaseScopeHandler { hints, ); - yield* this.query + const scopes = this.query .matches(document, start, end) .map((match) => this.matchToScope(editor, match)) .filter((scope): scope is TargetScope => scope != null) .sort((a, b) => compareTargetScopes(direction, position, a, b)); + + // Merge scopes that have the same domain into a single scope with multiple + // targets + yield* mergeAdjacentBy( + scopes, + (a, b) => a.domain.isRangeEqual(b.domain), + (a, b) => { + return { + ...a, + getTargets(isReversed: boolean) { + return [...a.getTargets(isReversed), ...b.getTargets(isReversed)]; + }, + }; + }, + ); } /** diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts index 286269f688e..973b4d2ecab 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterIterationScopeHandler.ts @@ -50,12 +50,13 @@ export class TreeSitterIterationScopeHandler extends BaseTreeSitterScopeHandler return { editor, domain, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new PlainTarget({ editor, isReversed, contentRange, }), + ], }; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts index f1ff04c2c8e..52852d09828 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterScopeHandler.ts @@ -65,7 +65,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { return { editor, domain, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new ScopeTypeTarget({ scopeTypeType, editor, @@ -77,6 +77,7 @@ export class TreeSitterScopeHandler extends BaseTreeSitterScopeHandler { interiorRange, // FIXME: Add delimiter text }), + ], }; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler.ts index 23849c4d4d3..7d09fc1aec7 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler.ts @@ -42,12 +42,13 @@ export class TreeSitterTextFragmentScopeHandler extends BaseTreeSitterScopeHandl return { editor, domain: contentRange, - getTarget: (isReversed) => + getTargets: (isReversed) => [ new PlainTarget({ editor, isReversed, contentRange, }), + ], }; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/mergeAdjacentBy.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/mergeAdjacentBy.ts new file mode 100644 index 00000000000..73c91e8309b --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/mergeAdjacentBy.ts @@ -0,0 +1,28 @@ +/** + * Merges adjacent elements of a list using a predicate and a merge function. + * Adjacent elements are merged if the predicate returns true for them. + * @param input The input list to merge adjacent elements of + * @param isEqual A function that returns true if two elements should be merged + * @param merge A function that merges two elements + * @returns A new list with adjacent elements merged + */ +export function mergeAdjacentBy( + input: T[], + isEqual: (a: T, b: T) => boolean, + merge: (a: T, b: T) => T, +): T[] { + const output: T[] = []; + for (const item of input) { + if (output.length === 0) { + output.push(item); + } else { + const last = output[output.length - 1]; + if (isEqual(last, item)) { + output[output.length - 1] = merge(last, item); + } else { + output.push(item); + } + } + } + return output; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts index 90edb49c468..975cf4bec2d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/WordScopeHandler.ts @@ -33,18 +33,20 @@ export default class WordScopeHandler extends NestedScopeHandler { return contentRanges.map((range, i) => ({ editor, domain: range, - getTarget: (isReversed) => { + getTargets: (isReversed) => { const previousContentRange = i > 0 ? contentRanges[i - 1] : null; const nextContentRange = i + 1 < contentRanges.length ? contentRanges[i + 1] : null; - return constructTarget( - isReversed, - editor, - previousContentRange, - range, - nextContentRange, - ); + return [ + constructTarget( + isReversed, + editor, + previousContentRange, + range, + nextContentRange, + ), + ]; }, })); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts index d253547d8a5..73ce68be3d8 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/scope.types.ts @@ -4,7 +4,7 @@ import type { Target } from "../../../typings/target.types"; /** * Represents a scope, which is a specific instantiation of a scope type, * eg a specific function, or a specific line or range of lines. Contains - * {@link getTarget}, which represents the actual scope, as well as + * {@link getTargets}, which represents the actual scope, as well as * {@link domain}, which represents the range within which the given scope is * canonical. For example, a scope representing the type of a parameter will * have the entire parameter as its domain, so that one can say "take type" @@ -33,7 +33,9 @@ export interface TargetScope { readonly domain: Range; /** - * The target corresponding to this scope. + * The targets corresponding to this scope. Note that there will almost + * always be exactly one target, but there are some exceptions, eg "tags" in + * HTML / jsx */ - getTarget(isReversed: boolean): Target; + getTargets(isReversed: boolean): Target[]; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts index 4e6fb6f5a09..69573df0b6e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/surroundingPair/index.ts @@ -89,7 +89,7 @@ function processSurroundingPairCore( return findSurroundingPairTextBased( editor, range, - containingScope.contentRange, + containingScope[0].contentRange, delimiters, scopeType, );