From 14c42e3e41b300d132177b4ff22b5296ecff950f Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Thu, 25 May 2023 14:05:10 +0100 Subject: [PATCH] Generalize "every" range hoisting code --- .../command/PartialTargetDescriptor.types.ts | 8 +- .../core/commandRunner/CommandRunnerImpl.ts | 10 +- .../src/core/handleHoistedModifiers.ts | 162 ++++++++++++++++++ .../src/core/inferFullTargets.ts | 104 +---------- .../src/processTargets/MarkStageFactory.ts | 2 +- .../processTargets/MarkStageFactoryImpl.ts | 13 +- .../processTargets/TargetPipelineRunner.ts | 158 +++++++---------- .../processTargets/marks/TargetMarkStage.ts | 15 ++ packages/cursorless-engine/src/runCommand.ts | 14 +- .../src/typings/TargetDescriptor.ts | 59 +++---- 10 files changed, 302 insertions(+), 243 deletions(-) create mode 100644 packages/cursorless-engine/src/core/handleHoistedModifiers.ts create mode 100644 packages/cursorless-engine/src/processTargets/marks/TargetMarkStage.ts diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index c575f36a1a..849077a3a5 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -37,13 +37,13 @@ export interface LineNumberMark { */ export interface RangeMark { type: "range"; - anchor: Mark; - active: Mark; + anchor: PartialMark; + active: PartialMark; excludeAnchor?: boolean; excludeActive?: boolean; } -export type Mark = +export type PartialMark = | CursorMark | ThatMark | SourceMark @@ -275,7 +275,7 @@ export interface PositionModifier { export interface PartialPrimitiveTargetDescriptor { type: "primitive"; - mark?: Mark; + mark?: PartialMark; modifiers?: Modifier[]; } diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index d050602194..e41f79e770 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -49,10 +49,12 @@ export class CommandRunnerImpl implements CommandRunner { const action = this.actions[actionName]; - const targets = this.pipelineRunner.run( - targetDescriptors, - action.getPrePositionStages?.(...actionArgs) ?? [], - action.getFinalStages?.(...actionArgs) ?? [], + const prePositionStages = + action.getPrePositionStages?.(...actionArgs) ?? []; + const finalStages = action.getFinalStages?.(...actionArgs) ?? []; + + const targets = targetDescriptors.map((targetDescriptor) => + this.pipelineRunner.run(targetDescriptor, prePositionStages, finalStages), ); const { diff --git a/packages/cursorless-engine/src/core/handleHoistedModifiers.ts b/packages/cursorless-engine/src/core/handleHoistedModifiers.ts new file mode 100644 index 0000000000..8fbbb3e35f --- /dev/null +++ b/packages/cursorless-engine/src/core/handleHoistedModifiers.ts @@ -0,0 +1,162 @@ +import { Modifier } from "@cursorless/common"; +import produce from "immer"; +import { findLastIndex } from "lodash"; +import { + PrimitiveTargetDescriptor, + RangeTargetDescriptor, +} from "../typings/TargetDescriptor"; + +/** + * This function exists to enable hoisted modifiers, eg for constructs like + * "every line air past bat". When we detect a range target which has a hoisted + * modifier on its anchor, we do the following: + * + * 1. Split the anchor's modifier chain, so that everything after the hoisted + * modifier remains on the anchor, and we reserve the remaining modifiers. + * 2. We remove everything before and including the hoisted modifier from the + * active, if it ended up there via inference. + * 3. We modify the range target if required by the hoisted modifier. For + * example "every" ranges need to handle endpoint exclusion carefully. + * 4. We construct a new {@link TargetMark} that emits the output of the range + * target, and move the reserved modifiers to a new primitive target which + * starts with this mark. + * + * We effectively break the chain into two parts, one that gets distributed over + * anchor and active, and is applied before constructing the range, and one that + * is run with the range as its input. For example: + * + * ``` + * "first token every line funk air past bat" + * ``` + * + * In this case, we create an "every" range target with anchor `"funk air"` and + * active `"funk bat"`. We then apply the modifier `"first token"` to the + * resulting range. + * + * @param targetDescriptor The full range target, post-inference + * @param isAnchorMarkImplicit `true` if the anchor mark was implicit on the + * original partial target + * @returns A new target descriptor which consists of a primitive target with a + * mark that emits the output of the range target, with the hoisted modifiers + * applied to it. + */ +export function handleHoistedModifiers( + targetDescriptor: RangeTargetDescriptor, + isAnchorMarkImplicit: boolean, +): PrimitiveTargetDescriptor | RangeTargetDescriptor { + const { anchor, rangeType, active } = targetDescriptor; + + if (anchor.type !== "primitive" || rangeType !== "continuous") { + return targetDescriptor; + } + + const indexedModifiers = anchor.modifiers.map((v, i) => [v, i] as const); + + // We iterate through the modifiers in reverse because the closest hoisted + // modifier to the range owns the range. For example if you say "every line + // every funk air past bat", the "every line" owns the range, and the "every + // funk" is applied to the output. + for (const [modifier, idx] of indexedModifiers.reverse()) { + for (const hoistedModifierType of hoistedModifierTypes) { + const acceptanceInfo = hoistedModifierType.accept(modifier); + if (acceptanceInfo.accepted) { + // We hoist the modifier and everything that comes before it. Every + // modifier that comes after it is left on the anchor (and left on the + // the active if it ended up there via inference from the anchor) + const [hoistedModifiers, unhoistedModifiers] = [ + anchor.modifiers.slice(0, idx + 1), + anchor.modifiers.slice(idx + 1), + ]; + + /** + * The input range target, transformed by removing the hoisted modifiers + * from anchor and active, and applying any required transformations + * from the hoisted modifier. + */ + let pipelineInputDescriptor: RangeTargetDescriptor = { + ...targetDescriptor, + anchor: + // If they say "every line past bat", the anchor is implicit, even though + // it comes across the wire as a primitive target due to the "every line", + // which we've now removed + unhoistedModifiers.length === 0 && isAnchorMarkImplicit + ? { type: "implicit" } + : { + type: "primitive", + mark: anchor.mark, + positionModifier: undefined, + modifiers: unhoistedModifiers, + }, + // Remove the hoisted modifier (and everything before it) from the + // active if it ended up there from inference + active: produce(active, (draft) => { + draft.modifiers = draft.modifiers.slice( + findLastIndex( + draft.modifiers, + (modifier) => hoistedModifierType.accept(modifier).accepted, + ) + 1, + ); + }), + }; + + pipelineInputDescriptor = + acceptanceInfo.transformTarget?.(pipelineInputDescriptor) ?? + pipelineInputDescriptor; + + // We create a new primitive target which starts with the output of the + // range target, and has the hoisted modifiers on it + return { + type: "primitive", + mark: { + type: "target", + target: pipelineInputDescriptor, + }, + positionModifier: anchor.positionModifier, + modifiers: hoistedModifiers, + }; + } + } + } + + return targetDescriptor; +} + +interface HoistedModifierAcceptanceInfo { + accepted: boolean; + + /** + * If the modifier is accepted, this function is called to transform the + * input range target. For example, "every" ranges need to handle endpoint + * exclusion carefully. + * @param target The input range target + */ + transformTarget?(target: RangeTargetDescriptor): RangeTargetDescriptor; +} + +interface HoistedModifierType { + accept(modifier: Modifier): HoistedModifierAcceptanceInfo; +} + +/** + * These modifiers need to be "hoisted" past range targets, ie we run them + * after running the range, rather than distributing them across anchor and + * active, the way we do with all other modifiers. + */ +const hoistedModifierTypes: HoistedModifierType[] = [ + // "every" ranges, eg "every line air past bat" + { + accept(modifier: Modifier) { + return modifier.type === "everyScope" + ? { + accepted: true, + transformTarget(target: RangeTargetDescriptor) { + return { + ...target, + exclusionScopeType: modifier.scopeType, + }; + }, + } + : { accepted: false }; + }, + }, +]; diff --git a/packages/cursorless-engine/src/core/inferFullTargets.ts b/packages/cursorless-engine/src/core/inferFullTargets.ts index b7b82dfc0b..73174e45b2 100644 --- a/packages/cursorless-engine/src/core/inferFullTargets.ts +++ b/packages/cursorless-engine/src/core/inferFullTargets.ts @@ -1,7 +1,5 @@ import { - EveryScopeModifier, ImplicitTargetDescriptor, - Mark, Modifier, PartialListTargetDescriptor, PartialPrimitiveTargetDescriptor, @@ -10,13 +8,12 @@ import { PositionModifier, } from "@cursorless/common"; import { - EveryRangeTargetDescriptor, + Mark, PrimitiveTargetDescriptor, RangeTargetDescriptor, TargetDescriptor, } from "../typings/TargetDescriptor"; -import { findLastIndex } from "lodash"; -import produce from "immer"; +import { handleHoistedModifiers } from "./handleHoistedModifiers"; /** * Performs inference on the partial targets provided by the user, using @@ -80,7 +77,7 @@ function inferNonListTarget( function inferRangeTarget( target: PartialRangeTargetDescriptor, previousTargets: PartialTargetDescriptor[], -): RangeTargetDescriptor { +): PrimitiveTargetDescriptor | RangeTargetDescriptor { const fullTarget: RangeTargetDescriptor = { type: "range", rangeType: target.rangeType ?? "continuous", @@ -96,100 +93,7 @@ function inferRangeTarget( const isAnchorMarkImplicit = target.anchor.type === "implicit" || target.anchor.mark == null; - return handleEveryRangeTarget(fullTarget, isAnchorMarkImplicit) ?? fullTarget; -} - -/** - * This function exists to enable constructs like "every line air past bat". - * When we detect a range target which has an `everyScope` modifier on its - * anchor, we do the following: - * - * 1. Split the anchor's modifier chain, so that everything after the - * `everyScope` modifier remains on the anchor, and we reserve everything - * that comes before the `everyScope` modifier. - * 2. We construct a special "every" range target that we handle specially in - * {@link TargetPipeline.processEveryRangeTarget}. - * 3. We put the reserved modifiers on the "every" range target to be applied - * after we've handled the "every" range target. - * 4. We remove everything before and including the `everyScope` modifier from - * the active target. - * - * We effectively break the chain into two parts, one that is applied before - * handling the "every" range target, and one that is applied after. For - * example: - * - * ``` - * "first token every line funk air past bat" - * ``` - * - * In this case, we create an "every" range target with anchor `"funk air"` and - * active `"funk bat"`. We then apply the modifier `"first token"` to the - * resulting range. - * - * @param fullTarget The full range target, post-inference - * @param isAnchorMarkImplicit `true` if the anchor mark was implicit on the - * original partial target - */ -function handleEveryRangeTarget( - fullTarget: RangeTargetDescriptor, - isAnchorMarkImplicit: boolean, -): EveryRangeTargetDescriptor | null { - const { anchor, rangeType, active, excludeAnchor, excludeActive } = - fullTarget; - - if (anchor.type !== "primitive" || rangeType !== "continuous") { - return null; - } - - const everyScopeModifierIndex = findLastIndex( - anchor.modifiers, - ({ type }) => type === "everyScope", - ); - - if (everyScopeModifierIndex === -1) { - return null; - } - - const scopeType = ( - anchor.modifiers[everyScopeModifierIndex] as EveryScopeModifier - ).scopeType; - - const [beforeEveryModifiers, afterEveryModifiers] = [ - anchor.modifiers.slice(0, everyScopeModifierIndex), - anchor.modifiers.slice(everyScopeModifierIndex + 1), - ]; - - return { - type: "range", - rangeType: "every", - scopeType, - anchor: - // If they say "every line past bat", the anchor is implicit, even though - // it comes across the wire as a primitive target due to the "every line", - // which we've now removed - afterEveryModifiers.length === 0 && isAnchorMarkImplicit - ? { type: "implicit" } - : { - type: "primitive", - mark: anchor.mark, - positionModifier: undefined, - modifiers: afterEveryModifiers, - }, - // Remove the "every" (and everything before it) from the active if it - // ended up there from inference - active: produce(active, (draft) => { - draft.modifiers = draft.modifiers.slice( - findLastIndex( - draft.modifiers, - (modifier) => modifier.type === "everyScope", - ) + 1, - ); - }), - modifiers: beforeEveryModifiers, - positionModifier: anchor.positionModifier, - excludeAnchor, - excludeActive, - }; + return handleHoistedModifiers(fullTarget, isAnchorMarkImplicit); } function inferPossiblyImplicitTarget( diff --git a/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts b/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts index 811e94359a..710ef6e72f 100644 --- a/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts +++ b/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts @@ -1,4 +1,4 @@ -import { Mark } from "@cursorless/common"; +import { Mark } from "../typings/TargetDescriptor"; import { MarkStage } from "./PipelineStages.types"; export interface MarkStageFactory { diff --git a/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts index e7674dbe3a..79e5bdb583 100644 --- a/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts @@ -1,4 +1,4 @@ -import { Mark, ReadOnlyHatMap } from "@cursorless/common"; +import { ReadOnlyHatMap } from "@cursorless/common"; import { StoredTargetMap } from ".."; import { MarkStageFactory } from "./MarkStageFactory"; import { MarkStage } from "./PipelineStages.types"; @@ -8,8 +8,17 @@ import LineNumberStage from "./marks/LineNumberStage"; import NothingStage from "./marks/NothingStage"; import RangeMarkStage from "./marks/RangeMarkStage"; import { StoredTargetStage } from "./marks/StoredTargetStage"; +import { Mark } from "../typings/TargetDescriptor"; +import { TargetPipelineRunner } from "."; +import { TargetMarkStage } from "./marks/TargetMarkStage"; export class MarkStageFactoryImpl implements MarkStageFactory { + private targetPipelineRunner!: TargetPipelineRunner; + + setPipelineRunner(targetPipelineRunner: TargetPipelineRunner) { + this.targetPipelineRunner = targetPipelineRunner; + } + constructor( private readableHatMap: ReadOnlyHatMap, private storedTargets: StoredTargetMap, @@ -32,6 +41,8 @@ export class MarkStageFactoryImpl implements MarkStageFactory { return new RangeMarkStage(this, mark); case "nothing": return new NothingStage(mark); + case "target": + return new TargetMarkStage(this.targetPipelineRunner, mark); } } } diff --git a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts index e21c2b4742..0757071290 100644 --- a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts +++ b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts @@ -1,12 +1,6 @@ -import { - ImplicitTargetDescriptor, - Modifier, - PositionModifier, - Range, -} from "@cursorless/common"; +import { ImplicitTargetDescriptor, Modifier, Range } from "@cursorless/common"; import { uniqWith, zip } from "lodash"; import { - EveryRangeTargetDescriptor, PrimitiveTargetDescriptor, RangeTargetDescriptor, TargetDescriptor, @@ -40,14 +34,14 @@ export class TargetPipelineRunner { * how to remove the target */ run( - targets: TargetDescriptor[], + target: TargetDescriptor, actionPrePositionStages?: ModifierStage[], actionFinalStages?: ModifierStage[], - ): Target[][] { + ): Target[] { return new TargetPipeline( this.modifierStageFactory, this.markStageFactory, - targets, + target, actionPrePositionStages ?? [], actionFinalStages ?? [], ).run(); @@ -58,7 +52,7 @@ class TargetPipeline { constructor( private modifierStageFactory: ModifierStageFactory, private markStageFactory: MarkStageFactory, - private targets: TargetDescriptor[], + private target: TargetDescriptor, private actionPrePositionStages: ModifierStage[], private actionFinalStages: ModifierStage[], ) {} @@ -77,10 +71,8 @@ class TargetPipeline { * containing it, and potentially rich context information such as how to remove * the target */ - run(): Target[][] { - return this.targets.map((target) => - uniqTargets(this.processTarget(target)), - ); + run(): Target[] { + return uniqTargets(this.processTarget(this.target)); } processTarget(target: TargetDescriptor): Target[] { @@ -111,14 +103,11 @@ class TargetPipeline { switch (targetDesc.rangeType) { case "continuous": - return [ - targetsToContinuousTarget( - anchorTarget, - activeTarget, - targetDesc.excludeAnchor, - targetDesc.excludeActive, - ), - ]; + return this.processContinuousRangeTarget( + anchorTarget, + activeTarget, + targetDesc, + ); case "vertical": return targetsToVerticalTarget( anchorTarget, @@ -126,28 +115,27 @@ class TargetPipeline { targetDesc.excludeAnchor, targetDesc.excludeActive, ); - case "every": - return this.processEveryRangeTarget( - anchorTarget, - activeTarget, - targetDesc, - ); } }, ); } - processEveryRangeTarget( + processContinuousRangeTarget( anchorTarget: Target, activeTarget: Target, - { - excludeAnchor, - excludeActive, - scopeType, - modifiers, - positionModifier, - }: EveryRangeTargetDescriptor, + { excludeAnchor, excludeActive, exclusionScopeType }: RangeTargetDescriptor, ): Target[] { + if (exclusionScopeType == null) { + return [ + targetsToContinuousTarget( + anchorTarget, + activeTarget, + excludeAnchor, + excludeActive, + ), + ]; + } + const isReversed = calcIsReversed(anchorTarget, activeTarget); // For "every" range targets, we need to be smart about how we handle @@ -161,52 +149,37 @@ class TargetPipeline { // endpoint is still in the domain, so we just get "funk name bat" back // again. So instead, we use the equivalent of "previous funk name" to find // our endpoint and then just use an inclusive range ending with that target. - const rangeTarget = targetsToContinuousTarget( - excludeAnchor - ? this.modifierStageFactory - .create({ - type: "relativeScope", - scopeType, - direction: isReversed ? "backward" : "forward", - length: 1, - offset: 1, - }) - // NB: The following line assumes that content range is always - // contained by domain, so that "every" will properly reconstruct - // the target from the content range. - .run(anchorTarget)[0] - : anchorTarget, - excludeActive - ? this.modifierStageFactory - .create({ - type: "relativeScope", - scopeType, - direction: isReversed ? "forward" : "backward", - length: 1, - offset: 1, - }) - .run(activeTarget)[0] - : activeTarget, - false, - false, - ); - - const everyTargets = this.modifierStageFactory - .create({ - type: "everyScope", - scopeType, - }) - .run(rangeTarget); - - // Run the final modifier pipeline on the output from the "every" modifier - return this.processPrimitiveTarget({ - type: "customPrimitiveTarget", - modifiers, - positionModifier, - markStage: { - run: () => everyTargets, - }, - }); + return [ + targetsToContinuousTarget( + excludeAnchor + ? this.modifierStageFactory + .create({ + type: "relativeScope", + scopeType: exclusionScopeType, + direction: isReversed ? "backward" : "forward", + length: 1, + offset: 1, + }) + // NB: The following line assumes that content range is always + // contained by domain, so that "every" will properly reconstruct + // the target from the content range. + .run(anchorTarget)[0] + : anchorTarget, + excludeActive + ? this.modifierStageFactory + .create({ + type: "relativeScope", + scopeType: exclusionScopeType, + direction: isReversed ? "forward" : "backward", + length: 1, + offset: 1, + }) + .run(activeTarget)[0] + : activeTarget, + false, + false, + ), + ]; } /** @@ -229,10 +202,7 @@ class TargetPipeline { * @returns The output of running the modifier pipeline on the output from the mark */ processPrimitiveTarget( - targetDescriptor: - | PrimitiveTargetDescriptor - | ImplicitTargetDescriptor - | CustomPrimitiveTargetDescriptor, + targetDescriptor: PrimitiveTargetDescriptor | ImplicitTargetDescriptor, ): Target[] { let markStage: MarkStage; let nonPositionModifierStages: ModifierStage[]; @@ -243,10 +213,7 @@ class TargetPipeline { nonPositionModifierStages = []; positionModifierStages = []; } else { - markStage = - targetDescriptor.type === "customPrimitiveTarget" - ? targetDescriptor.markStage - : this.markStageFactory.create(targetDescriptor.mark); + markStage = this.markStageFactory.create(targetDescriptor.mark); positionModifierStages = targetDescriptor.positionModifier == null ? [] @@ -293,15 +260,6 @@ export function getModifierStagesFromTargetModifiers( return targetModifiers.map(modifierStageFactory.create).reverse(); } -/** Can use this type when you are using some custom mark stage but want the - * output to pass through a set of modifiers */ -interface CustomPrimitiveTargetDescriptor { - type: "customPrimitiveTarget"; - modifiers: Modifier[]; - positionModifier?: PositionModifier; - markStage: MarkStage; -} - /** Run all targets through the modifier stages */ export function processModifierStages( modifierStages: ModifierStage[], diff --git a/packages/cursorless-engine/src/processTargets/marks/TargetMarkStage.ts b/packages/cursorless-engine/src/processTargets/marks/TargetMarkStage.ts new file mode 100644 index 0000000000..f9190b518d --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/marks/TargetMarkStage.ts @@ -0,0 +1,15 @@ +import { TargetPipelineRunner } from ".."; +import { TargetMark } from "../../typings/TargetDescriptor"; +import { Target } from "../../typings/target.types"; +import { MarkStage } from "../PipelineStages.types"; + +export class TargetMarkStage implements MarkStage { + constructor( + private targetPipelineRunner: TargetPipelineRunner, + private mark: TargetMark, + ) {} + + run(): Target[] { + return this.targetPipelineRunner.run(this.mark.target); + } +} diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 097ebe32ee..94332348b5 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -78,13 +78,19 @@ function createCommandRunner( new ScopeHandlerFactoryImpl(languageDefinitions), ); + const markStageFactory = new MarkStageFactoryImpl( + readableHatMap, + storedTargets, + ); + const targetPipelineRunner = new TargetPipelineRunner( + modifierStageFactory, + markStageFactory, + ); + markStageFactory.setPipelineRunner(targetPipelineRunner); return new CommandRunnerImpl( debug, storedTargets, - new TargetPipelineRunner( - modifierStageFactory, - new MarkStageFactoryImpl(readableHatMap, storedTargets), - ), + targetPipelineRunner, new Actions(snippets, rangeUpdater, modifierStageFactory), ); } diff --git a/packages/cursorless-engine/src/typings/TargetDescriptor.ts b/packages/cursorless-engine/src/typings/TargetDescriptor.ts index fd496d0769..0d4d76c92b 100644 --- a/packages/cursorless-engine/src/typings/TargetDescriptor.ts +++ b/packages/cursorless-engine/src/typings/TargetDescriptor.ts @@ -1,15 +1,17 @@ import { - PartialPrimitiveTargetDescriptor, - Mark, + ImplicitTargetDescriptor, Modifier, - PositionModifier, + PartialMark, PartialRangeType, - ImplicitTargetDescriptor, + PositionModifier, ScopeType, } from "@cursorless/common"; -export interface PrimitiveTargetDescriptor - extends PartialPrimitiveTargetDescriptor { +export type Mark = PartialMark | TargetMark; + +export interface PrimitiveTargetDescriptor { + type: "primitive"; + /** * The mark, eg "air", "this", "that", etc */ @@ -33,40 +35,39 @@ export interface PrimitiveTargetDescriptor positionModifier?: PositionModifier; } -interface BaseRangeTargetDescriptor { +/** + * Can be used when constructing a primitive target that applies modifiers to + * the output of some other complex target descriptor. For example, we use this + * to apply the hoisted modifiers to the output of a range target when we hoist + * the "every funk" modifier on a command like "take every funk air until bat". + */ +export interface TargetMark { + type: "target"; + + /** + * The target descriptor that will be used to generate the targets output by + * this mark. + */ + target: TargetDescriptor; +} + +export interface RangeTargetDescriptor { type: "range"; anchor: PrimitiveTargetDescriptor | ImplicitTargetDescriptor; active: PrimitiveTargetDescriptor; excludeAnchor: boolean; excludeActive: boolean; - rangeType: PartialRangeType | "every"; -} - -interface SimpleRangeTargetDescriptor extends BaseRangeTargetDescriptor { rangeType: PartialRangeType; -} - -/** Represents targets such as "every line air past bat" */ -export interface EveryRangeTargetDescriptor extends BaseRangeTargetDescriptor { - rangeType: "every"; - scopeType: ScopeType; - - /** Modifiers to be applied after constructing the "every" range */ - modifiers: Modifier[]; /** - * Position modifiers to be applied after constructing the "every" range. We - * separate the positional modifier from the other modifiers because it - * behaves differently and and makes the target behave like a destination for - * example for bring. This change is the first step toward #803 + * Indicates that endpoints should be excluded by going to the next or + * previous instance of the given scope type, rather than the default behavior + * of moving to the start or end of the given endpoint target. This field is primarily + * used by "every" ranges, eg "every funk air until bat" */ - positionModifier?: PositionModifier; + exclusionScopeType?: ScopeType; } -export type RangeTargetDescriptor = - | SimpleRangeTargetDescriptor - | EveryRangeTargetDescriptor; - export interface ListTargetDescriptor { type: "list"; elements: (PrimitiveTargetDescriptor | RangeTargetDescriptor)[];