From aa66ae48ff7b92017c8528e5699a89f45c0228f9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 07:54:12 +0100 Subject: [PATCH 01/17] Added spread support for relative and ordinal scopes --- .../src/modifiers/ordinal_scope.py | 20 +++++++-- .../src/modifiers/relative_scope.py | 12 +++-- cursorless-talon/src/spoken_forms.json | 3 ++ .../command/PartialTargetDescriptor.types.ts | 6 +++ .../primitiveTargetToSpokenForm.ts | 30 +++++++++---- .../modifiers/OrdinalScopeStage.ts | 5 +++ .../modifiers/RelativeExclusiveScopeStage.ts | 44 +++++++++++++++---- .../modifiers/RelativeInclusiveScopeStage.ts | 9 +++- .../modifiers/constructScopeRangeTarget.ts | 23 ++++++++-- .../modifiers/targetSequenceUtils.ts | 25 +++++++++-- .../src/spokenForms/SpokenFormType.ts | 3 +- .../spokenForms/defaultSpokenFormMapCore.ts | 1 + .../changeSpreadFirstTwoTokens.yml | 28 ++++++++++++ .../changeSpreadLastTwoTokens.yml | 28 ++++++++++++ .../changeSpreadNextTwoTokens.yml | 29 ++++++++++++ .../relativeScopes/changeSpreadTwoTokens.yml | 29 ++++++++++++ .../changeSpreadTwoTokensBackward.yml | 29 ++++++++++++ 17 files changed, 292 insertions(+), 32 deletions(-) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index 57e321ed87..25b138d428 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -44,18 +44,24 @@ def cursorless_ordinal_range(m) -> dict[str, Any]: @mod.capture( - rule="({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + rule="[{user.cursorless_spread_scope_modifier}] ({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " ) def cursorless_first_last(m) -> dict[str, Any]: """First/last `n` scopes; eg "first three funks""" - if m[0] == "first": + spread = hasattr(m, "cursorless_spread_scope_modifier") + print(m) + if hasattr(m, "cursorless_first_modifier"): return create_ordinal_scope_modifier( - m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small + m.cursorless_scope_type_plural, + 0, + m.private_cursorless_number_small, + spread, ) return create_ordinal_scope_modifier( m.cursorless_scope_type_plural, -m.private_cursorless_number_small, m.private_cursorless_number_small, + spread, ) @@ -65,10 +71,16 @@ def cursorless_ordinal_scope(m) -> dict[str, Any]: return m[0] -def create_ordinal_scope_modifier(scope_type: dict, start: int, length: int = 1): +def create_ordinal_scope_modifier( + scope_type: dict, + start: int, + length: int = 1, + spread: bool = False, +): return { "type": "ordinalScope", "scopeType": scope_type, "start": start, "length": length, + "spread": spread, } diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py index 5d20d42a1b..f58349bd5b 100644 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -8,6 +8,7 @@ mod.list( "cursorless_forward_backward_modifier", desc="Cursorless forward/backward modifiers" ) +mod.list("cursorless_spread_scope_modifier", desc="Cursorless spread modifiers") @mod.capture(rule="{user.cursorless_previous_next_modifier}") @@ -26,11 +27,12 @@ def cursorless_relative_scope_singular(m) -> dict[str, Any]: getattr(m, "ordinals_small", 1), 1, m.cursorless_relative_direction, + False, ) @mod.capture( - rule=" " + rule="[{user.cursorless_spread_scope_modifier}] " ) def cursorless_relative_scope_plural(m) -> dict[str, Any]: """Relative previous/next plural scope. `next three funks`""" @@ -39,11 +41,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]: 1, m.private_cursorless_number_small, m.cursorless_relative_direction, + hasattr(m, "cursorless_spread_scope_modifier"), ) @mod.capture( - rule=" [{user.cursorless_forward_backward_modifier}]" + rule="[{user.cursorless_spread_scope_modifier}] [{user.cursorless_forward_backward_modifier}]" ) def cursorless_relative_scope_count(m) -> dict[str, Any]: """Relative count scope. `three funks`""" @@ -52,6 +55,7 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]: 0, m.private_cursorless_number_small, getattr(m, "cursorless_forward_backward_modifier", "forward"), + hasattr(m, "cursorless_spread_scope_modifier"), ) @@ -65,6 +69,7 @@ def cursorless_relative_scope_one_backward(m) -> dict[str, Any]: 0, 1, m.cursorless_forward_backward_modifier, + False, ) @@ -82,7 +87,7 @@ def cursorless_relative_scope(m) -> dict[str, Any]: def create_relative_scope_modifier( - scope_type: dict, offset: int, length: int, direction: str + scope_type: dict, offset: int, length: int, direction: str, spread: bool ) -> dict[str, Any]: return { "type": "relativeScope", @@ -90,4 +95,5 @@ def create_relative_scope_modifier( "offset": offset, "length": length, "direction": direction, + "spread": spread, } diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index 863808f455..9191375e26 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -84,6 +84,9 @@ "visible": "visible" }, "simple_scope_modifier": { "every": "every", "grand": "ancestor" }, + "spread_scope_modifier": { + "spread": "spread" + }, "interior_modifier": { "inside": "interiorOnly" }, diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index 505a46d4ee..a14c915dcc 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -272,6 +272,9 @@ export interface OrdinalScopeModifier { /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ length: number; + + /** If true spread to individual targets instead of combined range */ + spread?: boolean; } export type Direction = "forward" | "backward"; @@ -297,6 +300,9 @@ export interface RelativeScopeModifier { /** Indicates which direction both {@link offset} and {@link length} go * relative to input target */ direction: Direction; + + /** If true spread to individual targets instead of combined range */ + spread?: boolean; } /** diff --git a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts index c42e5452f6..be70542f3c 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts @@ -83,28 +83,33 @@ export class PrimitiveTargetSpokenFormGenerator { case "ordinalScope": { const scope = this.handleScopeType(modifier.scopeType); + const spread = modifier.spread + ? this.spokenFormMap.modifierExtra.spread + : []; if (modifier.length === 1) { if (modifier.start === -1) { - return [this.spokenFormMap.modifierExtra.last, scope]; + return [spread, this.spokenFormMap.modifierExtra.last, scope]; } if (modifier.start === 0) { - return [this.spokenFormMap.modifierExtra.first, scope]; + return [spread, this.spokenFormMap.modifierExtra.first, scope]; } if (modifier.start < 0) { return [ + spread, ordinalToSpokenForm(Math.abs(modifier.start)), this.spokenFormMap.modifierExtra.last, scope, ]; } - return [ordinalToSpokenForm(modifier.start + 1), scope]; + return [spread, ordinalToSpokenForm(modifier.start + 1), scope]; } const number = numberToSpokenForm(modifier.length); if (modifier.start === 0) { return [ + spread, this.spokenFormMap.modifierExtra.first, number, pluralize(scope), @@ -112,6 +117,7 @@ export class PrimitiveTargetSpokenFormGenerator { } if (modifier.start === -modifier.length) { return [ + spread, this.spokenFormMap.modifierExtra.last, number, pluralize(scope), @@ -157,6 +163,9 @@ export class PrimitiveTargetSpokenFormGenerator { modifier: RelativeScopeModifier, ): SpokenFormComponent { const scope = this.handleScopeType(modifier.scopeType); + const spread = modifier.spread + ? this.spokenFormMap.modifierExtra.spread + : []; if (modifier.length === 1) { const direction = @@ -165,7 +174,7 @@ export class PrimitiveTargetSpokenFormGenerator { : connectives.backward; // token forward/backward - return [scope, direction]; + return [spread, scope, direction]; } const length = numberToSpokenForm(modifier.length); @@ -174,11 +183,11 @@ export class PrimitiveTargetSpokenFormGenerator { // two tokens // This could also have been "two tokens forward"; there is no way to disambiguate. if (modifier.direction === "forward") { - return [length, scopePlural]; + return [spread, length, scopePlural]; } // two tokens backward - return [length, scopePlural, connectives.backward]; + return [spread, length, scopePlural, connectives.backward]; } private handleRelativeScopeExclusive( @@ -189,25 +198,28 @@ export class PrimitiveTargetSpokenFormGenerator { modifier.direction === "forward" ? connectives.next : connectives.previous; + const spread = modifier.spread + ? this.spokenFormMap.modifierExtra.spread + : []; if (modifier.offset === 1) { const number = numberToSpokenForm(modifier.length); if (modifier.length === 1) { // next/previous token - return [direction, scope]; + return [spread, direction, scope]; } const scopePlural = pluralize(scope); // next/previous two tokens - return [direction, number, scopePlural]; + return [spread, direction, number, scopePlural]; } if (modifier.length === 1) { const ordinal = ordinalToSpokenForm(modifier.offset); // second next/previous token - return [ordinal, direction, scope]; + return [spread, ordinal, direction, scope]; } throw new NoSpokenFormError( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts index db6d863e4e..02ee54d91f 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -4,6 +4,7 @@ import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; import { createRangeTargetFromIndices, + sliceTargetsByIndices, getEveryScopeTargets, } from "./targetSequenceUtils"; @@ -24,6 +25,10 @@ export class OrdinalScopeStage implements ModifierStage { this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); const endIndex = startIndex + this.modifier.length - 1; + if (this.modifier.spread) { + return sliceTargetsByIndices(targets, startIndex, endIndex); + } + return [ createRangeTargetFromIndices( target.isReversed, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 9dc97beee3..0ffa25a806 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -2,11 +2,17 @@ import type { RelativeScopeModifier } from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { + constructScopeRangeTarget, + constructTargetsFromScopes, +} from "./constructScopeRangeTarget"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { TargetScope } from "./scopeHandlers/scope.types"; -import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types"; +import type { + ContainmentPolicy, + ScopeHandler, +} from "./scopeHandlers/scopeHandler.types"; import { OutOfRangeError } from "./targetSequenceUtils"; /** @@ -32,7 +38,26 @@ export class RelativeExclusiveScopeStage implements ModifierStage { return runLegacy(this.modifierStageFactory, this.modifier, target); } - const { isReversed, editor, contentRange: inputRange } = target; + const scopes = this.getsScopes(scopeHandler, target); + const { isReversed } = target; + + if (this.modifier.spread) { + return constructTargetsFromScopes(isReversed, scopes); + } + + // Then make a range when we get the desired number of scopes + return constructScopeRangeTarget( + isReversed, + scopes[0], + scopes[scopes.length - 1], + ); + } + + private getsScopes( + scopeHandler: ScopeHandler, + target: Target, + ): TargetScope[] { + const { editor, contentRange: inputRange } = target; const { length: desiredScopeCount, direction, offset } = this.modifier; const initialPosition = @@ -45,8 +70,8 @@ export class RelativeExclusiveScopeStage implements ModifierStage { ? "disallowed" : "disallowedIfStrict"; + const scopes: TargetScope[] = []; let scopeCount = 0; - let proximalScope: TargetScope | undefined; for (const scope of scopeHandler.generateScopes( editor, initialPosition, @@ -63,17 +88,20 @@ export class RelativeExclusiveScopeStage implements ModifierStage { if (scopeCount === offset) { // When we hit offset, that becomes proximal scope if (desiredScopeCount === 1) { - // Just yield it if we only want 1 scope - return scope.getTargets(isReversed); + // Just return it if we only want 1 scope + return [scope]; } - proximalScope = scope; + scopes.push(scope); + continue; } if (scopeCount === offset + desiredScopeCount - 1) { + scopes.push(scope); + // Then make a range when we get the desired number of scopes - return constructScopeRangeTarget(isReversed, proximalScope!, scope); + return scopes; } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index 73528db44c..993e3cdfd5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -5,7 +5,10 @@ import { import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { + constructScopeRangeTarget, + constructTargetsFromScopes, +} from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; @@ -74,6 +77,10 @@ export class RelativeInclusiveScopeStage implements ModifierStage { throw new OutOfRangeError(); } + if (this.modifier.spread) { + return constructTargetsFromScopes(isReversed, scopes); + } + return constructScopeRangeTarget( isReversed, scopes[0], diff --git a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts index b3fc409ea5..9fa142a28c 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -26,9 +26,8 @@ export function constructScopeRangeTarget( 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"); - } + assertTargetLength(targets2); + assertTargetLength(targets1); const [target1] = targets1; const [target2] = targets2; @@ -45,3 +44,21 @@ export function constructScopeRangeTarget( createContinuousRangeTarget(isReversed, startTarget, endTarget, true, true), ]; } + +/** Construct list of targets based upon the input list of scopes. */ +export function constructTargetsFromScopes( + isReversed: boolean, + scopes: TargetScope[], +): Target[] { + return scopes.flatMap((scope) => { + const targets = scope.getTargets(isReversed); + assertTargetLength(targets); + return targets; + }); +} + +function assertTargetLength(targets: Target[]) { + if (targets.length !== 1) { + throw Error("Scope range targets must be single-target"); + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index c3439a88cd..e6cfd5c7e3 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -10,6 +10,16 @@ export class OutOfRangeError extends Error { } } +function assertIndices( + targets: Target[], + startIndex: number, + endIndex: number, +): void { + if (startIndex < 0 || endIndex >= targets.length) { + throw new OutOfRangeError(); + } +} + /** * Construct a single range target between two targets in a list of targets, * inclusive @@ -25,9 +35,7 @@ export function createRangeTargetFromIndices( startIndex: number, endIndex: number, ): Target { - if (startIndex < 0 || endIndex >= targets.length) { - throw new OutOfRangeError(); - } + assertIndices(targets, startIndex, endIndex); if (startIndex === endIndex) { return targets[startIndex]; @@ -42,6 +50,17 @@ export function createRangeTargetFromIndices( ); } +/** Slice list of targets by given indices */ +export function sliceTargetsByIndices( + targets: Target[], + startIndex: number, + endIndex: number, +): Target[] { + assertIndices(targets, startIndex, endIndex); + + return targets.slice(startIndex, endIndex + 1); +} + export function getEveryScopeTargets( modifierStageFactory: ModifierStageFactory, target: Target, diff --git a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts index 3665eb5cd9..658ab3bc50 100644 --- a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts +++ b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts @@ -61,6 +61,7 @@ type ModifierExtra = | "next" | "forward" | "backward" - | "ancestor"; + | "ancestor" + | "spread"; export type SpokenFormType = keyof SpokenFormMapKeyTypes; diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 300c2cacab..8c078570b4 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -134,6 +134,7 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { forward: "forward", backward: "backward", ancestor: "grand", + spread: "spread", }, customRegex: {}, diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml new file mode 100644 index 0000000000..ac65de7b5a --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change spread first two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: 0 + length: 2 + spread: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " baz" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml new file mode 100644 index 0000000000..894544dabc --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml @@ -0,0 +1,28 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change spread last two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: -2 + length: 2 + spread: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml new file mode 100644 index 0000000000..082b414bd8 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change spread next two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 2 + direction: forward + spread: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml new file mode 100644 index 0000000000..0c422e93ce --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change spread two tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: forward + spread: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " baz" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml new file mode 100644 index 0000000000..95aae0303f --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml @@ -0,0 +1,29 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change spread two tokens backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 2 + direction: backward + spread: true + usePrePhraseSnapshot: true +initialState: + documentContents: foo bar baz + selections: + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + marks: {} +finalState: + documentContents: "foo " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} From 5c6770d9e69c60f579532c5818ac98aed6da40d2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 07:59:14 +0100 Subject: [PATCH 02/17] Added changelog --- changelog/2024-03-addedSpreadModifier.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/2024-03-addedSpreadModifier.md diff --git a/changelog/2024-03-addedSpreadModifier.md b/changelog/2024-03-addedSpreadModifier.md new file mode 100644 index 0000000000..02cb08c5de --- /dev/null +++ b/changelog/2024-03-addedSpreadModifier.md @@ -0,0 +1,9 @@ +--- +tags: [enhancement] +pullRequest: 2254 +--- + +- Added increment spread modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range. + +- `take spread two tokens` selects two tokens as separate selections +- `take spread first two tokens` selects two tokens as separate selections From 0f61f4df1edc6b79f0a7d5056c6c31d6c3ccd990 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 08:01:36 +0100 Subject: [PATCH 03/17] Update documentation --- changelog/2024-03-addedSpreadModifier.md | 4 ++-- docs/user/README.md | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelog/2024-03-addedSpreadModifier.md b/changelog/2024-03-addedSpreadModifier.md index 02cb08c5de..5a74cbdf3f 100644 --- a/changelog/2024-03-addedSpreadModifier.md +++ b/changelog/2024-03-addedSpreadModifier.md @@ -5,5 +5,5 @@ pullRequest: 2254 - Added increment spread modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range. -- `take spread two tokens` selects two tokens as separate selections -- `take spread first two tokens` selects two tokens as separate selections +- `"take spread two tokens"` selects two tokens as separate selections +- `"take spread first two tokens"` selects two tokens as separate selections diff --git a/docs/user/README.md b/docs/user/README.md index 4589addccc..1127250579 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -205,6 +205,13 @@ And here is a table of the spoken forms: | `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` | | `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` | +##### `spread` + +The modifier `spread` can be used to get multiple selections from the above numbered scopes instead of one contiguous range. + +- `"take spread two tokens"` selects two tokens as separate selections +- `"take spread first two tokens"` selects two tokens as separate selections + ##### `"every"` The modifier `"every"` can be used to select a syntactic element and all of its matching siblings. From df2eb669742171089b21e66f4e67b94be7f6588d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 08:11:06 +0100 Subject: [PATCH 04/17] Clean up --- cursorless-talon/src/modifiers/ordinal_scope.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index 25b138d428..0cc823a819 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -49,7 +49,6 @@ def cursorless_ordinal_range(m) -> dict[str, Any]: def cursorless_first_last(m) -> dict[str, Any]: """First/last `n` scopes; eg "first three funks""" spread = hasattr(m, "cursorless_spread_scope_modifier") - print(m) if hasattr(m, "cursorless_first_modifier"): return create_ordinal_scope_modifier( m.cursorless_scope_type_plural, From 07708567761def65bdd33e83119341af37103a8f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 08:16:19 +0100 Subject: [PATCH 05/17] Makes spread argument optional Talon side --- cursorless-talon/src/modifiers/ordinal_scope.py | 6 ++++-- cursorless-talon/src/modifiers/relative_scope.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index 0cc823a819..cb413a9876 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -76,10 +76,12 @@ def create_ordinal_scope_modifier( length: int = 1, spread: bool = False, ): - return { + res = { "type": "ordinalScope", "scopeType": scope_type, "start": start, "length": length, - "spread": spread, } + if spread: + res["spread"] = True + return res diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py index f58349bd5b..111f1ffbfd 100644 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -87,13 +87,19 @@ def cursorless_relative_scope(m) -> dict[str, Any]: def create_relative_scope_modifier( - scope_type: dict, offset: int, length: int, direction: str, spread: bool + scope_type: dict, + offset: int, + length: int, + direction: str, + spread: bool, ) -> dict[str, Any]: - return { + res = { "type": "relativeScope", "scopeType": scope_type, "offset": offset, "length": length, "direction": direction, - "spread": spread, } + if spread: + res["spread"] = True + return res From 8c73b6d7d135fc620d548dec33b157eb1b4d2be2 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 18:15:58 +0100 Subject: [PATCH 06/17] Remove spread and reuse every --- changelog/2024-03-addedSpreadModifier.md | 6 +-- .../src/modifiers/ordinal_scope.py | 14 +++---- .../src/modifiers/relative_scope.py | 15 ++++---- .../src/modifiers/simple_scope_modifier.py | 37 ++++++++++--------- cursorless-talon/src/spoken_forms.json | 6 +-- docs/user/README.md | 12 +++--- .../command/PartialTargetDescriptor.types.ts | 8 ++-- .../primitiveTargetToSpokenForm.ts | 36 +++++++++--------- .../modifiers/OrdinalScopeStage.ts | 2 +- .../modifiers/RelativeExclusiveScopeStage.ts | 2 +- .../modifiers/RelativeInclusiveScopeStage.ts | 2 +- .../src/spokenForms/SpokenFormType.ts | 3 +- .../spokenForms/defaultSpokenFormMapCore.ts | 1 - .../changeSpreadFirstTwoTokens.yml | 4 +- .../changeSpreadLastTwoTokens.yml | 4 +- .../changeSpreadNextTwoTokens.yml | 4 +- .../relativeScopes/changeSpreadTwoTokens.yml | 4 +- .../changeSpreadTwoTokensBackward.yml | 4 +- 18 files changed, 79 insertions(+), 85 deletions(-) diff --git a/changelog/2024-03-addedSpreadModifier.md b/changelog/2024-03-addedSpreadModifier.md index 5a74cbdf3f..2e5b1fd369 100644 --- a/changelog/2024-03-addedSpreadModifier.md +++ b/changelog/2024-03-addedSpreadModifier.md @@ -3,7 +3,7 @@ tags: [enhancement] pullRequest: 2254 --- -- Added increment spread modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range. +- Added every/spread ordinal/relative modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range. -- `"take spread two tokens"` selects two tokens as separate selections -- `"take spread first two tokens"` selects two tokens as separate selections +- `"take every two tokens"` selects two tokens as separate selections +- `"take every first two tokens"` selects two tokens as separate selections diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index cb413a9876..f3f8d528be 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -44,23 +44,23 @@ def cursorless_ordinal_range(m) -> dict[str, Any]: @mod.capture( - rule="[{user.cursorless_spread_scope_modifier}] ({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + rule="[{user.cursorless_every_scope_modifier}] ({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " ) def cursorless_first_last(m) -> dict[str, Any]: """First/last `n` scopes; eg "first three funks""" - spread = hasattr(m, "cursorless_spread_scope_modifier") + is_every = hasattr(m, "cursorless_every_scope_modifier") if hasattr(m, "cursorless_first_modifier"): return create_ordinal_scope_modifier( m.cursorless_scope_type_plural, 0, m.private_cursorless_number_small, - spread, + is_every, ) return create_ordinal_scope_modifier( m.cursorless_scope_type_plural, -m.private_cursorless_number_small, m.private_cursorless_number_small, - spread, + is_every, ) @@ -74,7 +74,7 @@ def create_ordinal_scope_modifier( scope_type: dict, start: int, length: int = 1, - spread: bool = False, + is_every: bool = False, ): res = { "type": "ordinalScope", @@ -82,6 +82,6 @@ def create_ordinal_scope_modifier( "start": start, "length": length, } - if spread: - res["spread"] = True + if is_every: + res["isEvery"] = True return res diff --git a/cursorless-talon/src/modifiers/relative_scope.py b/cursorless-talon/src/modifiers/relative_scope.py index 111f1ffbfd..1fd60ac693 100644 --- a/cursorless-talon/src/modifiers/relative_scope.py +++ b/cursorless-talon/src/modifiers/relative_scope.py @@ -8,7 +8,6 @@ mod.list( "cursorless_forward_backward_modifier", desc="Cursorless forward/backward modifiers" ) -mod.list("cursorless_spread_scope_modifier", desc="Cursorless spread modifiers") @mod.capture(rule="{user.cursorless_previous_next_modifier}") @@ -32,7 +31,7 @@ def cursorless_relative_scope_singular(m) -> dict[str, Any]: @mod.capture( - rule="[{user.cursorless_spread_scope_modifier}] " + rule="[{user.cursorless_every_scope_modifier}] " ) def cursorless_relative_scope_plural(m) -> dict[str, Any]: """Relative previous/next plural scope. `next three funks`""" @@ -41,12 +40,12 @@ def cursorless_relative_scope_plural(m) -> dict[str, Any]: 1, m.private_cursorless_number_small, m.cursorless_relative_direction, - hasattr(m, "cursorless_spread_scope_modifier"), + hasattr(m, "cursorless_every_scope_modifier"), ) @mod.capture( - rule="[{user.cursorless_spread_scope_modifier}] [{user.cursorless_forward_backward_modifier}]" + rule="[{user.cursorless_every_scope_modifier}] [{user.cursorless_forward_backward_modifier}]" ) def cursorless_relative_scope_count(m) -> dict[str, Any]: """Relative count scope. `three funks`""" @@ -55,7 +54,7 @@ def cursorless_relative_scope_count(m) -> dict[str, Any]: 0, m.private_cursorless_number_small, getattr(m, "cursorless_forward_backward_modifier", "forward"), - hasattr(m, "cursorless_spread_scope_modifier"), + hasattr(m, "cursorless_every_scope_modifier"), ) @@ -91,7 +90,7 @@ def create_relative_scope_modifier( offset: int, length: int, direction: str, - spread: bool, + is_every: bool, ) -> dict[str, Any]: res = { "type": "relativeScope", @@ -100,6 +99,6 @@ def create_relative_scope_modifier( "length": length, "direction": direction, } - if spread: - res["spread"] = True + if is_every: + res["isEvery"] = True return res diff --git a/cursorless-talon/src/modifiers/simple_scope_modifier.py b/cursorless-talon/src/modifiers/simple_scope_modifier.py index 5d51ba8a4e..f0a1179df7 100644 --- a/cursorless-talon/src/modifiers/simple_scope_modifier.py +++ b/cursorless-talon/src/modifiers/simple_scope_modifier.py @@ -5,31 +5,32 @@ mod = Module() mod.list( - "cursorless_simple_scope_modifier", - desc='Cursorless simple scope modifiers, eg "every"', + "cursorless_every_scope_modifier", + desc="Cursorless every scope modifiers", +) +mod.list( + "cursorless_ancestor_scope_modifier", + desc="Cursorless ancestor scope modifiers", ) @mod.capture( - rule="[{user.cursorless_simple_scope_modifier}] " + rule="{user.cursorless_every_scope_modifier} | [{user.cursorless_ancestor_scope_modifier}] " ) def cursorless_simple_scope_modifier(m) -> dict[str, Any]: """Containing scope, every scope, etc""" - if hasattr(m, "cursorless_simple_scope_modifier"): - modifier = m.cursorless_simple_scope_modifier - - if modifier == "every": - return { - "type": "everyScope", - "scopeType": m.cursorless_scope_type, - } - - if modifier == "ancestor": - return { - "type": "containingScope", - "scopeType": m.cursorless_scope_type, - "ancestorIndex": 1, - } + if hasattr(m, "cursorless_every_scope_modifier"): + return { + "type": "everyScope", + "scopeType": m.cursorless_scope_type, + } + + if hasattr(m, "cursorless_ancestor_scope_modifier"): + return { + "type": "containingScope", + "scopeType": m.cursorless_scope_type, + "ancestorIndex": 1, + } return { "type": "containingScope", diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index 9191375e26..4322a3e6ff 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -83,10 +83,8 @@ "its": "inferPreviousMark", "visible": "visible" }, - "simple_scope_modifier": { "every": "every", "grand": "ancestor" }, - "spread_scope_modifier": { - "spread": "spread" - }, + "every_scope_modifier": { "every": "every" }, + "ancestor_scope_modifier": { "grand": "ancestor" }, "interior_modifier": { "inside": "interiorOnly" }, diff --git a/docs/user/README.md b/docs/user/README.md index 1127250579..4324e6ad36 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -205,13 +205,6 @@ And here is a table of the spoken forms: | `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` | | `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` | -##### `spread` - -The modifier `spread` can be used to get multiple selections from the above numbered scopes instead of one contiguous range. - -- `"take spread two tokens"` selects two tokens as separate selections -- `"take spread first two tokens"` selects two tokens as separate selections - ##### `"every"` The modifier `"every"` can be used to select a syntactic element and all of its matching siblings. @@ -222,6 +215,11 @@ The modifier `"every"` can be used to select a syntactic element and all of its For example, the command `"take every key [blue] air"` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'. +The modifier `every` can also be used to get multiple selections from the ordinal and relative numbered scopes instead of one contiguous range. + +- `"take every two tokens"` selects two tokens as separate selections +- `"take every first two tokens"` selects two tokens as separate selections + ##### `"grand"` The modifier `"grand"` can be used to select the parent of the containing syntactic element. diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index a14c915dcc..925af1856d 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -273,8 +273,8 @@ export interface OrdinalScopeModifier { /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ length: number; - /** If true spread to individual targets instead of combined range */ - spread?: boolean; + /** If true use individual targets instead of combined range */ + isEvery?: boolean; } export type Direction = "forward" | "backward"; @@ -301,8 +301,8 @@ export interface RelativeScopeModifier { * relative to input target */ direction: Direction; - /** If true spread to individual targets instead of combined range */ - spread?: boolean; + /** If true use individual targets instead of combined range */ + isEvery?: boolean; } /** diff --git a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts index be70542f3c..f9695c8399 100644 --- a/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts +++ b/packages/cursorless-engine/src/generateSpokenForm/primitiveTargetToSpokenForm.ts @@ -83,33 +83,33 @@ export class PrimitiveTargetSpokenFormGenerator { case "ordinalScope": { const scope = this.handleScopeType(modifier.scopeType); - const spread = modifier.spread - ? this.spokenFormMap.modifierExtra.spread + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope : []; if (modifier.length === 1) { if (modifier.start === -1) { - return [spread, this.spokenFormMap.modifierExtra.last, scope]; + return [isEvery, this.spokenFormMap.modifierExtra.last, scope]; } if (modifier.start === 0) { - return [spread, this.spokenFormMap.modifierExtra.first, scope]; + return [isEvery, this.spokenFormMap.modifierExtra.first, scope]; } if (modifier.start < 0) { return [ - spread, + isEvery, ordinalToSpokenForm(Math.abs(modifier.start)), this.spokenFormMap.modifierExtra.last, scope, ]; } - return [spread, ordinalToSpokenForm(modifier.start + 1), scope]; + return [isEvery, ordinalToSpokenForm(modifier.start + 1), scope]; } const number = numberToSpokenForm(modifier.length); if (modifier.start === 0) { return [ - spread, + isEvery, this.spokenFormMap.modifierExtra.first, number, pluralize(scope), @@ -117,7 +117,7 @@ export class PrimitiveTargetSpokenFormGenerator { } if (modifier.start === -modifier.length) { return [ - spread, + isEvery, this.spokenFormMap.modifierExtra.last, number, pluralize(scope), @@ -163,8 +163,8 @@ export class PrimitiveTargetSpokenFormGenerator { modifier: RelativeScopeModifier, ): SpokenFormComponent { const scope = this.handleScopeType(modifier.scopeType); - const spread = modifier.spread - ? this.spokenFormMap.modifierExtra.spread + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope : []; if (modifier.length === 1) { @@ -174,7 +174,7 @@ export class PrimitiveTargetSpokenFormGenerator { : connectives.backward; // token forward/backward - return [spread, scope, direction]; + return [isEvery, scope, direction]; } const length = numberToSpokenForm(modifier.length); @@ -183,11 +183,11 @@ export class PrimitiveTargetSpokenFormGenerator { // two tokens // This could also have been "two tokens forward"; there is no way to disambiguate. if (modifier.direction === "forward") { - return [spread, length, scopePlural]; + return [isEvery, length, scopePlural]; } // two tokens backward - return [spread, length, scopePlural, connectives.backward]; + return [isEvery, length, scopePlural, connectives.backward]; } private handleRelativeScopeExclusive( @@ -198,8 +198,8 @@ export class PrimitiveTargetSpokenFormGenerator { modifier.direction === "forward" ? connectives.next : connectives.previous; - const spread = modifier.spread - ? this.spokenFormMap.modifierExtra.spread + const isEvery = modifier.isEvery + ? this.spokenFormMap.simpleModifier.everyScope : []; if (modifier.offset === 1) { @@ -207,19 +207,19 @@ export class PrimitiveTargetSpokenFormGenerator { if (modifier.length === 1) { // next/previous token - return [spread, direction, scope]; + return [isEvery, direction, scope]; } const scopePlural = pluralize(scope); // next/previous two tokens - return [spread, direction, number, scopePlural]; + return [isEvery, direction, number, scopePlural]; } if (modifier.length === 1) { const ordinal = ordinalToSpokenForm(modifier.offset); // second next/previous token - return [spread, ordinal, direction, scope]; + return [isEvery, ordinal, direction, scope]; } throw new NoSpokenFormError( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts index 02ee54d91f..f82d5d2ed0 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -25,7 +25,7 @@ export class OrdinalScopeStage implements ModifierStage { this.modifier.start + (this.modifier.start < 0 ? targets.length : 0); const endIndex = startIndex + this.modifier.length - 1; - if (this.modifier.spread) { + if (this.modifier.isEvery) { return sliceTargetsByIndices(targets, startIndex, endIndex); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 0ffa25a806..b156615ddf 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -41,7 +41,7 @@ export class RelativeExclusiveScopeStage implements ModifierStage { const scopes = this.getsScopes(scopeHandler, target); const { isReversed } = target; - if (this.modifier.spread) { + if (this.modifier.isEvery) { return constructTargetsFromScopes(isReversed, scopes); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index 993e3cdfd5..b7fa22526d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -77,7 +77,7 @@ export class RelativeInclusiveScopeStage implements ModifierStage { throw new OutOfRangeError(); } - if (this.modifier.spread) { + if (this.modifier.isEvery) { return constructTargetsFromScopes(isReversed, scopes); } diff --git a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts index 658ab3bc50..3665eb5cd9 100644 --- a/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts +++ b/packages/cursorless-engine/src/spokenForms/SpokenFormType.ts @@ -61,7 +61,6 @@ type ModifierExtra = | "next" | "forward" | "backward" - | "ancestor" - | "spread"; + | "ancestor"; export type SpokenFormType = keyof SpokenFormMapKeyTypes; diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 8c078570b4..300c2cacab 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -134,7 +134,6 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { forward: "forward", backward: "backward", ancestor: "grand", - spread: "spread", }, customRegex: {}, diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml index ac65de7b5a..d0d2a375c4 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadFirstTwoTokens.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 6 - spokenForm: change spread first two tokens + spokenForm: change every first two tokens action: name: clearAndSetSelection target: @@ -11,7 +11,7 @@ command: scopeType: {type: token} start: 0 length: 2 - spread: true + isEvery: true usePrePhraseSnapshot: true initialState: documentContents: foo bar baz diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml index 894544dabc..9e5f640cd7 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeSpreadLastTwoTokens.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 6 - spokenForm: change spread last two tokens + spokenForm: change every last two tokens action: name: clearAndSetSelection target: @@ -11,7 +11,7 @@ command: scopeType: {type: token} start: -2 length: 2 - spread: true + isEvery: true usePrePhraseSnapshot: true initialState: documentContents: foo bar baz diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml index 082b414bd8..f4313682e0 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadNextTwoTokens.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 6 - spokenForm: change spread next two tokens + spokenForm: change every next two tokens action: name: clearAndSetSelection target: @@ -12,7 +12,7 @@ command: offset: 1 length: 2 direction: forward - spread: true + isEvery: true usePrePhraseSnapshot: true initialState: documentContents: foo bar baz diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml index 0c422e93ce..5e06b34203 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokens.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 6 - spokenForm: change spread two tokens + spokenForm: change every two tokens action: name: clearAndSetSelection target: @@ -12,7 +12,7 @@ command: offset: 0 length: 2 direction: forward - spread: true + isEvery: true usePrePhraseSnapshot: true initialState: documentContents: foo bar baz diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml index 95aae0303f..e9faecff0d 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeSpreadTwoTokensBackward.yml @@ -1,7 +1,7 @@ languageId: plaintext command: version: 6 - spokenForm: change spread two tokens backward + spokenForm: change every two tokens backward action: name: clearAndSetSelection target: @@ -12,7 +12,7 @@ command: offset: 0 length: 2 direction: backward - spread: true + isEvery: true usePrePhraseSnapshot: true initialState: documentContents: foo bar baz From ac0bc761fda6621b7d7748fea98be34f90d86650 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 27 Feb 2024 18:23:59 +0100 Subject: [PATCH 07/17] cleanup --- .../modifiers/targetSequenceUtils.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index e6cfd5c7e3..d5c51973fb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -10,16 +10,6 @@ export class OutOfRangeError extends Error { } } -function assertIndices( - targets: Target[], - startIndex: number, - endIndex: number, -): void { - if (startIndex < 0 || endIndex >= targets.length) { - throw new OutOfRangeError(); - } -} - /** * Construct a single range target between two targets in a list of targets, * inclusive @@ -72,3 +62,13 @@ export function getEveryScopeTargets( }); return containingStage.run(target); } + +function assertIndices( + targets: Target[], + startIndex: number, + endIndex: number, +): void { + if (startIndex < 0 || endIndex >= targets.length) { + throw new OutOfRangeError(); + } +} From e0e2459f604dc53079009607e4335775b872273c Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:36:11 +0000 Subject: [PATCH 08/17] Minor tweaks --- changelog/2024-03-addedSpreadModifier.md | 4 +-- docs/user/README.md | 8 +++-- .../processTargets/modifiers/InstanceStage.ts | 2 +- .../modifiers/OrdinalScopeStage.ts | 4 +-- .../modifiers/RelativeExclusiveScopeStage.ts | 2 +- .../modifiers/RelativeInclusiveScopeStage.ts | 3 +- .../src/processTargets/modifiers/listUtils.ts | 27 +++++++++++++++++ .../modifiers/relativeScopeLegacy.ts | 2 +- .../modifiers/targetSequenceUtils.ts | 29 +------------------ 9 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts diff --git a/changelog/2024-03-addedSpreadModifier.md b/changelog/2024-03-addedSpreadModifier.md index 2e5b1fd369..cfe6733226 100644 --- a/changelog/2024-03-addedSpreadModifier.md +++ b/changelog/2024-03-addedSpreadModifier.md @@ -3,7 +3,7 @@ tags: [enhancement] pullRequest: 2254 --- -- Added every/spread ordinal/relative modifier. Turn relative and ordinal range modifiers into multiple target selections instead of contiguous range. +- Added every/spread ordinal/relative modifier. Turns relative and ordinal range modifiers into multiple target selections instead of contiguous range. - `"take every two tokens"` selects two tokens as separate selections -- `"take every first two tokens"` selects two tokens as separate selections +- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors) diff --git a/docs/user/README.md b/docs/user/README.md index 4324e6ad36..c13d6fb1ec 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -205,6 +205,8 @@ And here is a table of the spoken forms: | `"previous [number] [scope]s"` | previous `[number]` instances of `[scope]` | `"take previous three funks"` | | `"previous [scope]"` | Previous instance of `[scope]` | `"take previous funk"` | +You can prefix the modifier with `"every"` to yield multiple targets rather than a range. For example, `"take every two tokens"` selects two tokens as separate selections. + ##### `"every"` The modifier `"every"` can be used to select a syntactic element and all of its matching siblings. @@ -215,10 +217,12 @@ The modifier `"every"` can be used to select a syntactic element and all of its For example, the command `"take every key [blue] air"` will select every key in the map/object/dict including the token with a blue hat over the letter 'a'. -The modifier `every` can also be used to get multiple selections from the ordinal and relative numbered scopes instead of one contiguous range. +###### Use with relative / ordinal modifiers + +The modifier `every` can also be used to cause [relative / ordinal modifiers](#previous--next--ordinal--number) to yield multiple targets rather than a range: - `"take every two tokens"` selects two tokens as separate selections -- `"take every first two tokens"` selects two tokens as separate selections +- `"pre every first two lines"` puts a cursor before each of first two lines in block (results in multiple cursors) ##### `"grand"` diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts index 156819c7c8..952645d92e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InstanceStage.ts @@ -15,8 +15,8 @@ import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; import { PlainTarget } from "../targets"; import { ContainingTokenIfUntypedEmptyStage } from "./ConditionalModifierStages"; -import { OutOfRangeError } from "./targetSequenceUtils"; import { StoredTargetMap } from "../.."; +import { OutOfRangeError } from "./listUtils"; export class InstanceStage implements ModifierStage { constructor( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts index f82d5d2ed0..babdcd2801 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -4,9 +4,9 @@ import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; import { createRangeTargetFromIndices, - sliceTargetsByIndices, getEveryScopeTargets, } from "./targetSequenceUtils"; +import { sliceStrict } from "./listUtils"; export class OrdinalScopeStage implements ModifierStage { constructor( @@ -26,7 +26,7 @@ export class OrdinalScopeStage implements ModifierStage { const endIndex = startIndex + this.modifier.length - 1; if (this.modifier.isEvery) { - return sliceTargetsByIndices(targets, startIndex, endIndex); + return sliceStrict(targets, startIndex, endIndex); } return [ diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index b156615ddf..a780138915 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -13,7 +13,7 @@ import type { ContainmentPolicy, ScopeHandler, } from "./scopeHandlers/scopeHandler.types"; -import { OutOfRangeError } from "./targetSequenceUtils"; +import { OutOfRangeError } from "./listUtils"; /** * Handles relative modifiers that don't include targets intersecting with the diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index b7fa22526d..c87d9f961b 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -13,7 +13,8 @@ import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPo import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { itake } from "itertools"; -import { OutOfRangeError } from "./targetSequenceUtils"; +import { OutOfRangeError } from "./listUtils"; + /** * Handles relative modifiers that include targets intersecting with the input, * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: diff --git a/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts new file mode 100644 index 0000000000..1b28e296d8 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/listUtils.ts @@ -0,0 +1,27 @@ +export class OutOfRangeError extends Error { + constructor() { + super("Scope index out of range"); + this.name = "OutOfRangeError"; + } +} + +/** Slice list of by given indices */ +export function sliceStrict( + targets: T[], + startIndex: number, + endIndex: number, +): T[] { + assertIndices(targets, startIndex, endIndex); + + return targets.slice(startIndex, endIndex + 1); +} + +export function assertIndices( + targets: T[], + startIndex: number, + endIndex: number, +): void { + if (startIndex < 0 || endIndex >= targets.length) { + throw new OutOfRangeError(); + } +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts index 5c952a7270..02d85bc92e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts @@ -6,9 +6,9 @@ import { UntypedTarget } from "../targets"; import { createRangeTargetFromIndices, getEveryScopeTargets, - OutOfRangeError, } from "./targetSequenceUtils"; import { TooFewScopesError } from "./TooFewScopesError"; +import { OutOfRangeError } from "./listUtils"; interface ContainingIndices { start: number; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index d5c51973fb..6da4a41524 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -2,13 +2,7 @@ import { ScopeType } from "@cursorless/common"; import { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import { createContinuousRangeTarget } from "../createContinuousRangeTarget"; - -export class OutOfRangeError extends Error { - constructor() { - super("Scope index out of range"); - this.name = "OutOfRangeError"; - } -} +import { assertIndices } from "./listUtils"; /** * Construct a single range target between two targets in a list of targets, @@ -40,17 +34,6 @@ export function createRangeTargetFromIndices( ); } -/** Slice list of targets by given indices */ -export function sliceTargetsByIndices( - targets: Target[], - startIndex: number, - endIndex: number, -): Target[] { - assertIndices(targets, startIndex, endIndex); - - return targets.slice(startIndex, endIndex + 1); -} - export function getEveryScopeTargets( modifierStageFactory: ModifierStageFactory, target: Target, @@ -62,13 +45,3 @@ export function getEveryScopeTargets( }); return containingStage.run(target); } - -function assertIndices( - targets: Target[], - startIndex: number, - endIndex: number, -): void { - if (startIndex < 0 || endIndex >= targets.length) { - throw new OutOfRangeError(); - } -} From 066a84df321afa22bbde80a77d65f80d493eafbb Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:46:54 +0000 Subject: [PATCH 09/17] more tweaks --- .../command/PartialTargetDescriptor.types.ts | 2 +- .../modifiers/RelativeExclusiveScopeStage.ts | 14 ++++------- .../modifiers/RelativeInclusiveScopeStage.ts | 7 ++---- .../modifiers/constructScopeRangeTarget.ts | 23 +++---------------- 4 files changed, 10 insertions(+), 36 deletions(-) diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index 925af1856d..61690ced1a 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -273,7 +273,7 @@ export interface OrdinalScopeModifier { /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ length: number; - /** If true use individual targets instead of combined range */ + /** If true, yields individual targets instead of contiguous range. Defaults to `false` */ isEvery?: boolean; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index a780138915..12e31b16f9 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -2,10 +2,7 @@ import type { RelativeScopeModifier } from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { - constructScopeRangeTarget, - constructTargetsFromScopes, -} from "./constructScopeRangeTarget"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { TargetScope } from "./scopeHandlers/scope.types"; @@ -38,11 +35,11 @@ export class RelativeExclusiveScopeStage implements ModifierStage { return runLegacy(this.modifierStageFactory, this.modifier, target); } - const scopes = this.getsScopes(scopeHandler, target); + const scopes = this.getScopes(scopeHandler, target); const { isReversed } = target; if (this.modifier.isEvery) { - return constructTargetsFromScopes(isReversed, scopes); + return scopes.flatMap((scope) => scope.getTargets(isReversed)); } // Then make a range when we get the desired number of scopes @@ -53,10 +50,7 @@ export class RelativeExclusiveScopeStage implements ModifierStage { ); } - private getsScopes( - scopeHandler: ScopeHandler, - target: Target, - ): TargetScope[] { + private getScopes(scopeHandler: ScopeHandler, target: Target): TargetScope[] { const { editor, contentRange: inputRange } = target; const { length: desiredScopeCount, direction, offset } = this.modifier; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index c87d9f961b..570457c3f0 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -5,10 +5,7 @@ import { import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { - constructScopeRangeTarget, - constructTargetsFromScopes, -} from "./constructScopeRangeTarget"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; @@ -79,7 +76,7 @@ export class RelativeInclusiveScopeStage implements ModifierStage { } if (this.modifier.isEvery) { - return constructTargetsFromScopes(isReversed, scopes); + return scopes.flatMap((scope) => scope.getTargets(isReversed)); } return constructScopeRangeTarget( diff --git a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts index 9fa142a28c..b3fc409ea5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -26,8 +26,9 @@ export function constructScopeRangeTarget( const targets1 = scope1.getTargets(isReversed); const targets2 = scope2.getTargets(isReversed); - assertTargetLength(targets2); - assertTargetLength(targets1); + if (targets1.length !== 1 || targets2.length !== 1) { + throw Error("Scope range targets must be single-target"); + } const [target1] = targets1; const [target2] = targets2; @@ -44,21 +45,3 @@ export function constructScopeRangeTarget( createContinuousRangeTarget(isReversed, startTarget, endTarget, true, true), ]; } - -/** Construct list of targets based upon the input list of scopes. */ -export function constructTargetsFromScopes( - isReversed: boolean, - scopes: TargetScope[], -): Target[] { - return scopes.flatMap((scope) => { - const targets = scope.getTargets(isReversed); - assertTargetLength(targets); - return targets; - }); -} - -function assertTargetLength(targets: Target[]) { - if (targets.length !== 1) { - throw Error("Scope range targets must be single-target"); - } -} From 96d94656d1713c0f4c34f82c8f98dd976409a1cc Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:49:51 +0000 Subject: [PATCH 10/17] minor tweak --- cursorless-talon/src/modifiers/ordinal_scope.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cursorless-talon/src/modifiers/ordinal_scope.py b/cursorless-talon/src/modifiers/ordinal_scope.py index f3f8d528be..0ff0ac3982 100644 --- a/cursorless-talon/src/modifiers/ordinal_scope.py +++ b/cursorless-talon/src/modifiers/ordinal_scope.py @@ -44,7 +44,11 @@ def cursorless_ordinal_range(m) -> dict[str, Any]: @mod.capture( - rule="[{user.cursorless_every_scope_modifier}] ({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + rule=( + "[{user.cursorless_every_scope_modifier}] " + "({user.cursorless_first_modifier} | {user.cursorless_last_modifier}) " + " " + ), ) def cursorless_first_last(m) -> dict[str, Any]: """First/last `n` scopes; eg "first three funks""" From 87b1cf45c299c5ab45be7f3b95185f63af6881aa Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:53:22 +0000 Subject: [PATCH 11/17] more spoken tweaks --- cursorless-talon/src/modifiers/simple_scope_modifier.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cursorless-talon/src/modifiers/simple_scope_modifier.py b/cursorless-talon/src/modifiers/simple_scope_modifier.py index f0a1179df7..cb0d2d4868 100644 --- a/cursorless-talon/src/modifiers/simple_scope_modifier.py +++ b/cursorless-talon/src/modifiers/simple_scope_modifier.py @@ -15,7 +15,10 @@ @mod.capture( - rule="{user.cursorless_every_scope_modifier} | [{user.cursorless_ancestor_scope_modifier}] " + rule=( + "[{user.cursorless_every_scope_modifier} | {user.cursorless_ancestor_scope_modifier}] " + "" + ), ) def cursorless_simple_scope_modifier(m) -> dict[str, Any]: """Containing scope, every scope, etc""" From 3f4b66ce7266c2d7c3dfe1e84c9b801d8b11070f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:15:29 +0000 Subject: [PATCH 12/17] Add test cases --- .../changeEveryFirstThreeTokens.yml | 30 ++++++++++++++++++ .../changeEveryLastThreeTokens.yml | 30 ++++++++++++++++++ .../changeEveryNextThreeTokens.yml | 31 +++++++++++++++++++ .../changeEveryPreviousThreeTokens.yml | 31 +++++++++++++++++++ .../relativeScopes/changeEveryThreeTokens.yml | 31 +++++++++++++++++++ .../changeEveryThreeTokensBackward.yml | 31 +++++++++++++++++++ 6 files changed, 184 insertions(+) create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml create mode 100644 packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml new file mode 100644 index 0000000000..5bf1c3fb76 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryFirstThreeTokens.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every first three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: 0 + length: 3 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml new file mode 100644 index 0000000000..0cbcecb0a5 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/ordinalScopes/changeEveryLastThreeTokens.yml @@ -0,0 +1,30 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every last three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: ordinalScope + scopeType: {type: token} + start: -3 + length: 3 + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml new file mode 100644 index 0000000000..0c7b8e4673 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryNextThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every next three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 3 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml new file mode 100644 index 0000000000..902ea42729 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryPreviousThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every previous three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 1 + length: 3 + direction: backward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml new file mode 100644 index 0000000000..162f94b5a7 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokens.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every three tokens + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: forward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: " ddd" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + - anchor: {line: 0, character: 2} + active: {line: 0, character: 2} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml new file mode 100644 index 0000000000..e14810748b --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/relativeScopes/changeEveryThreeTokensBackward.yml @@ -0,0 +1,31 @@ +languageId: plaintext +command: + version: 6 + spokenForm: change every three tokens backward + action: + name: clearAndSetSelection + target: + type: primitive + modifiers: + - type: relativeScope + scopeType: {type: token} + offset: 0 + length: 3 + direction: backward + isEvery: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa bbb ccc ddd + selections: + - anchor: {line: 0, character: 15} + active: {line: 0, character: 15} + marks: {} +finalState: + documentContents: "aaa " + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} From 42540c8bae5eb8949256345a7910b2ce209661c2 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:38:35 +0000 Subject: [PATCH 13/17] fix "every three scopes" --- .../modifiers/RelativeExclusiveScopeStage.ts | 47 ++++++------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 12e31b16f9..66390b6768 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -10,6 +10,7 @@ import type { ContainmentPolicy, ScopeHandler, } from "./scopeHandlers/scopeHandler.types"; +import { islice } from "itertools"; import { OutOfRangeError } from "./listUtils"; /** @@ -64,41 +65,21 @@ export class RelativeExclusiveScopeStage implements ModifierStage { ? "disallowed" : "disallowedIfStrict"; - const scopes: TargetScope[] = []; - let scopeCount = 0; - for (const scope of scopeHandler.generateScopes( - editor, - initialPosition, - direction, - { containment, skipAncestorScopes: true }, - )) { - scopeCount += 1; - - if (scopeCount < offset) { - // Skip until we hit `offset` - continue; - } - - if (scopeCount === offset) { - // When we hit offset, that becomes proximal scope - if (desiredScopeCount === 1) { - // Just return it if we only want 1 scope - return [scope]; - } - - scopes.push(scope); - - continue; - } - - if (scopeCount === offset + desiredScopeCount - 1) { - scopes.push(scope); + const scopes = Array.from( + islice( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment, + skipAncestorScopes: true, + }), + offset - 1, + offset + desiredScopeCount - 1, + ), + ); - // Then make a range when we get the desired number of scopes - return scopes; - } + if (scopes.length < desiredScopeCount) { + throw new OutOfRangeError(); } - throw new OutOfRangeError(); + return scopes; } } From 1a6800a35d3e8c0700805f12c2f037bd1ac4135e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:06:02 +0000 Subject: [PATCH 14/17] some cleanup --- .../modifiers/RelativeExclusiveScopeStage.ts | 52 ++++++------------- .../modifiers/RelativeInclusiveScopeStage.ts | 38 +++++--------- .../modifiers/scopesToTargets.ts | 35 +++++++++++++ 3 files changed, 64 insertions(+), 61 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 66390b6768..b4437fb258 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -2,16 +2,11 @@ import type { RelativeScopeModifier } from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; -import { TargetScope } from "./scopeHandlers/scope.types"; -import type { - ContainmentPolicy, - ScopeHandler, -} from "./scopeHandlers/scopeHandler.types"; +import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types"; import { islice } from "itertools"; -import { OutOfRangeError } from "./listUtils"; +import { scopesToTargets } from "./scopesToTargets"; /** * Handles relative modifiers that don't include targets intersecting with the @@ -36,22 +31,6 @@ export class RelativeExclusiveScopeStage implements ModifierStage { return runLegacy(this.modifierStageFactory, this.modifier, target); } - const scopes = this.getScopes(scopeHandler, target); - const { isReversed } = target; - - if (this.modifier.isEvery) { - return scopes.flatMap((scope) => scope.getTargets(isReversed)); - } - - // Then make a range when we get the desired number of scopes - return constructScopeRangeTarget( - isReversed, - scopes[0], - scopes[scopes.length - 1], - ); - } - - private getScopes(scopeHandler: ScopeHandler, target: Target): TargetScope[] { const { editor, contentRange: inputRange } = target; const { length: desiredScopeCount, direction, offset } = this.modifier; @@ -65,21 +44,20 @@ export class RelativeExclusiveScopeStage implements ModifierStage { ? "disallowed" : "disallowedIfStrict"; - const scopes = Array.from( - islice( - scopeHandler.generateScopes(editor, initialPosition, direction, { - containment, - skipAncestorScopes: true, - }), - offset - 1, - offset + desiredScopeCount - 1, - ), + const iter = islice( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment, + skipAncestorScopes: true, + }), + offset - 1, + offset + desiredScopeCount - 1, ); - if (scopes.length < desiredScopeCount) { - throw new OutOfRangeError(); - } - - return scopes; + return scopesToTargets( + iter, + this.modifier.length, + this.modifier.isEvery ?? false, + target.isReversed, + ); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index 570457c3f0..fccf2e9e54 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -5,12 +5,11 @@ import { import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { itake } from "itertools"; -import { OutOfRangeError } from "./listUtils"; +import { scopesToTargets } from "./scopesToTargets"; /** * Handles relative modifiers that include targets intersecting with the input, @@ -57,32 +56,23 @@ export class RelativeInclusiveScopeStage implements ModifierStage { throw new NoContainingScopeError(this.modifier.scopeType.type); } - const scopes = Array.from( - itake( - desiredScopeCount, - scopeHandler.generateScopes( - editor, - direction === "forward" ? initialRange.start : initialRange.end, - direction, - { - skipAncestorScopes: true, - }, - ), + const iter = itake( + desiredScopeCount, + scopeHandler.generateScopes( + editor, + direction === "forward" ? initialRange.start : initialRange.end, + direction, + { + skipAncestorScopes: true, + }, ), ); - if (scopes.length < desiredScopeCount) { - throw new OutOfRangeError(); - } - - if (this.modifier.isEvery) { - return scopes.flatMap((scope) => scope.getTargets(isReversed)); - } - - return constructScopeRangeTarget( + return scopesToTargets( + iter, + desiredScopeCount, + this.modifier.isEvery ?? false, isReversed, - scopes[0], - scopes[scopes.length - 1], ); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts new file mode 100644 index 0000000000..3073f6d8d5 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts @@ -0,0 +1,35 @@ +import type { Target } from "../../typings/target.types"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { OutOfRangeError } from "./listUtils"; + +/** + * Helper used by relative scope stages to construct the final target(s). + * + * @param scopeIter The scopes + * @param desiredScopeCount How many scopes were requested + * @param isEvery Whether to return scopes as a listor make a range + * @param isReversed Whether target should be reversed selection + * @returns Either a list of the input targets, or a range from first to last + */ +export function scopesToTargets( + scopeIter: Iterable, + desiredScopeCount: number, + isEvery: boolean, + isReversed: boolean, +): Target[] { + const scopes = Array.from(scopeIter); + + if (scopes.length < desiredScopeCount) { + throw new OutOfRangeError(); + } + + if (isEvery) { + return scopes.flatMap((scope) => scope.getTargets(isReversed)); + } + + return constructScopeRangeTarget( + isReversed, + scopes[0], + scopes[scopes.length - 1], + ); +} From c832112786e26083646039bb19d488bef0bbc658 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:17:46 +0000 Subject: [PATCH 15/17] Unify --- .../modifiers/RelativeExclusiveScopeStage.ts | 63 ------- .../modifiers/RelativeInclusiveScopeStage.ts | 78 --------- .../modifiers/RelativeScopeStage.ts | 159 +++++++++++++++--- .../modifiers/scopesToTargets.ts | 35 ---- 4 files changed, 132 insertions(+), 203 deletions(-) delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts deleted file mode 100644 index b4437fb258..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { RelativeScopeModifier } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; -import { ModifierStageFactory } from "../ModifierStageFactory"; -import type { ModifierStage } from "../PipelineStages.types"; -import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; -import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types"; -import { islice } from "itertools"; -import { scopesToTargets } from "./scopesToTargets"; - -/** - * Handles relative modifiers that don't include targets intersecting with the - * input, eg "next funk", "previous two tokens". Proceeds by running - * {@link ScopeHandler.generateScopes} to get the desired scopes, skipping the - * first scope if input range is empty and is at start of that scope. - */ -export class RelativeExclusiveScopeStage implements ModifierStage { - constructor( - private modifierStageFactory: ModifierStageFactory, - private scopeHandlerFactory: ScopeHandlerFactory, - private modifier: RelativeScopeModifier, - ) {} - - run(target: Target): Target[] { - const scopeHandler = this.scopeHandlerFactory.create( - this.modifier.scopeType, - target.editor.document.languageId, - ); - - if (scopeHandler == null) { - return runLegacy(this.modifierStageFactory, this.modifier, target); - } - - const { editor, contentRange: inputRange } = target; - const { length: desiredScopeCount, direction, offset } = this.modifier; - - const initialPosition = - direction === "forward" ? inputRange.end : inputRange.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 - ? "disallowed" - : "disallowedIfStrict"; - - const iter = islice( - scopeHandler.generateScopes(editor, initialPosition, direction, { - containment, - skipAncestorScopes: true, - }), - offset - 1, - offset + desiredScopeCount - 1, - ); - - return scopesToTargets( - iter, - this.modifier.length, - this.modifier.isEvery ?? false, - target.isReversed, - ); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts deleted file mode 100644 index fccf2e9e54..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - NoContainingScopeError, - RelativeScopeModifier, -} from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; -import { ModifierStageFactory } from "../ModifierStageFactory"; -import type { ModifierStage } from "../PipelineStages.types"; -import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; -import { runLegacy } from "./relativeScopeLegacy"; -import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; -import { itake } from "itertools"; -import { scopesToTargets } from "./scopesToTargets"; - -/** - * Handles relative modifiers that include targets intersecting with the input, - * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: - * - * 1. Constructs the initial range to use as the starting point for the relative - * scope search. Expands from the proximal end of the input target (start - * for "forward", end for "backward") to the smallest containing scope, - * breaking ties in direction {@link direction} rather than the tie-breaking - * heuristics we use for containing scope, to make this modifier easier to - * reason about when between scopes. - * 2. Calls {@link ScopeHandler.generateScopes} to get as many scopes as - * desired, starting from the proximal end of the initial range (ie the start - * if direction is "forward", the end if direction is "backward"). - */ -export class RelativeInclusiveScopeStage implements ModifierStage { - constructor( - private modifierStageFactory: ModifierStageFactory, - private scopeHandlerFactory: ScopeHandlerFactory, - private modifier: RelativeScopeModifier, - ) {} - - run(target: Target): Target[] { - const scopeHandler = this.scopeHandlerFactory.create( - this.modifier.scopeType, - target.editor.document.languageId, - ); - - if (scopeHandler == null) { - return runLegacy(this.modifierStageFactory, this.modifier, target); - } - - const { isReversed, editor, contentRange } = target; - const { length: desiredScopeCount, direction } = this.modifier; - - const initialRange = getPreferredScopeTouchingPosition( - scopeHandler, - editor, - direction === "forward" ? contentRange.start : contentRange.end, - direction, - )?.domain; - - if (initialRange == null) { - throw new NoContainingScopeError(this.modifier.scopeType.type); - } - - const iter = itake( - desiredScopeCount, - scopeHandler.generateScopes( - editor, - direction === "forward" ? initialRange.start : initialRange.end, - direction, - { - skipAncestorScopes: true, - }, - ), - ); - - return scopesToTargets( - iter, - desiredScopeCount, - this.modifier.isEvery ?? false, - isReversed, - ); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index abe3fb0095..7d3a34c9d4 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,40 +1,145 @@ -import type { RelativeScopeModifier } from "@cursorless/common"; +import { + NoContainingScopeError, + type RelativeScopeModifier, +} from "@cursorless/common"; import type { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import { RelativeExclusiveScopeStage } from "./RelativeExclusiveScopeStage"; -import { RelativeInclusiveScopeStage } from "./RelativeInclusiveScopeStage"; +import { runLegacy } from "./relativeScopeLegacy"; import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; +import { TargetScope } from "./scopeHandlers/scope.types"; +import type { + ContainmentPolicy, + ScopeHandler, +} from "./scopeHandlers/scopeHandler.types"; +import { islice, itake } from "itertools"; +import { OutOfRangeError } from "./listUtils"; +import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; +import { getPreferredScopeTouchingPosition } from "./getPreferredScopeTouchingPosition"; /** - * Implements relative scope modifiers like "next funk", "two tokens", etc. - * Proceeds by determining whether the modifier wants to include the scope(s) - * touching the input target (ie if {@link modifier.offset} is 0), and then - * delegating to {@link RelativeInclusiveScopeStage} if so, or - * {@link RelativeExclusiveScopeStage} if not. + * Handles relative modifiers input, eg "next funk", "two funks", "previous two + * tokens". Proceeds by running {@link ScopeHandler.generateScopes} to get the + * desired scopes, skipping the first scope if input range is empty and is at + * start of that scope. */ export class RelativeScopeStage implements ModifierStage { - private modiferStage: ModifierStage; constructor( - modifierStageFactory: ModifierStageFactory, - scopeHandlerFactory: ScopeHandlerFactory, - modifier: RelativeScopeModifier, - ) { - this.modiferStage = - modifier.offset === 0 - ? new RelativeInclusiveScopeStage( - modifierStageFactory, - scopeHandlerFactory, - modifier, - ) - : new RelativeExclusiveScopeStage( - modifierStageFactory, - scopeHandlerFactory, - modifier, - ); - } + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + protected modifier: RelativeScopeModifier, + ) {} run(target: Target): Target[] { - return this.modiferStage.run(target); + const scopeHandler = this.scopeHandlerFactory.create( + this.modifier.scopeType, + target.editor.document.languageId, + ); + + if (scopeHandler == null) { + return runLegacy(this.modifierStageFactory, this.modifier, target); + } + + const scopes = Array.from( + this.modifier.offset === 0 + ? generateScopesInclusive(scopeHandler, target, this.modifier) + : generateScopesExclusive(scopeHandler, target, this.modifier), + ); + + if (scopes.length < this.modifier.length) { + throw new OutOfRangeError(); + } + + const { isReversed } = target; + + if (this.modifier.isEvery) { + return scopes.flatMap((scope) => scope.getTargets(isReversed)); + } + + return constructScopeRangeTarget( + isReversed, + scopes[0], + scopes[scopes.length - 1], + ); + } +} + +/** + * Handles relative modifiers that include targets intersecting with the input, + * eg `"two funks"`, `"token backward"`, etc. Proceeds as follows: + * + * 1. Constructs the initial range to use as the starting point for the relative + * scope search. Expands from the proximal end of the input target (start + * for "forward", end for "backward") to the smallest containing scope, + * breaking ties in direction {@link direction} rather than the tie-breaking + * heuristics we use for containing scope, to make this modifier easier to + * reason about when between scopes. + * 2. Calls {@link ScopeHandler.generateScopes} to get as many scopes as + * desired, starting from the proximal end of the initial range (ie the start + * if direction is "forward", the end if direction is "backward"). + */ +function generateScopesInclusive( + scopeHandler: ScopeHandler, + target: Target, + modifier: RelativeScopeModifier, +): Iterable { + const { editor, contentRange } = target; + const { length: desiredScopeCount, direction } = modifier; + + const initialRange = getPreferredScopeTouchingPosition( + scopeHandler, + editor, + direction === "forward" ? contentRange.start : contentRange.end, + direction, + )?.domain; + + if (initialRange == null) { + throw new NoContainingScopeError(modifier.scopeType.type); } + + return itake( + desiredScopeCount, + scopeHandler.generateScopes( + editor, + direction === "forward" ? initialRange.start : initialRange.end, + direction, + { + skipAncestorScopes: true, + }, + ), + ); +} + +/** + * Handles relative modifiers that don't include targets intersecting with the + * input, eg "next funk", "previous two tokens". Proceeds by running + * {@link ScopeHandler.generateScopes} to get the desired scopes, skipping the + * first scope if input range is empty and is at start of that scope. + */ +function generateScopesExclusive( + scopeHandler: ScopeHandler, + target: Target, + modifier: RelativeScopeModifier, +): Iterable { + const { editor, contentRange: inputRange } = target; + const { length: desiredScopeCount, direction, offset } = modifier; + + const initialPosition = + direction === "forward" ? inputRange.end : inputRange.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 + ? "disallowed" + : "disallowedIfStrict"; + + return islice( + scopeHandler.generateScopes(editor, initialPosition, direction, { + containment, + skipAncestorScopes: true, + }), + offset - 1, + offset + desiredScopeCount - 1, + ); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts deleted file mode 100644 index 3073f6d8d5..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopesToTargets.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Target } from "../../typings/target.types"; -import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; -import { OutOfRangeError } from "./listUtils"; - -/** - * Helper used by relative scope stages to construct the final target(s). - * - * @param scopeIter The scopes - * @param desiredScopeCount How many scopes were requested - * @param isEvery Whether to return scopes as a listor make a range - * @param isReversed Whether target should be reversed selection - * @returns Either a list of the input targets, or a range from first to last - */ -export function scopesToTargets( - scopeIter: Iterable, - desiredScopeCount: number, - isEvery: boolean, - isReversed: boolean, -): Target[] { - const scopes = Array.from(scopeIter); - - if (scopes.length < desiredScopeCount) { - throw new OutOfRangeError(); - } - - if (isEvery) { - return scopes.flatMap((scope) => scope.getTargets(isReversed)); - } - - return constructScopeRangeTarget( - isReversed, - scopes[0], - scopes[scopes.length - 1], - ); -} From cb1e7c40f05c98d4a3b145d29feb1bc25cca90ca Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:33:23 +0000 Subject: [PATCH 16/17] Fix / update cheatsheet --- .../src/cheatsheet/sections/modifiers.py | 81 +++++++++++++------ .../lib/sampleSpokenFormInfos/defaults.json | 72 ++++++++++++++--- 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/cursorless-talon/src/cheatsheet/sections/modifiers.py b/cursorless-talon/src/cheatsheet/sections/modifiers.py index 55d3f396a3..af8164d312 100644 --- a/cursorless-talon/src/cheatsheet/sections/modifiers.py +++ b/cursorless-talon/src/cheatsheet/sections/modifiers.py @@ -1,10 +1,14 @@ +from itertools import chain +from typing import TypedDict + from ..get_list import get_raw_list, make_dict_readable MODIFIER_LIST_NAMES = [ "simple_modifier", "interior_modifier", "head_tail_modifier", - "simple_scope_modifier", + "every_scope_modifier", + "ancestor_scope_modifier", "first_modifier", "last_modifier", "previous_next_modifier", @@ -132,10 +136,6 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['next']} ", "description": " instance of after target", }, - { - "spokenForm": f"{complex_modifiers['previous']} s", - "description": "previous instances of ", - }, { "spokenForm": f" {complex_modifiers['backward']}", "description": "single instance of including target, going backwards", @@ -144,18 +144,25 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['forward']}", "description": "single instance of including target, going forwards", }, - { - "spokenForm": f" s {complex_modifiers['backward']}", - "description": " instances of including target, going backwards", - }, - { - "spokenForm": " s", - "description": " instances of including target, going forwards", - }, - { - "spokenForm": f"{complex_modifiers['next']} s", - "description": "next instances of ", - }, + *generateOptionalEvery( + complex_modifiers["every"], + { + "spokenForm": f" s {complex_modifiers['backward']}", + "description": " instances of including target, going backwards", + }, + { + "spokenForm": " s", + "description": " instances of including target, going forwards", + }, + { + "spokenForm": f"{complex_modifiers['previous']} s", + "description": "previous instances of ", + }, + { + "spokenForm": f"{complex_modifiers['next']} s", + "description": "next instances of ", + }, + ), ], }, { @@ -170,14 +177,40 @@ def get_modifiers(): "spokenForm": f" {complex_modifiers['last']} ", "description": "-to-last instance of in iteration scope", }, + *generateOptionalEvery( + complex_modifiers["every"], + { + "spokenForm": f"{complex_modifiers['first']} s", + "description": "first instances of in iteration scope", + }, + { + "spokenForm": f"{complex_modifiers['last']} s", + "description": "last instances of in iteration scope", + }, + ), + ], + }, + ] + + +class Entry(TypedDict): + spokenForm: str + description: str + + +def generateOptionalEvery(every: str, *entries: Entry) -> list[Entry]: + return list( + chain.from_iterable( + [ { - "spokenForm": f"{complex_modifiers['first']} s", - "description": "First instances of in iteration scope", + "spokenForm": entry["spokenForm"], + "description": f"{entry['description']}, as contiguous range", }, { - "spokenForm": f"{complex_modifiers['last']} s", - "description": "Last instances of in iteration scope", + "spokenForm": f"{every} {entry['spokenForm']}", + "description": f"{entry['description']}, as individual targets", }, - ], - }, - ] + ] + for entry in entries + ) + ) diff --git a/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json b/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json index f70df30696..a4a808d164 100644 --- a/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json +++ b/packages/cheatsheet/src/lib/sampleSpokenFormInfos/defaults.json @@ -68,6 +68,16 @@ } ] }, + { + "id": "decrement", + "type": "action", + "variations": [ + { + "spokenForm": "decrement ", + "description": "Decrement" + } + ] + }, { "id": "deselect", "type": "action", @@ -168,6 +178,16 @@ } ] }, + { + "id": "increment", + "type": "action", + "variations": [ + { + "spokenForm": "increment ", + "description": "Increment" + } + ] + }, { "id": "indentLine", "type": "action", @@ -710,6 +730,16 @@ "name": "Modifiers", "id": "modifiers", "items": [ + { + "id": "ancestor", + "type": "modifier", + "variations": [ + { + "spokenForm": "grand ", + "description": "Grandparent containing instance of " + } + ] + }, { "id": "containingScope", "type": "modifier", @@ -842,11 +872,19 @@ }, { "spokenForm": "first s", - "description": "First instances of in iteration scope" + "description": "first instances of in iteration scope, as contiguous range" + }, + { + "spokenForm": "every first s", + "description": "first instances of in iteration scope, as individual targets" }, { "spokenForm": "last s", - "description": "Last instances of in iteration scope" + "description": "last instances of in iteration scope, as contiguous range" + }, + { + "spokenForm": "every last s", + "description": "last instances of in iteration scope, as individual targets" } ] }, @@ -870,10 +908,6 @@ "spokenForm": " next ", "description": " instance of after target" }, - { - "spokenForm": "previous s", - "description": "previous instances of " - }, { "spokenForm": " backward", "description": "single instance of including target, going backwards" @@ -884,15 +918,35 @@ }, { "spokenForm": " s backward", - "description": " instances of including target, going backwards" + "description": " instances of including target, going backwards, as contiguous range" + }, + { + "spokenForm": "every s backward", + "description": " instances of including target, going backwards, as individual targets" }, { "spokenForm": " s", - "description": " instances of including target, going forwards" + "description": " instances of including target, going forwards, as contiguous range" + }, + { + "spokenForm": "every s", + "description": " instances of including target, going forwards, as individual targets" + }, + { + "spokenForm": "previous s", + "description": "previous instances of , as contiguous range" + }, + { + "spokenForm": "every previous s", + "description": "previous instances of , as individual targets" }, { "spokenForm": "next s", - "description": "next instances of " + "description": "next instances of , as contiguous range" + }, + { + "spokenForm": "every next s", + "description": "next instances of , as individual targets" } ] }, From 5ba078aff4cbc10d01508fd49a2f91899ecc727b Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:07:50 +0000 Subject: [PATCH 17/17] tweak --- .../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 7d3a34c9d4..0bd7a14cbb 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -27,7 +27,7 @@ export class RelativeScopeStage implements ModifierStage { constructor( private modifierStageFactory: ModifierStageFactory, private scopeHandlerFactory: ScopeHandlerFactory, - protected modifier: RelativeScopeModifier, + private modifier: RelativeScopeModifier, ) {} run(target: Target): Target[] {