From bb7c8996327f14d1ed1d6cc98f7e635cfd0c5cab Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 18 Dec 2025 10:16:19 +0100 Subject: [PATCH 1/4] Doing more work on relative scopes --- .../relativeScopes/changeNextCall.yml | 26 +++++++ .../relativeScopes/changePreviousCall.yml | 26 +++++++ .../modifiers/RelativeScopeStage.ts | 68 +++++++++++-------- 3 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 data/fixtures/recorded/relativeScopes/changeNextCall.yml create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousCall.yml 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..1b381b1081 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,30 +194,20 @@ 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)); + return scopeHandler.scopeType?.type === "surroundingPair" + ? getSurroundingPairInteriorTargets( + scopeHandler, + editor, + initialPosition, + direction, + ) + : getLanguageInteriorTargets( + scopeHandlerFactory, + scopeHandler, + editor, + initialPosition, + direction, + ); } function getSurroundingPairInteriorTargets( @@ -225,7 +215,7 @@ function getSurroundingPairInteriorTargets( editor: TextEditor, initialPosition: Position, direction: Direction, -): Target[] { +): Range[] { const containingScope = getContainingScope( scopeHandler, editor, @@ -239,7 +229,11 @@ function getSurroundingPairInteriorTargets( return containingScope .getTargets(false) - .flatMap((t) => t.getInterior() ?? []); + .flatMap((t) => t.getInterior() ?? []) + .map((t) => + t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, + ) + .filter((r) => !r.contains(initialPosition)); } function getLanguageInteriorTargets( @@ -248,7 +242,7 @@ function getLanguageInteriorTargets( editor: TextEditor, initialPosition: Position, direction: Direction, -): Target[] { +): Range[] { const interiorScopeHandler = scopeHandlerFactory.maybeCreate( { type: "interior" }, editor.document.languageId, @@ -288,7 +282,23 @@ function getLanguageInteriorTargets( }, ); - return Array.from(interiorScopes).flatMap((s) => s.getTargets(false)); + const interiorTargets = Array.from(interiorScopes).flatMap((s) => + s.getTargets(false), + ); + + // 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. + if (interiorTargets.length > 0) { + return interiorTargets + .map((t) => + t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, + ) + .filter((r) => !r.contains(initialPosition)); + } + + // This containing scope has no interior. Exclude the entire containing scope. + return containingScope.getTargets(false).map((t) => t.contentRange); } function getContainingScope( From b0978fd53da59d0b55a5bcd3cd68aef8b4e5e4bd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 18 Dec 2025 10:55:24 +0100 Subject: [PATCH 2/4] Refactoring and cleanup --- .../modifiers/RelativeScopeStage.ts | 118 +++++++----------- 1 file changed, 47 insertions(+), 71 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 1b381b1081..60f26eb344 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -194,91 +194,53 @@ function getExcludedInteriorRanges( initialPosition: Position, direction: Direction, ): Range[] { - return scopeHandler.scopeType?.type === "surroundingPair" - ? getSurroundingPairInteriorTargets( - scopeHandler, - editor, - initialPosition, - direction, - ) - : getLanguageInteriorTargets( - scopeHandlerFactory, - scopeHandler, - editor, - initialPosition, - direction, - ); -} - -function getSurroundingPairInteriorTargets( - scopeHandler: ScopeHandler, - editor: TextEditor, - initialPosition: Position, - direction: Direction, -): Range[] { - 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() ?? []) - .map((t) => - t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, - ) - .filter((r) => !r.contains(initialPosition)); -} + const containingInteriorTargets = containingScopeTarget.getInterior(); -function getLanguageInteriorTargets( - scopeHandlerFactory: ScopeHandlerFactory, - scopeHandler: ScopeHandler, - editor: TextEditor, - initialPosition: Position, - direction: Direction, -): Range[] { + // Containing targets 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, }, ); @@ -286,32 +248,46 @@ function getLanguageInteriorTargets( s.getTargets(false), ); - // 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. if (interiorTargets.length > 0) { - return interiorTargets - .map((t) => - t instanceof InteriorTarget ? t.fullInteriorRange : t.contentRange, - ) - .filter((r) => !r.contains(initialPosition)); + return getFilteredInteriorRanges(interiorTargets, initialPosition); } - // This containing scope has no interior. Exclude the entire containing scope. - return containingScope.getTargets(false).map((t) => t.contentRange); + // 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 getContainingScope( +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 getContainingScopeTarget( scopeHandler: ScopeHandler, editor: TextEditor, initialPosition: Position, direction: Direction, -): TargetScope | undefined { +): Target | undefined { return find( scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", allowAdjacentScopes: true, skipAncestorScopes: true, }), - ); + )?.getTargets(false)[0]; } From a51537d1aa229cfb6836467a86296e5d61198ce9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 18 Dec 2025 10:56:52 +0100 Subject: [PATCH 3/4] Fix typo --- .../src/processTargets/modifiers/RelativeScopeStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 60f26eb344..dfc5958a2d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -208,7 +208,7 @@ function getExcludedInteriorRanges( const containingInteriorTargets = containingScopeTarget.getInterior(); - // Containing targets already has an interior. eg a surrounding pair scope. + // Containing target already has an interior. eg a surrounding pair scope. if (containingInteriorTargets != null) { return getFilteredInteriorRanges( containingInteriorTargets, From 31185dd31922699e803fce8470e9f9c3da383606 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 18 Dec 2025 10:59:59 +0100 Subject: [PATCH 4/4] Cleanup --- .../src/processTargets/modifiers/RelativeScopeStage.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index dfc5958a2d..27f09074e3 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -283,11 +283,12 @@ function getContainingScopeTarget( initialPosition: Position, direction: Direction, ): Target | undefined { - return find( + const containingScope = find( scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", allowAdjacentScopes: true, skipAncestorScopes: true, }), - )?.getTargets(false)[0]; + ); + return containingScope?.getTargets(false)[0]; }