diff --git a/data/fixtures/recorded/relativeScopes/changeNextCall.yml b/data/fixtures/recorded/relativeScopes/changeNextCall.yml new file mode 100644 index 0000000000..0962bb3223 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextCall.yml @@ -0,0 +1,26 @@ +languageId: typescript +command: + version: 7 + spokenForm: change next call + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: functionCall} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: aaa(bbb()); ccc(); + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: aaa(bbb()); ; + selections: + - anchor: {line: 0, character: 12} + active: {line: 0, character: 12} diff --git a/data/fixtures/recorded/relativeScopes/changePreviousCall.yml b/data/fixtures/recorded/relativeScopes/changePreviousCall.yml new file mode 100644 index 0000000000..4cd991d4c5 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousCall.yml @@ -0,0 +1,26 @@ +languageId: typescript +command: + version: 7 + spokenForm: change previous call + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: functionCall} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: aaa(); bbb(ccc()); + selections: + - anchor: {line: 0, character: 17} + active: {line: 0, character: 17} + marks: {} +finalState: + documentContents: ; bbb(ccc()); + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index c02687ca9e..27f09074e3 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -165,7 +165,7 @@ function generateScopesExclusive( } /** - * Gets the interior scope range(s) within the containing scope of + * Gets the scope range(s) within the containing scope of * {@link initialPosition} that should be used to exclude next / previous * scopes. * @@ -194,114 +194,101 @@ function getExcludedInteriorRanges( initialPosition: Position, direction: Direction, ): Range[] { - const interiorTargets = - scopeHandler.scopeType?.type === "surroundingPair" - ? getSurroundingPairInteriorTargets( - scopeHandler, - editor, - initialPosition, - direction, - ) - : getLanguageInteriorTargets( - scopeHandlerFactory, - scopeHandler, - editor, - initialPosition, - direction, - ); - - // Interiors containing the initial position are excluded. This happens when - // you are in the body of an if statement and use `next state` and in that - // case we don't want to exclude scopes within the same interior. - return interiorTargets - .map((t) => - t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, - ) - .filter((r) => !r.contains(initialPosition)); -} - -function getSurroundingPairInteriorTargets( - scopeHandler: ScopeHandler, - editor: TextEditor, - initialPosition: Position, - direction: Direction, -): Target[] { - const containingScope = getContainingScope( + const containingScopeTarget = getContainingScopeTarget( scopeHandler, editor, initialPosition, direction, ); - if (containingScope == null) { + // No containing scope, nothing to exclude. + if (containingScopeTarget == null) { return []; } - return containingScope - .getTargets(false) - .flatMap((t) => t.getInterior() ?? []); -} + const containingInteriorTargets = containingScopeTarget.getInterior(); -function getLanguageInteriorTargets( - scopeHandlerFactory: ScopeHandlerFactory, - scopeHandler: ScopeHandler, - editor: TextEditor, - initialPosition: Position, - direction: Direction, -): Target[] { + // Containing target already has an interior. eg a surrounding pair scope. + if (containingInteriorTargets != null) { + return getFilteredInteriorRanges( + containingInteriorTargets, + initialPosition, + ); + } + + // Fallback to language specific interior scope handler const interiorScopeHandler = scopeHandlerFactory.maybeCreate( { type: "interior" }, editor.document.languageId, ); + // No interior scope handler, nothing to exclude. + // For languages that hasn't defined the interior scope handler yet we default + // to NOT excluding anything. if (interiorScopeHandler == null) { return []; } - const containingScope = getContainingScope( - scopeHandler, - editor, - initialPosition, + const containingPositions = getPositions( + containingScopeTarget.contentRange, direction, ); - if (containingScope == null) { - return []; - } - - const containingInitialPosition = - direction === "forward" - ? containingScope.domain.start - : containingScope.domain.end; - const containingDistalPosition = - direction === "forward" - ? containingScope.domain.end - : containingScope.domain.start; - const interiorScopes = interiorScopeHandler.generateScopes( editor, - containingInitialPosition, + containingPositions.initial, direction, { skipAncestorScopes: true, - distalPosition: containingDistalPosition, + distalPosition: containingPositions.distal, }, ); - return Array.from(interiorScopes).flatMap((s) => s.getTargets(false)); + const interiorTargets = Array.from(interiorScopes).flatMap((s) => + s.getTargets(false), + ); + + if (interiorTargets.length > 0) { + return getFilteredInteriorRanges(interiorTargets, initialPosition); + } + + // This containing scope has no interior. + // Default to excluding the entire containing scope. + return [containingScopeTarget.contentRange]; +} + +function getPositions(range: Range, direction: Direction) { + return direction === "forward" + ? { initial: range.start, distal: range.end } + : { initial: range.end, distal: range.start }; +} + +function getFilteredInteriorRanges( + interiorTargets: Target[], + initialPosition: Position, +) { + // Interiors containing the initial position are excluded. This happens when + // you are in the body of an if statement and use `next state` and in that + // case we don't want to exclude scopes within the same interior. + return interiorTargets + .map((t) => + t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, + ) + .filter((r) => !r.contains(initialPosition)); } -function getContainingScope( +function getContainingScopeTarget( scopeHandler: ScopeHandler, editor: TextEditor, initialPosition: Position, direction: Direction, -): TargetScope | undefined { - return find( +): Target | undefined { + const containingScope = find( scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", allowAdjacentScopes: true, skipAncestorScopes: true, }), ); + return containingScope?.getTargets(false)[0]; }