From fc2896fca2db12ea43a08e697be0b70c4c635854 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 13 Dec 2025 15:12:12 +0100 Subject: [PATCH 01/17] Utilize interior in relative scope modifier --- .../relativeScopes/changeNextState.yml | 33 +++++++ .../modifiers/RelativeScopeStage.ts | 85 ++++++++++++++++--- 2 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 data/fixtures/recorded/relativeScopes/changeNextState.yml diff --git a/data/fixtures/recorded/relativeScopes/changeNextState.yml b/data/fixtures/recorded/relativeScopes/changeNextState.yml new file mode 100644 index 0000000000..3aefc7f3b9 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextState.yml @@ -0,0 +1,33 @@ +languageId: typescript +command: + version: 7 + spokenForm: change next state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + if (true) { + const a = 1; + } + const b = 2; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: | + if (true) { + const a = 1; + } + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 27641d1bfb..a8ab73690b 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,8 +1,11 @@ -import { - NoContainingScopeError, - type RelativeScopeModifier, +import type { + Position, + Range, + RelativeScopeModifier, + TextEditor, } from "@cursorless/common"; -import { islice, itake } from "itertools"; +import { NoContainingScopeError } from "@cursorless/common"; +import { find, ifilter, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; @@ -36,7 +39,12 @@ export class RelativeScopeStage implements ModifierStage { const scopes = Array.from( this.modifier.offset === 0 ? generateScopesInclusive(scopeHandler, target, this.modifier) - : generateScopesExclusive(scopeHandler, target, this.modifier), + : generateScopesExclusive( + this.scopeHandlerFactory, + scopeHandler, + target, + this.modifier, + ), ); if (scopes.length < this.modifier.length) { @@ -113,6 +121,7 @@ function generateScopesInclusive( * first scope if input range is empty and is at start of that scope. */ function generateScopesExclusive( + scopeHandlerFactory: ScopeHandlerFactory, scopeHandler: ScopeHandler, target: Target, modifier: RelativeScopeModifier, @@ -130,12 +139,68 @@ function generateScopesExclusive( ? "disallowed" : "disallowedIfStrict"; - return islice( - scopeHandler.generateScopes(editor, initialPosition, direction, { - containment, + let scopes = scopeHandler.generateScopes(editor, initialPosition, direction, { + containment, + skipAncestorScopes: true, + }); + + const interiorRanges = getInteriorRanges( + scopeHandlerFactory, + scopeHandler, + editor, + initialPosition, + ); + + if (interiorRanges != null) { + scopes = ifilter( + scopes, + (s) => !interiorRanges.some((r) => r.contains(s.domain)), + ); + } + + return islice(scopes, offset - 1, offset + desiredScopeCount - 1); +} + +function getInteriorRanges( + scopeHandlerFactory: ScopeHandlerFactory, + scopeHandler: ScopeHandler, + editor: TextEditor, + initialPosition: Position, +): Range[] | undefined { + const interiorScopeHandler = scopeHandlerFactory.maybeCreate( + { type: "interior" }, + editor.document.languageId, + ); + + if (interiorScopeHandler == null) { + return undefined; + } + + const containingScope = find( + scopeHandler.generateScopes(editor, initialPosition, "forward", { + containment: "required", + allowAdjacentScopes: true, skipAncestorScopes: true, }), - offset - 1, - offset + desiredScopeCount - 1, ); + + if (containingScope == null) { + return undefined; + } + + const interiorScopes = interiorScopeHandler.generateScopes( + editor, + containingScope.domain.start, + "forward", + { + skipAncestorScopes: true, + distalPosition: containingScope.domain.end, + }, + ); + + // Interiors containing the initial position are excluded + const interiorRanges = Array.from(interiorScopes) + .filter((s) => !s.domain.contains(initialPosition)) + .map((s) => s.domain); + return interiorRanges.length > 0 ? interiorRanges : undefined; } From 9603cdc406e85ab0866c60735c586fc3af14fbfe Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 13 Dec 2025 15:16:02 +0100 Subject: [PATCH 02/17] Added additional test --- .../relativeScopes/changeNextState2.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 data/fixtures/recorded/relativeScopes/changeNextState2.yml diff --git a/data/fixtures/recorded/relativeScopes/changeNextState2.yml b/data/fixtures/recorded/relativeScopes/changeNextState2.yml new file mode 100644 index 0000000000..dff245025a --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextState2.yml @@ -0,0 +1,34 @@ +languageId: typescript +command: + version: 7 + spokenForm: change next state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + if (true) { + + const a = 1; + } + selections: + - anchor: {line: 1, character: 3} + active: {line: 1, character: 3} + marks: {} +finalState: + documentContents: |- + if (true) { + + + } + selections: + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} From dc4fd2398b55ccd9b66ec889d031227a506710aa Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 14 Dec 2025 08:22:24 +0100 Subject: [PATCH 03/17] Add backwards tests --- .../relativeScopes/changePreviousState.yml | 34 ++++++++++++++++++ .../relativeScopes/changePreviousState2.yml | 36 +++++++++++++++++++ .../modifiers/RelativeScopeStage.ts | 20 ++++++++--- 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousState.yml create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousState2.yml diff --git a/data/fixtures/recorded/relativeScopes/changePreviousState.yml b/data/fixtures/recorded/relativeScopes/changePreviousState.yml new file mode 100644 index 0000000000..584484db3d --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousState.yml @@ -0,0 +1,34 @@ +languageId: typescript +command: + version: 7 + spokenForm: change previous state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + const b = 2; + if (true) { + const a = 1; + } + selections: + - anchor: {line: 3, character: 1} + active: {line: 3, character: 1} + marks: {} +finalState: + documentContents: |- + + if (true) { + const a = 1; + } + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/relativeScopes/changePreviousState2.yml b/data/fixtures/recorded/relativeScopes/changePreviousState2.yml new file mode 100644 index 0000000000..27547f31f4 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousState2.yml @@ -0,0 +1,36 @@ +languageId: typescript +command: + version: 7 + spokenForm: change previous state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + const b = 2; + if (true) { + const a = 1; + + } + selections: + - anchor: {line: 3, character: 3} + active: {line: 3, character: 3} + marks: {} +finalState: + documentContents: |- + const b = 2; + if (true) { + + + } + selections: + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index a8ab73690b..ab8c74c848 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,4 +1,5 @@ import type { + Direction, Position, Range, RelativeScopeModifier, @@ -149,6 +150,7 @@ function generateScopesExclusive( scopeHandler, editor, initialPosition, + direction, ); if (interiorRanges != null) { @@ -166,6 +168,7 @@ function getInteriorRanges( scopeHandler: ScopeHandler, editor: TextEditor, initialPosition: Position, + direction: Direction, ): Range[] | undefined { const interiorScopeHandler = scopeHandlerFactory.maybeCreate( { type: "interior" }, @@ -177,7 +180,7 @@ function getInteriorRanges( } const containingScope = find( - scopeHandler.generateScopes(editor, initialPosition, "forward", { + scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", allowAdjacentScopes: true, skipAncestorScopes: true, @@ -188,13 +191,22 @@ function getInteriorRanges( return undefined; } + 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, - containingScope.domain.start, - "forward", + containingInitialPosition, + direction, { skipAncestorScopes: true, - distalPosition: containingScope.domain.end, + distalPosition: containingDistalPosition, }, ); From 216deba54e2615d09a1206f3e8d521b1705ed7f4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 15 Dec 2025 13:19:37 +0100 Subject: [PATCH 04/17] Update names --- .../modifiers/RelativeScopeStage.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index ab8c74c848..9bea7c9997 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -6,7 +6,7 @@ import type { TextEditor, } from "@cursorless/common"; import { NoContainingScopeError } from "@cursorless/common"; -import { find, ifilter, islice, itake } from "itertools"; +import { find, ifilter, imap, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; @@ -145,7 +145,7 @@ function generateScopesExclusive( skipAncestorScopes: true, }); - const interiorRanges = getInteriorRanges( + const interiorRanges = getInteriorScopeRanges( scopeHandlerFactory, scopeHandler, editor, @@ -163,7 +163,12 @@ function generateScopesExclusive( return islice(scopes, offset - 1, offset + desiredScopeCount - 1); } -function getInteriorRanges( +/** + * Gets interior scope ranges within the containing scope at the given position. + * These are used to filter out scopes that are within interior scopes when + * applying relative scope modifiers. + */ +function getInteriorScopeRanges( scopeHandlerFactory: ScopeHandlerFactory, scopeHandler: ScopeHandler, editor: TextEditor, @@ -210,9 +215,13 @@ function getInteriorRanges( }, ); - // Interiors containing the initial position are excluded - const interiorRanges = Array.from(interiorScopes) - .filter((s) => !s.domain.contains(initialPosition)) - .map((s) => s.domain); + // 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. + const interiorRangesIt = imap( + ifilter(interiorScopes, (s) => !s.domain.contains(initialPosition)), + (s) => s.domain, + ); + const interiorRanges = Array.from(interiorRangesIt); return interiorRanges.length > 0 ? interiorRanges : undefined; } From 34694d7e970db1bd81b24ded09a86a5b83466a60 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 15 Dec 2025 13:52:47 +0100 Subject: [PATCH 05/17] More cleanup --- .../processTargets/modifiers/RelativeScopeStage.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 9bea7c9997..b22dd5e62e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -175,15 +175,6 @@ function getInteriorScopeRanges( initialPosition: Position, direction: Direction, ): Range[] | undefined { - const interiorScopeHandler = scopeHandlerFactory.maybeCreate( - { type: "interior" }, - editor.document.languageId, - ); - - if (interiorScopeHandler == null) { - return undefined; - } - const containingScope = find( scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", @@ -196,6 +187,11 @@ function getInteriorScopeRanges( return undefined; } + const interiorScopeHandler = scopeHandlerFactory.create( + { type: "interior" }, + editor.document.languageId, + ); + const containingInitialPosition = direction === "forward" ? containingScope.domain.start From bddd63bde95c49a49911b18c2cea63fd16d74f86 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:59:47 +0000 Subject: [PATCH 06/17] Improve comments; slight impl tweak --- .../modifiers/RelativeScopeStage.ts | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index b22dd5e62e..2b2ebcd665 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -6,7 +6,7 @@ import type { TextEditor, } from "@cursorless/common"; import { NoContainingScopeError } from "@cursorless/common"; -import { find, ifilter, imap, islice, itake } from "itertools"; +import { find, flatmap, ifilter, imap, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; @@ -145,7 +145,7 @@ function generateScopesExclusive( skipAncestorScopes: true, }); - const interiorRanges = getInteriorScopeRanges( + const interiorRanges = getExcludedInteriorRanges( scopeHandlerFactory, scopeHandler, editor, @@ -164,11 +164,29 @@ function generateScopesExclusive( } /** - * Gets interior scope ranges within the containing scope at the given position. - * These are used to filter out scopes that are within interior scopes when - * applying relative scope modifiers. + * Gets the interior scope range(s) within the containing scope of + * {@link initialPosition} that should be used to exclude next / previous + * scopes. + * + * The idea is that when you're in the headline of an if statement / function / + * etc, you're thinking at the same level as that scope, so the next scope + * should be outside of it. But when you're inside the body, so the next scope + * should be within it. + * + * For example, in the following code: + * + * ```typescript + * if (aaa) { + * bbb(); + * ccc(); + * } + * ddd(); + * ``` + * + * The target `"next state air"` should refer to `ddd();`, not `bbb();`. + * However, `"next state bat"` should refer to `ccc();`. */ -function getInteriorScopeRanges( +function getExcludedInteriorRanges( scopeHandlerFactory: ScopeHandlerFactory, scopeHandler: ScopeHandler, editor: TextEditor, @@ -214,10 +232,10 @@ function getInteriorScopeRanges( // 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. - const interiorRangesIt = imap( - ifilter(interiorScopes, (s) => !s.domain.contains(initialPosition)), - (s) => s.domain, + const relevantScopes = ifilter( + interiorScopes, + (s) => !s.domain.contains(initialPosition), ); - const interiorRanges = Array.from(interiorRangesIt); - return interiorRanges.length > 0 ? interiorRanges : undefined; + const targets = flatmap(relevantScopes, (s) => s.getTargets(false)); + return Array.from(imap(targets, (t) => t.contentRange)); } From 4ad475df75d100009b71f732b3f54881b82b34e9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:19:51 +0100 Subject: [PATCH 07/17] Also support surrounding pairs --- .../relativeScopes/changeNextPair.yml | 33 +++++++++++++ .../relativeScopes/changeNextPair2.yml | 34 ++++++++++++++ .../relativeScopes/changeNextState3.yml | 39 ++++++++++++++++ .../relativeScopes/changePreviousPair.yml | 34 ++++++++++++++ .../relativeScopes/changePreviousPair2.yml | 34 ++++++++++++++ .../relativeScopes/changePreviousState3.yml | 40 ++++++++++++++++ .../relativeScopes/clearNextCall2.yml | 6 +-- .../relativeScopes/clearNextCall4.yml | 6 +-- .../processTargets/modifiers/HeadTailStage.ts | 12 +++-- .../processTargets/modifiers/InteriorStage.ts | 46 +++++++++---------- .../modifiers/RelativeScopeStage.ts | 21 ++++----- 11 files changed, 259 insertions(+), 46 deletions(-) create mode 100644 data/fixtures/recorded/relativeScopes/changeNextPair.yml create mode 100644 data/fixtures/recorded/relativeScopes/changeNextPair2.yml create mode 100644 data/fixtures/recorded/relativeScopes/changeNextState3.yml create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousPair.yml create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousPair2.yml create mode 100644 data/fixtures/recorded/relativeScopes/changePreviousState3.yml diff --git a/data/fixtures/recorded/relativeScopes/changeNextPair.yml b/data/fixtures/recorded/relativeScopes/changeNextPair.yml new file mode 100644 index 0000000000..d7535b6056 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextPair.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + ( + () + ) + () + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: | + ( + () + ) + selections: + - anchor: {line: 3, character: 0} + active: {line: 3, character: 0} diff --git a/data/fixtures/recorded/relativeScopes/changeNextPair2.yml b/data/fixtures/recorded/relativeScopes/changeNextPair2.yml new file mode 100644 index 0000000000..b56d92c557 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextPair2.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change next pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + ( + + () + ) + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} + marks: {} +finalState: + documentContents: |- + ( + + + ) + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} diff --git a/data/fixtures/recorded/relativeScopes/changeNextState3.yml b/data/fixtures/recorded/relativeScopes/changeNextState3.yml new file mode 100644 index 0000000000..edbdac5985 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changeNextState3.yml @@ -0,0 +1,39 @@ +languageId: typescript +command: + version: 7 + spokenForm: change next state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: forward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + if (true) { + + } + else if (false) { + const a = 1; + } + const b = 2; + selections: + - anchor: {line: 1, character: 3} + active: {line: 1, character: 3} + marks: {} +finalState: + documentContents: | + if (true) { + + } + else if (false) { + const a = 1; + } + selections: + - anchor: {line: 6, character: 0} + active: {line: 6, character: 0} diff --git a/data/fixtures/recorded/relativeScopes/changePreviousPair.yml b/data/fixtures/recorded/relativeScopes/changePreviousPair.yml new file mode 100644 index 0000000000..d7c32bed48 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousPair.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: | + () + ( + () + ) + selections: + - anchor: {line: 3, character: 1} + active: {line: 3, character: 1} + marks: {} +finalState: + documentContents: | + + ( + () + ) + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/relativeScopes/changePreviousPair2.yml b/data/fixtures/recorded/relativeScopes/changePreviousPair2.yml new file mode 100644 index 0000000000..f58911dedd --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousPair2.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + version: 7 + spokenForm: change previous pair + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: surroundingPair, delimiter: any} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + ( + () + + ) + selections: + - anchor: {line: 2, character: 4} + active: {line: 2, character: 4} + marks: {} +finalState: + documentContents: |- + ( + + + ) + selections: + - anchor: {line: 1, character: 4} + active: {line: 1, character: 4} diff --git a/data/fixtures/recorded/relativeScopes/changePreviousState3.yml b/data/fixtures/recorded/relativeScopes/changePreviousState3.yml new file mode 100644 index 0000000000..811a47c7f9 --- /dev/null +++ b/data/fixtures/recorded/relativeScopes/changePreviousState3.yml @@ -0,0 +1,40 @@ +languageId: typescript +command: + version: 7 + spokenForm: change previous state + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: statement} + offset: 1 + length: 1 + direction: backward + usePrePhraseSnapshot: false +initialState: + documentContents: |- + const b = 2; + if (true) { + const a = 1; + } + else if (false) { + + } + selections: + - anchor: {line: 5, character: 3} + active: {line: 5, character: 3} + marks: {} +finalState: + documentContents: |- + + if (true) { + const a = 1; + } + else if (false) { + + } + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} diff --git a/data/fixtures/recorded/relativeScopes/clearNextCall2.yml b/data/fixtures/recorded/relativeScopes/clearNextCall2.yml index 01f5ca7706..6f96a7b4b9 100644 --- a/data/fixtures/recorded/relativeScopes/clearNextCall2.yml +++ b/data/fixtures/recorded/relativeScopes/clearNextCall2.yml @@ -20,7 +20,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/data/fixtures/recorded/relativeScopes/clearNextCall4.yml b/data/fixtures/recorded/relativeScopes/clearNextCall4.yml index 4752f3a689..bd00a806e8 100644 --- a/data/fixtures/recorded/relativeScopes/clearNextCall4.yml +++ b/data/fixtures/recorded/relativeScopes/clearNextCall4.yml @@ -20,7 +20,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-engine/src/processTargets/modifiers/HeadTailStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts index f5a09216cf..dbb77b7859 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts @@ -15,7 +15,7 @@ import { processModifierStages, } from "../TargetPipelineRunner"; import { HeadTailTarget, PlainTarget } from "../targets"; -import { createContainingInteriorStage } from "./InteriorStage"; +import { createCompoundInteriorScopeType } from "./InteriorStage"; class HeadTailStage implements ModifierStage { constructor( @@ -99,10 +99,12 @@ class BoundedLineStage implements ModifierStage { options: ModifierStateOptions, ): Target | undefined { try { - return createContainingInteriorStage(this.modifierStageFactory).run( - target, - options, - )[0]; + return this.modifierStageFactory + .create({ + type: "containingScope", + scopeType: createCompoundInteriorScopeType(), + }) + .run(target, options)[0]; } catch (error) { if (error instanceof NoContainingScopeError) { return undefined; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts index 6d6b400608..2146bbdd3a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts @@ -1,6 +1,7 @@ import { NoContainingScopeError, type InteriorOnlyModifier, + type ScopeType, } from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import type { ModifierStageFactory } from "../ModifierStageFactory"; @@ -11,7 +12,7 @@ import type { export class InteriorOnlyStage implements ModifierStage { constructor( - private modifierHandlerFactory: ModifierStageFactory, + private modifierStageFactory: ModifierStageFactory, private modifier: InteriorOnlyModifier, ) {} @@ -34,7 +35,7 @@ export class InteriorOnlyStage implements ModifierStage { // most cases, as long as the nearest interior is what we expect, which it // usually is. if (target.hasExplicitScopeType) { - const everyModifier = this.modifierHandlerFactory.create({ + const everyModifier = this.modifierStageFactory.create({ type: "everyScope", scopeType: { type: "interior", @@ -46,10 +47,12 @@ export class InteriorOnlyStage implements ModifierStage { // eg "inside air" try { - return createContainingInteriorStage(this.modifierHandlerFactory).run( - target, - options, - ); + return this.modifierStageFactory + .create({ + type: "containingScope", + scopeType: createCompoundInteriorScopeType(), + }) + .run(target, options); } catch (e) { if (e instanceof NoContainingScopeError) { throw new NoContainingScopeError("interior"); @@ -59,22 +62,17 @@ export class InteriorOnlyStage implements ModifierStage { } } -export function createContainingInteriorStage( - modifierHandlerFactory: ModifierStageFactory, -): ModifierStage { - return modifierHandlerFactory.create({ - type: "containingScope", - scopeType: { - type: "oneOf", - scopeTypes: [ - { - type: "interior", - }, - { - type: "surroundingPairInterior", - delimiter: "any", - }, - ], - }, - }); +export function createCompoundInteriorScopeType(): ScopeType { + return { + type: "oneOf", + scopeTypes: [ + { + type: "interior", + }, + { + type: "surroundingPairInterior", + delimiter: "any", + }, + ], + }; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 2b2ebcd665..ace69f0166 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -6,9 +6,10 @@ import type { TextEditor, } from "@cursorless/common"; import { NoContainingScopeError } from "@cursorless/common"; -import { find, flatmap, ifilter, imap, islice, itake } from "itertools"; +import { find, ifilter, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; +import { createCompoundInteriorScopeType } from "./InteriorStage"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { OutOfRangeError } from "./listUtils"; @@ -153,7 +154,7 @@ function generateScopesExclusive( direction, ); - if (interiorRanges != null) { + if (interiorRanges.length > 0) { scopes = ifilter( scopes, (s) => !interiorRanges.some((r) => r.contains(s.domain)), @@ -192,7 +193,7 @@ function getExcludedInteriorRanges( editor: TextEditor, initialPosition: Position, direction: Direction, -): Range[] | undefined { +): Range[] { const containingScope = find( scopeHandler.generateScopes(editor, initialPosition, direction, { containment: "required", @@ -202,11 +203,11 @@ function getExcludedInteriorRanges( ); if (containingScope == null) { - return undefined; + return []; } const interiorScopeHandler = scopeHandlerFactory.create( - { type: "interior" }, + createCompoundInteriorScopeType(), editor.document.languageId, ); @@ -232,10 +233,8 @@ function getExcludedInteriorRanges( // 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. - const relevantScopes = ifilter( - interiorScopes, - (s) => !s.domain.contains(initialPosition), - ); - const targets = flatmap(relevantScopes, (s) => s.getTargets(false)); - return Array.from(imap(targets, (t) => t.contentRange)); + return Array.from(interiorScopes) + .filter((s) => !s.domain.contains(initialPosition)) + .flatMap((s) => s.getTargets(false)) + .map((t) => t.contentRange); } From 43886206a30ef33268fb156c30c4d6c0182f42fb Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:28:34 +0100 Subject: [PATCH 08/17] Use more specific scope type --- .../src/processTargets/modifiers/RelativeScopeStage.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index ace69f0166..9fe3597b37 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -207,7 +207,12 @@ function getExcludedInteriorRanges( } const interiorScopeHandler = scopeHandlerFactory.create( - createCompoundInteriorScopeType(), + scopeHandler.scopeType?.type === "surroundingPair" + ? { + type: "surroundingPairInterior", + delimiter: scopeHandler.scopeType.delimiter, + } + : { type: "interior" }, editor.document.languageId, ); From affb04c7fdaff2db37866158abab45279822b385 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:32:53 +0100 Subject: [PATCH 09/17] make interior scope handler be able to return undefined --- .../scopeHandlers/ScopeHandlerFactoryImpl.ts | 4 +-- .../scopeHandlers/SortedScopeHandler.ts | 14 ++++++-- .../InteriorScopeHandler.ts | 35 ++++++++++--------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index e3b85c0a6a..a78e17bc4d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -119,10 +119,8 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { languageId, ); case "interior": - return new InteriorScopeHandler( - this, + return InteriorScopeHandler.maybeCreate( this.languageDefinitions, - scopeType, languageId, ); case "custom": diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts index b5dc7fe7f5..1941877233 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SortedScopeHandler.ts @@ -26,9 +26,17 @@ export class SortedScopeHandler extends BaseScopeHandler { scopeType: OneOfScopeType, languageId: string, ): ScopeHandler { - const scopeHandlers: ScopeHandler[] = scopeType.scopeTypes.map( - (scopeType) => scopeHandlerFactory.create(scopeType, languageId), - ); + const scopeHandlers = scopeType.scopeTypes + .map((scopeType) => + scopeHandlerFactory.maybeCreate(scopeType, languageId), + ) + .filter( + (scopeHandler): scopeHandler is ScopeHandler => scopeHandler != null, + ); + + if (scopeHandlers.length === 1) { + return scopeHandlers[0]; + } return this.createFromScopeHandlers( scopeHandlerFactory, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/InteriorScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/InteriorScopeHandler.ts index c0baf6efc7..7d0bba3a5e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/InteriorScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/SurroundingPairScopeHandler/InteriorScopeHandler.ts @@ -14,17 +14,28 @@ import type { ComplexScopeType, ScopeIteratorRequirements, } from "../scopeHandler.types"; -import type { ScopeHandlerFactory } from "../ScopeHandlerFactory"; +import type { TreeSitterScopeHandler } from "../TreeSitterScopeHandler"; export class InteriorScopeHandler extends BaseScopeHandler { + public readonly scopeType = { type: "interior" } as const; protected isHierarchical = true; - constructor( - private scopeHandlerFactory: ScopeHandlerFactory, - private languageDefinitions: LanguageDefinitions, - public readonly scopeType: ScopeType, - private languageId: string, - ) { + static maybeCreate( + languageDefinitions: LanguageDefinitions, + languageId: string, + ): InteriorScopeHandler | undefined { + const scopeHandler = languageDefinitions + .get(languageId) + ?.getScopeHandler({ type: "interior" }); + + if (scopeHandler == null) { + return undefined; + } + + return new InteriorScopeHandler(scopeHandler); + } + + private constructor(private scopeHandler: TreeSitterScopeHandler) { super(); } @@ -40,15 +51,7 @@ export class InteriorScopeHandler extends BaseScopeHandler { direction: Direction, hints: ScopeIteratorRequirements, ): Iterable { - const scopeHandler = this.languageDefinitions - .get(this.languageId) - ?.getScopeHandler(this.scopeType); - - if (scopeHandler == null) { - return; - } - - const scopes = scopeHandler.generateScopes( + const scopes = this.scopeHandler.generateScopes( editor, position, direction, From 10a8a5128072fa7f2aa754cabac4a864a8d37f8a Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:42:19 +0100 Subject: [PATCH 10/17] Update tests --- .../recorded/relativeScopes/clearSecondNextCall.yml | 8 ++++---- .../src/processTargets/modifiers/RelativeScopeStage.ts | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml b/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml index 9b64a18946..2a1a30c00c 100644 --- a/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml +++ b/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml @@ -14,13 +14,13 @@ command: direction: forward usePrePhraseSnapshot: true initialState: - documentContents: aaa(bbb(), ccc()) + ddd() + documentContents: aaa(bbb(), ccc()) + ddd() + eee() selections: - anchor: {line: 0, character: 10} active: {line: 0, character: 10} marks: {} finalState: - documentContents: "aaa(bbb(), ccc()) + " + documentContents: "aaa(bbb(), ccc()) + ddd() + " selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} + - anchor: {line: 0, character: 28} + active: {line: 0, character: 28} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 9fe3597b37..9bb075dd67 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -9,7 +9,6 @@ import { NoContainingScopeError } from "@cursorless/common"; import { find, ifilter, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; -import { createCompoundInteriorScopeType } from "./InteriorStage"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { OutOfRangeError } from "./listUtils"; @@ -206,7 +205,7 @@ function getExcludedInteriorRanges( return []; } - const interiorScopeHandler = scopeHandlerFactory.create( + const interiorScopeHandler = scopeHandlerFactory.maybeCreate( scopeHandler.scopeType?.type === "surroundingPair" ? { type: "surroundingPairInterior", @@ -216,6 +215,10 @@ function getExcludedInteriorRanges( editor.document.languageId, ); + if (interiorScopeHandler == null) { + return containingScope.getTargets(false).map((t) => t.contentRange); + } + const containingInitialPosition = direction === "forward" ? containingScope.domain.start From 50a4bbc0564bdf0ab26763dc7893124f0d266ff9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:45:42 +0100 Subject: [PATCH 11/17] Convert function to constant --- .../processTargets/modifiers/HeadTailStage.ts | 4 +-- .../processTargets/modifiers/InteriorStage.ts | 28 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts index dbb77b7859..8d41f1b9bf 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts @@ -15,7 +15,7 @@ import { processModifierStages, } from "../TargetPipelineRunner"; import { HeadTailTarget, PlainTarget } from "../targets"; -import { createCompoundInteriorScopeType } from "./InteriorStage"; +import { compoundInteriorScopeType } from "./InteriorStage"; class HeadTailStage implements ModifierStage { constructor( @@ -102,7 +102,7 @@ class BoundedLineStage implements ModifierStage { return this.modifierStageFactory .create({ type: "containingScope", - scopeType: createCompoundInteriorScopeType(), + scopeType: compoundInteriorScopeType, }) .run(target, options)[0]; } catch (error) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts index 2146bbdd3a..43a4a03920 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts @@ -50,7 +50,7 @@ export class InteriorOnlyStage implements ModifierStage { return this.modifierStageFactory .create({ type: "containingScope", - scopeType: createCompoundInteriorScopeType(), + scopeType: compoundInteriorScopeType, }) .run(target, options); } catch (e) { @@ -62,17 +62,15 @@ export class InteriorOnlyStage implements ModifierStage { } } -export function createCompoundInteriorScopeType(): ScopeType { - return { - type: "oneOf", - scopeTypes: [ - { - type: "interior", - }, - { - type: "surroundingPairInterior", - delimiter: "any", - }, - ], - }; -} +export const compoundInteriorScopeType: ScopeType = { + type: "oneOf", + scopeTypes: [ + { + type: "interior", + }, + { + type: "surroundingPairInterior", + delimiter: "any", + }, + ], +}; From e16e135c8e338ebac375525fa16c4433efaf7cc6 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 04:53:03 +0100 Subject: [PATCH 12/17] Update test --- .../recorded/relativeScopes/changePreviousState2.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/data/fixtures/recorded/relativeScopes/changePreviousState2.yml b/data/fixtures/recorded/relativeScopes/changePreviousState2.yml index 27547f31f4..a1ff4beb3b 100644 --- a/data/fixtures/recorded/relativeScopes/changePreviousState2.yml +++ b/data/fixtures/recorded/relativeScopes/changePreviousState2.yml @@ -15,22 +15,20 @@ command: usePrePhraseSnapshot: false initialState: documentContents: |- - const b = 2; if (true) { const a = 1; } selections: - - anchor: {line: 3, character: 3} - active: {line: 3, character: 3} + - anchor: {line: 2, character: 3} + active: {line: 2, character: 3} marks: {} finalState: documentContents: |- - const b = 2; if (true) { } selections: - - anchor: {line: 2, character: 3} - active: {line: 2, character: 3} + - anchor: {line: 1, character: 3} + active: {line: 1, character: 3} From 7a014237dcfac0b8181daeaf569fc3a2f3e37817 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 05:07:12 +0100 Subject: [PATCH 13/17] Update tests --- data/fixtures/recorded/relativeScopes/clearNextCall2.yml | 6 +++--- data/fixtures/recorded/relativeScopes/clearNextCall4.yml | 6 +++--- .../recorded/relativeScopes/clearSecondNextCall.yml | 8 ++++---- .../src/processTargets/modifiers/RelativeScopeStage.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/fixtures/recorded/relativeScopes/clearNextCall2.yml b/data/fixtures/recorded/relativeScopes/clearNextCall2.yml index 6f96a7b4b9..01f5ca7706 100644 --- a/data/fixtures/recorded/relativeScopes/clearNextCall2.yml +++ b/data/fixtures/recorded/relativeScopes/clearNextCall2.yml @@ -20,7 +20,7 @@ initialState: active: {line: 0, character: 0} marks: {} finalState: - documentContents: "aaa(bbb(), ccc()) + " + documentContents: aaa(, ccc()) + ddd() selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/relativeScopes/clearNextCall4.yml b/data/fixtures/recorded/relativeScopes/clearNextCall4.yml index bd00a806e8..4752f3a689 100644 --- a/data/fixtures/recorded/relativeScopes/clearNextCall4.yml +++ b/data/fixtures/recorded/relativeScopes/clearNextCall4.yml @@ -20,7 +20,7 @@ initialState: active: {line: 0, character: 1} marks: {} finalState: - documentContents: "aaa(bbb(), ccc()) + " + documentContents: aaa(, ccc()) + ddd() selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} diff --git a/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml b/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml index 2a1a30c00c..9b64a18946 100644 --- a/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml +++ b/data/fixtures/recorded/relativeScopes/clearSecondNextCall.yml @@ -14,13 +14,13 @@ command: direction: forward usePrePhraseSnapshot: true initialState: - documentContents: aaa(bbb(), ccc()) + ddd() + eee() + documentContents: aaa(bbb(), ccc()) + ddd() selections: - anchor: {line: 0, character: 10} active: {line: 0, character: 10} marks: {} finalState: - documentContents: "aaa(bbb(), ccc()) + ddd() + " + documentContents: "aaa(bbb(), ccc()) + " selections: - - anchor: {line: 0, character: 28} - active: {line: 0, character: 28} + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 9bb075dd67..43de97f5b1 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -170,7 +170,7 @@ function generateScopesExclusive( * * The idea is that when you're in the headline of an if statement / function / * etc, you're thinking at the same level as that scope, so the next scope - * should be outside of it. But when you're inside the body, so the next scope + * should be outside of it. But when you're inside the body, the next scope * should be within it. * * For example, in the following code: @@ -216,7 +216,7 @@ function getExcludedInteriorRanges( ); if (interiorScopeHandler == null) { - return containingScope.getTargets(false).map((t) => t.contentRange); + return []; } const containingInitialPosition = From 24a44d836a88867b48aecedb71b0ad58f3516e53 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 05:16:58 +0100 Subject: [PATCH 14/17] Reorder --- .../modifiers/RelativeScopeStage.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 43de97f5b1..5c78f8c7e5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -193,18 +193,6 @@ function getExcludedInteriorRanges( initialPosition: Position, direction: Direction, ): Range[] { - const containingScope = find( - scopeHandler.generateScopes(editor, initialPosition, direction, { - containment: "required", - allowAdjacentScopes: true, - skipAncestorScopes: true, - }), - ); - - if (containingScope == null) { - return []; - } - const interiorScopeHandler = scopeHandlerFactory.maybeCreate( scopeHandler.scopeType?.type === "surroundingPair" ? { @@ -219,6 +207,18 @@ function getExcludedInteriorRanges( return []; } + const containingScope = find( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment: "required", + allowAdjacentScopes: true, + skipAncestorScopes: true, + }), + ); + + if (containingScope == null) { + return []; + } + const containingInitialPosition = direction === "forward" ? containingScope.domain.start From 4db1d26799b0a89f39d35bcb646227d4f675b384 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 05:56:15 +0100 Subject: [PATCH 15/17] Refactor into separate code paths for surrounding pair and language pacific --- .../surroundingPair/changeNextRound4.yml | 6 +- .../processTargets/modifiers/InteriorStage.ts | 10 +- .../modifiers/RelativeScopeStage.ts | 98 +++++++++++++++---- 3 files changed, 91 insertions(+), 23 deletions(-) diff --git a/data/fixtures/recorded/surroundingPair/changeNextRound4.yml b/data/fixtures/recorded/surroundingPair/changeNextRound4.yml index 92e38103ad..2d52b2add6 100644 --- a/data/fixtures/recorded/surroundingPair/changeNextRound4.yml +++ b/data/fixtures/recorded/surroundingPair/changeNextRound4.yml @@ -20,7 +20,7 @@ initialState: active: {line: 0, character: 0} marks: {} finalState: - documentContents: () () + documentContents: "(()) " selections: - - anchor: {line: 0, character: 1} - active: {line: 0, character: 1} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts index 43a4a03920..796f2beab4 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts @@ -1,5 +1,6 @@ import { NoContainingScopeError, + UnsupportedScopeError, type InteriorOnlyModifier, type ScopeType, } from "@cursorless/common"; @@ -42,7 +43,14 @@ export class InteriorOnlyStage implements ModifierStage { }, }); - return everyModifier.run(target, options); + try { + return everyModifier.run(target, options); + } catch (e) { + if (e instanceof UnsupportedScopeError) { + throw new NoContainingScopeError("interior"); + } + throw e; + } } // eg "inside air" diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 5c78f8c7e5..88ba060e9a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -9,6 +9,7 @@ import { NoContainingScopeError } from "@cursorless/common"; import { find, ifilter, islice, itake } from "itertools"; import type { Target } from "../../typings/target.types"; import type { ModifierStage } from "../PipelineStages.types"; +import { InteriorTarget } from "../targets"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { OutOfRangeError } from "./listUtils"; @@ -193,13 +194,64 @@ function getExcludedInteriorRanges( initialPosition: Position, direction: Direction, ): Range[] { - const interiorScopeHandler = scopeHandlerFactory.maybeCreate( + const interiorTargets = scopeHandler.scopeType?.type === "surroundingPair" - ? { - type: "surroundingPairInterior", - delimiter: scopeHandler.scopeType.delimiter, - } - : { type: "interior" }, + ? 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( + scopeHandler, + editor, + initialPosition, + direction, + ); + + if (containingScope == null) { + return []; + } + + return containingScope + .getTargets(false) + .flatMap((t) => t.getInterior()) + .filter((t): t is Target => t != null); +} + +function getLanguageInteriorTargets( + scopeHandlerFactory: ScopeHandlerFactory, + scopeHandler: ScopeHandler, + editor: TextEditor, + initialPosition: Position, + direction: Direction, +): Target[] { + const interiorScopeHandler = scopeHandlerFactory.maybeCreate( + { type: "interior" }, editor.document.languageId, ); @@ -207,12 +259,11 @@ function getExcludedInteriorRanges( return []; } - const containingScope = find( - scopeHandler.generateScopes(editor, initialPosition, direction, { - containment: "required", - allowAdjacentScopes: true, - skipAncestorScopes: true, - }), + const containingScope = getContainingScope( + scopeHandler, + editor, + initialPosition, + direction, ); if (containingScope == null) { @@ -238,11 +289,20 @@ function getExcludedInteriorRanges( }, ); - // 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 Array.from(interiorScopes) - .filter((s) => !s.domain.contains(initialPosition)) - .flatMap((s) => s.getTargets(false)) - .map((t) => t.contentRange); + return Array.from(interiorScopes).flatMap((s) => s.getTargets(false)); +} + +function getContainingScope( + scopeHandler: ScopeHandler, + editor: TextEditor, + initialPosition: Position, + direction: Direction, +): TargetScope | undefined { + return find( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment: "required", + allowAdjacentScopes: true, + skipAncestorScopes: true, + }), + ); } From f2d1a0e7ee7ec694abc34428838cca15a076dd72 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 06:48:21 +0100 Subject: [PATCH 16/17] Cleanup --- .../src/processTargets/modifiers/RelativeScopeStage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 88ba060e9a..c02687ca9e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -239,8 +239,7 @@ function getSurroundingPairInteriorTargets( return containingScope .getTargets(false) - .flatMap((t) => t.getInterior()) - .filter((t): t is Target => t != null); + .flatMap((t) => t.getInterior() ?? []); } function getLanguageInteriorTargets( From d878bfa3aebcc463afc4217f7a527bbe84f12bc7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 17 Dec 2025 09:13:37 +0100 Subject: [PATCH 17/17] skip neovim test --- packages/cursorless-neovim-e2e/src/shouldRunTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cursorless-neovim-e2e/src/shouldRunTest.ts b/packages/cursorless-neovim-e2e/src/shouldRunTest.ts index b2d4262772..42f3417ed1 100644 --- a/packages/cursorless-neovim-e2e/src/shouldRunTest.ts +++ b/packages/cursorless-neovim-e2e/src/shouldRunTest.ts @@ -14,6 +14,8 @@ const failingFixtures = [ "recorded/implicitExpansion/cloneThat2", "recorded/implicitExpansion/cloneThis", "recorded/implicitExpansion/cloneThis2", + // Incorrect final state + "recorded/relativeScopes/changePreviousPair", ]; function isFailingFixture(name: string, fixture: TestCaseFixtureLegacy) {