diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 9dc97beee3..287c99689c 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -3,6 +3,8 @@ import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getContainingScopeTarget } from "./getContainingScopeTarget"; +import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { TargetScope } from "./scopeHandlers/scope.types"; @@ -32,16 +34,34 @@ export class RelativeExclusiveScopeStage implements ModifierStage { return runLegacy(this.modifierStageFactory, this.modifier, target); } - const { isReversed, editor, contentRange: inputRange } = target; + const { isReversed, editor, contentRange } = target; const { length: desiredScopeCount, direction, offset } = this.modifier; + const initialRange = (() => { + if (contentRange.isEmpty) { + return ( + getPreferredScopeTouchingPosition( + scopeHandler, + editor, + direction === "forward" ? contentRange.start : contentRange.end, + direction, + )?.domain ?? contentRange + ); + } + + return ( + getContainingScopeTarget(target, scopeHandler)?.[0].contentRange ?? + contentRange + ); + })(); + const initialPosition = - direction === "forward" ? inputRange.end : inputRange.start; + direction === "forward" ? initialRange.end : initialRange.start; // If inputRange is empty, then we skip past any scopes that start at // inputRange. Otherwise just disallow any scopes that start strictly // before the end of input range (strictly after for "backward"). - const containment: ContainmentPolicy | undefined = inputRange.isEmpty + const containment: ContainmentPolicy | undefined = initialRange.isEmpty ? "disallowed" : "disallowedIfStrict"; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeNextState.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeNextState.yml new file mode 100644 index 0000000000..1f7821b291 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeNextState.yml @@ -0,0 +1,35 @@ +languageId: typescript +command: + version: 6 + spokenForm: change next state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: true +initialState: + documentContents: |- + if (true) { + console.log(1) + } + + console.log(2) + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: {} +finalState: + documentContents: |+ + if (true) { + console.log(1) + } + + selections: + - anchor: {line: 4, character: 0} + active: {line: 4, character: 0} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall2.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall2.yml index 266114dd4d..b42b2fee35 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall2.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall2.yml @@ -19,7 +19,7 @@ initialState: active: {line: 0, character: 0} marks: {} finalState: - documentContents: aaa(, ccc()) + ddd() + documentContents: "aaa(bbb(), ccc()) + " selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall4.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall4.yml index 37790fa0af..1f5565a3df 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall4.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearNextCall4.yml @@ -19,7 +19,7 @@ initialState: active: {line: 0, character: 1} marks: {} finalState: - documentContents: aaa(, ccc()) + ddd() + documentContents: "aaa(bbb(), ccc()) + " selections: - - anchor: {line: 0, character: 4} - active: {line: 0, character: 4} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearSecondNextCall.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearSecondNextCall.yml index 2ae6b8da7e..f7f6924e38 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearSecondNextCall.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/clearSecondNextCall.yml @@ -1,14 +1,14 @@ languageId: ruby command: version: 5 - spokenForm: change second next call + spokenForm: change next call action: {name: clearAndSetSelection} targets: - type: primitive modifiers: - type: relativeScope scopeType: {type: functionCall} - offset: 2 + offset: 1 length: 1 direction: forward usePrePhraseSnapshot: true