diff --git a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts index 3681f9ca3f..7cc634d1fa 100644 --- a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts +++ b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts @@ -19,6 +19,7 @@ import { ImplicitStage } from "./marks/ImplicitStage"; import { ContainingTokenIfUntypedEmptyStage } from "./modifiers/ConditionalModifierStages"; import { PlainTarget } from "./targets"; import { uniqWithHash } from "../util/uniqWithHash"; +import { createContinuousRangeTarget } from "./createContinuousRangeTarget"; export class TargetPipelineRunner { constructor( @@ -315,8 +316,9 @@ export function targetsToContinuousTarget( const excludeStart = isReversed ? excludeActive : excludeAnchor; const excludeEnd = isReversed ? excludeAnchor : excludeActive; - return startTarget.createContinuousRangeTarget( + return createContinuousRangeTarget( isReversed, + startTarget, endTarget, !excludeStart, !excludeEnd, diff --git a/packages/cursorless-engine/src/processTargets/createContinuousRangeTarget.ts b/packages/cursorless-engine/src/processTargets/createContinuousRangeTarget.ts new file mode 100644 index 0000000000..e9ef40eb8f --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/createContinuousRangeTarget.ts @@ -0,0 +1,47 @@ +import { Target } from "../typings/target.types"; +import { + createContinuousRange, + createContinuousRangeUntypedTarget, +} from "./targetUtil/createContinuousRange"; +import { PlainTarget } from "./targets"; + +export function createContinuousRangeTarget( + isReversed: boolean, + startTarget: Target, + endTarget: Target, + includeStart: boolean, + includeEnd: boolean, +): Target { + const richTarget = startTarget.maybeCreateRichRangeTarget( + isReversed, + endTarget, + includeStart, + includeEnd, + ); + + if (richTarget != null) { + return richTarget; + } + + if (!includeStart || !includeEnd) { + return new PlainTarget({ + editor: startTarget.editor, + contentRange: createContinuousRange( + startTarget, + endTarget, + includeStart, + includeEnd, + ), + isReversed, + isToken: false, + }); + } + + return createContinuousRangeUntypedTarget( + isReversed, + startTarget, + endTarget, + includeStart, + includeEnd, + ); +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts index 170b8c157d..b3fc409ea5 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/constructScopeRangeTarget.ts @@ -1,4 +1,5 @@ import { Target } from "../../typings/target.types"; +import { createContinuousRangeTarget } from "../createContinuousRangeTarget"; import { TargetScope } from "./scopeHandlers/scope.types"; /** @@ -41,6 +42,6 @@ export function constructScopeRangeTarget( : [target2, target1]; return [ - startTarget.createContinuousRangeTarget(isReversed, endTarget, true, true), + createContinuousRangeTarget(isReversed, startTarget, endTarget, true, true), ]; } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index 547029eb02..c3439a88cd 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -1,6 +1,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() { @@ -32,8 +33,9 @@ export function createRangeTargetFromIndices( return targets[startIndex]; } - return targets[startIndex].createContinuousRangeTarget( + return createContinuousRangeTarget( isReversed, + targets[startIndex], targets[endIndex], true, true, diff --git a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts index 575857d5c4..23c87c9ce2 100644 --- a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts @@ -14,10 +14,7 @@ import { isEqual } from "lodash"; import type { EditWithRangeUpdater } from "../../typings/Types"; import type { Destination, Target } from "../../typings/target.types"; import { isSameType } from "../../util/typeUtils"; -import { - createContinuousRange, - createContinuousRangeUntypedTarget, -} from "../targetUtil/createContinuousRange"; +import { createContinuousRange } from "../targetUtil/createContinuousRange"; import { DestinationImpl } from "./DestinationImpl"; /** Parameters supported by all target classes */ @@ -129,34 +126,30 @@ export abstract class BaseTarget< protected abstract getCloneParameters(): EnforceUndefined; - createContinuousRangeTarget( + maybeCreateRichRangeTarget( isReversed: boolean, endTarget: Target, includeStart: boolean, includeEnd: boolean, - ): Target { - if (isSameType(this, endTarget)) { - const constructor = Object.getPrototypeOf(this).constructor; - - return new constructor({ - ...this.getCloneParameters(), - isReversed, - contentRange: createContinuousRange( - this, - endTarget, - includeStart, - includeEnd, - ), - }); + ): Target | undefined { + if (!includeStart || !includeEnd || !isSameType(this, endTarget)) { + return undefined; } - return createContinuousRangeUntypedTarget( + return this.createRichRangeTarget(isReversed, endTarget); + } + + protected createRichRangeTarget( + isReversed: boolean, + endTarget: ThisType & Target, + ): ThisType & Target { + const { constructor } = Object.getPrototypeOf(this); + + return new constructor({ + ...this.getCloneParameters(), isReversed, - this, - endTarget, - includeStart, - includeEnd, - ); + contentRange: createContinuousRange(this, endTarget, true, true), + }); } isEqual(otherTarget: Target): boolean { diff --git a/packages/cursorless-engine/src/processTargets/targets/InteriorTarget.ts b/packages/cursorless-engine/src/processTargets/targets/InteriorTarget.ts index f5464f429b..97c88535a4 100644 --- a/packages/cursorless-engine/src/processTargets/targets/InteriorTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/InteriorTarget.ts @@ -1,12 +1,7 @@ import { Range } from "@cursorless/common"; import { BaseTarget, MinimumTargetParameters } from "."; -import { Target } from "../../typings/target.types"; import { shrinkRangeToFitContent } from "../../util/selectionUtils"; -import { isSameType } from "../../util/typeUtils"; -import { - createContinuousRangeFromRanges, - createContinuousRangeUntypedTarget, -} from "../targetUtil/createContinuousRange"; +import { createContinuousRangeFromRanges } from "../targetUtil/createContinuousRange"; export interface InteriorTargetParameters extends MinimumTargetParameters { readonly fullInteriorRange: Range; @@ -39,33 +34,19 @@ export class InteriorTarget extends BaseTarget { }; } - createContinuousRangeTarget( + createRichRangeTarget( isReversed: boolean, - endTarget: Target, - includeStart: boolean, - includeEnd: boolean, - ): Target { - if (isSameType(this, endTarget)) { - const constructor = Object.getPrototypeOf(this).constructor; - - return new constructor({ - ...this.getCloneParameters(), - isReversed, - fullInteriorRange: createContinuousRangeFromRanges( - this.fullInteriorRange, - endTarget.fullInteriorRange, - includeStart, - includeEnd, - ), - }); - } - - return createContinuousRangeUntypedTarget( + endTarget: InteriorTarget, + ): InteriorTarget { + return new InteriorTarget({ + ...this.getCloneParameters(), isReversed, - this, - endTarget, - includeStart, - includeEnd, - ); + fullInteriorRange: createContinuousRangeFromRanges( + this.fullInteriorRange, + endTarget.fullInteriorRange, + true, + true, + ), + }); } } diff --git a/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts b/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts index 582fdbeaba..7907f54ad8 100644 --- a/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/LineTarget.ts @@ -42,12 +42,12 @@ export class LineTarget extends BaseTarget { getRemovalHighlightRange = () => this.fullLineContentRange; - createContinuousRangeTarget( + maybeCreateRichRangeTarget( isReversed: boolean, endTarget: Target, includeStart: boolean, includeEnd: boolean, - ): Target { + ): Target | undefined { if (endTarget.isLine) { return new LineTarget({ editor: this.editor, @@ -61,12 +61,7 @@ export class LineTarget extends BaseTarget { }); } - return super.createContinuousRangeTarget( - isReversed, - endTarget, - includeStart, - includeEnd, - ); + return undefined; } protected getCloneParameters() { diff --git a/packages/cursorless-engine/src/processTargets/targets/ParagraphTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ParagraphTarget.ts index 50fcdad16a..b2392bd496 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ParagraphTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ParagraphTarget.ts @@ -73,12 +73,12 @@ export class ParagraphTarget extends BaseTarget { : this.fullLineContentRange; } - createContinuousRangeTarget( + maybeCreateRichRangeTarget( isReversed: boolean, endTarget: Target, includeStart: boolean, includeEnd: boolean, - ): Target { + ): Target | undefined { if (isSameType(this, endTarget)) { return new ParagraphTarget({ ...this.getCloneParameters(), @@ -105,12 +105,7 @@ export class ParagraphTarget extends BaseTarget { }); } - return super.createContinuousRangeTarget( - isReversed, - endTarget, - includeStart, - includeEnd, - ); + return undefined; } protected getCloneParameters() { diff --git a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts index a90b2ba186..50cf6e316e 100644 --- a/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/ScopeTypeTarget.ts @@ -99,47 +99,44 @@ export class ScopeTypeTarget extends BaseTarget { : getTokenRemovalRange(this); } - createContinuousRangeTarget( + maybeCreateRichRangeTarget( isReversed: boolean, endTarget: Target, includeStart: boolean, includeEnd: boolean, - ): Target { - if (isSameType(this, endTarget)) { - const scopeTarget = endTarget; - if (this.scopeTypeType_ === scopeTarget.scopeTypeType_) { - const contentRemovalRange = - this.removalRange_ != null || scopeTarget.removalRange_ != null - ? createContinuousRangeFromRanges( - this.removalRange_ ?? this.contentRange, - scopeTarget.removalRange_ ?? scopeTarget.contentRange, - includeStart, - includeEnd, - ) - : undefined; + ): Target | undefined { + if ( + !includeStart || + !includeEnd || + !isSameType(this, endTarget) || + this.scopeTypeType_ !== endTarget.scopeTypeType_ + ) { + return undefined; + } - return new ScopeTypeTarget({ - ...this.getCloneParameters(), - isReversed, - leadingDelimiterRange: this.leadingDelimiterRange_, - trailingDelimiterRange: scopeTarget.trailingDelimiterRange_, - removalRange: contentRemovalRange, - contentRange: createContinuousRange( - this, - endTarget, + const contentRemovalRange = + this.removalRange_ != null || endTarget.removalRange_ != null + ? createContinuousRangeFromRanges( + this.removalRange_ ?? this.contentRange, + endTarget.removalRange_ ?? endTarget.contentRange, includeStart, includeEnd, - ), - }); - } - } + ) + : undefined; - return super.createContinuousRangeTarget( + return new ScopeTypeTarget({ + ...this.getCloneParameters(), isReversed, - endTarget, - includeStart, - includeEnd, - ); + leadingDelimiterRange: this.leadingDelimiterRange_, + trailingDelimiterRange: endTarget.trailingDelimiterRange_, + removalRange: contentRemovalRange, + contentRange: createContinuousRange( + this, + endTarget, + includeStart, + includeEnd, + ), + }); } protected getCloneParameters() { diff --git a/packages/cursorless-engine/src/processTargets/targets/SubTokenWordTarget.ts b/packages/cursorless-engine/src/processTargets/targets/SubTokenWordTarget.ts index 26b10d5d45..224b4d3628 100644 --- a/packages/cursorless-engine/src/processTargets/targets/SubTokenWordTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/SubTokenWordTarget.ts @@ -1,8 +1,6 @@ import { Range } from "@cursorless/common"; import { BaseTarget, CommonTargetParameters } from "."; -import { Target } from "../../typings/target.types"; import { tryConstructPlainTarget } from "../../util/tryConstructTarget"; -import { isSameType } from "../../util/typeUtils"; import { createContinuousRange } from "../targetUtil/createContinuousRange"; import { getDelimitedSequenceRemovalRange } from "../targetUtil/insertionRemovalBehaviors/DelimitedSequenceInsertionRemovalBehavior"; @@ -47,32 +45,16 @@ export class SubTokenWordTarget extends BaseTarget { return getDelimitedSequenceRemovalRange(this); } - createContinuousRangeTarget( + createRichRangeTarget( isReversed: boolean, - endTarget: Target, - includeStart: boolean, - includeEnd: boolean, - ): Target { - if (isSameType(this, endTarget)) { - return new SubTokenWordTarget({ - ...this.getCloneParameters(), - isReversed, - contentRange: createContinuousRange( - this, - endTarget, - includeStart, - includeEnd, - ), - trailingDelimiterRange: endTarget.trailingDelimiterRange_, - }); - } - - return super.createContinuousRangeTarget( + endTarget: SubTokenWordTarget, + ): SubTokenWordTarget { + return new SubTokenWordTarget({ + ...this.getCloneParameters(), isReversed, - endTarget, - includeStart, - includeEnd, - ); + contentRange: createContinuousRange(this, endTarget, true, true), + trailingDelimiterRange: endTarget.trailingDelimiterRange_, + }); } protected getCloneParameters() { diff --git a/packages/cursorless-engine/src/processTargets/targets/UntypedTarget.ts b/packages/cursorless-engine/src/processTargets/targets/UntypedTarget.ts index 631c6baf56..187609e027 100644 --- a/packages/cursorless-engine/src/processTargets/targets/UntypedTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/UntypedTarget.ts @@ -1,7 +1,6 @@ import { Range } from "@cursorless/common"; import { BaseTarget, CommonTargetParameters } from "."; import type { Target } from "../../typings/target.types"; -import { createContinuousRangeUntypedTarget } from "../targetUtil/createContinuousRange"; import { getTokenLeadingDelimiterTarget, getTokenRemovalRange, @@ -42,19 +41,8 @@ export class UntypedTarget extends BaseTarget { : getTokenRemovalRange(this); } - createContinuousRangeTarget( - isReversed: boolean, - endTarget: Target, - includeStart: boolean, - includeEnd: boolean, - ): Target { - return createContinuousRangeUntypedTarget( - isReversed, - this, - endTarget, - includeStart, - includeEnd, - ); + maybeCreateRichRangeTarget(): undefined { + return undefined; } protected getCloneParameters() { diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index c0a34f0606..5eaaa0ed78 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -152,12 +152,24 @@ export interface Target { getRemovalHighlightRange(): Range; withThatTarget(thatTarget: Target): Target; withContentRange(contentRange: Range): Target; - createContinuousRangeTarget( + /** + * Attempt to create a range target that preserves some of the semantics of + * this target. Most targets will return `undefined` for targets not of the + * same type, and for targets of the same type, inherit some of the args from + * the two targets. Trailing delimiter should come from end target, leading + * from start target, etc. + * + * Note that you likely don't want to call this function directly; it is + * designed to be used by {@link createContinuousRangeTarget}. + * @param isReversed + * @param endTarget + */ + maybeCreateRichRangeTarget( isReversed: boolean, endTarget: Target, includeStart: boolean, includeEnd: boolean, - ): Target; + ): Target | undefined; /** Constructs removal edit */ constructRemovalEdit(): EditWithRangeUpdater; isEqual(target: Target): boolean; diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/subtoken/chuckSecondUntilFourthWord.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/subtoken/chuckSecondUntilFourthWord.yml new file mode 100644 index 0000000000..875414cc40 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/subtoken/chuckSecondUntilFourthWord.yml @@ -0,0 +1,34 @@ +languageId: plaintext +command: + version: 6 + spokenForm: chuck second until fourth word + action: + name: remove + target: + type: primitive + modifiers: + - type: range + anchor: + type: ordinalScope + scopeType: {type: word} + start: 1 + length: 1 + active: + type: ordinalScope + scopeType: {type: word} + start: 3 + length: 1 + excludeAnchor: false + excludeActive: true + usePrePhraseSnapshot: true +initialState: + documentContents: aaa_bbb_ccc_ddd_eee + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: aaa_ddd_eee + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0}