From b4d99c1f0e2522a11a55fa149636b09bb53fee05 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 7 Apr 2023 19:09:52 +0100 Subject: [PATCH] Add factories for stages and scope handlers --- .../cursorless-engine/src/actions/Actions.ts | 51 +++- .../src/actions/EditNew/EditNew.ts | 11 +- .../src/actions/InsertCopy.ts | 27 +- .../src/actions/InsertSnippet.ts | 4 +- .../cursorless-engine/src/actions/Rewrap.ts | 14 +- .../src/actions/ToggleBreakpoint.ts | 11 +- .../src/actions/WrapWithSnippet.ts | 9 +- .../src/core/commandRunner/CommandRunner.ts | 23 +- .../cursorless-engine/src/cursorlessEngine.ts | 12 +- .../src/processTargets/MarkStageFactory.ts | 6 + .../processTargets/MarkStageFactoryImpl.ts | 34 +++ .../processTargets/ModifierStageFactory.ts | 13 + .../ModifierStageFactoryImpl.ts | 134 +++++++++ .../src/processTargets/TargetPipeline.ts | 276 ++++++++++++++++++ .../src/processTargets/getMarkStage.ts | 27 -- .../src/processTargets/getModifierStage.ts | 64 ---- .../src/processTargets/index.ts | 3 +- .../processTargets/marks/RangeMarkStage.ts | 15 +- .../modifiers/CascadingStage.ts | 13 +- .../modifiers/ConditionalModifierStages.ts | 25 +- .../modifiers/ContainingScopeStage.ts | 20 +- .../modifiers/EveryScopeStage.ts | 26 +- .../processTargets/modifiers/HeadTailStage.ts | 33 ++- .../processTargets/modifiers/InteriorStage.ts | 21 +- .../modifiers/LeadingTrailingStages.ts | 49 ++-- .../modifiers/OrdinalScopeStage.ts | 9 +- .../modifiers/RangeModifierStage.ts | 15 +- .../modifiers/RelativeExclusiveScopeStage.ts | 20 +- .../modifiers/RelativeInclusiveScopeStage.ts | 24 +- .../modifiers/RelativeScopeStage.ts | 24 +- ...ommonContainingScopeIfUntypedModifiers.ts} | 24 +- .../modifiers/getLegacyScopeStage.ts | 59 ---- .../modifiers/relativeScopeLegacy.ts | 6 +- .../scopeHandlers/NestedScopeHandler.ts | 7 +- .../scopeHandlers/OneOfScopeHandler.ts | 18 +- .../scopeHandlers/ScopeHandlerFactory.ts | 6 + ...eHandler.ts => ScopeHandlerFactoryImpl.ts} | 50 ++-- .../modifiers/scopeHandlers/index.ts | 3 +- .../BoundedNonWhitespaceStage.ts | 13 +- .../modifiers/targetSequenceUtils.ts | 7 +- .../src/processTargets/processTargets.ts | 269 ----------------- .../cursorless-engine/src/util/unifyRanges.ts | 2 +- 42 files changed, 868 insertions(+), 609 deletions(-) create mode 100644 packages/cursorless-engine/src/processTargets/MarkStageFactory.ts create mode 100644 packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts create mode 100644 packages/cursorless-engine/src/processTargets/ModifierStageFactory.ts create mode 100644 packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts create mode 100644 packages/cursorless-engine/src/processTargets/TargetPipeline.ts delete mode 100644 packages/cursorless-engine/src/processTargets/getMarkStage.ts delete mode 100644 packages/cursorless-engine/src/processTargets/getModifierStage.ts rename packages/cursorless-engine/src/processTargets/modifiers/{commonContainingScopeIfUntypedStages.ts => commonContainingScopeIfUntypedModifiers.ts} (72%) delete mode 100644 packages/cursorless-engine/src/processTargets/modifiers/getLegacyScopeStage.ts create mode 100644 packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts rename packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/{getScopeHandler.ts => ScopeHandlerFactoryImpl.ts} (50%) delete mode 100644 packages/cursorless-engine/src/processTargets/processTargets.ts diff --git a/packages/cursorless-engine/src/actions/Actions.ts b/packages/cursorless-engine/src/actions/Actions.ts index eea9cf5ba8..1d19006c1a 100644 --- a/packages/cursorless-engine/src/actions/Actions.ts +++ b/packages/cursorless-engine/src/actions/Actions.ts @@ -1,5 +1,6 @@ import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { Bring, Move, Swap } from "./BringMoveSwap"; import Call from "./Call"; import Clear from "./Clear"; @@ -58,16 +59,28 @@ import { ActionRecord } from "./actions.types"; * Keeps a map from action names to objects that implement the given action */ export class Actions implements ActionRecord { - constructor(private snippets: Snippets, private rangeUpdater: RangeUpdater) {} + constructor( + private snippets: Snippets, + private rangeUpdater: RangeUpdater, + private modifierStageFactory: ModifierStageFactory, + ) {} callAsFunction = new Call(this); clearAndSetSelection = new Clear(this); copyToClipboard = new CopyToClipboard(this.rangeUpdater); cutToClipboard = new CutToClipboard(this); deselect = new Deselect(); - editNew = new EditNew(this.rangeUpdater, this); - editNewLineAfter = new EditNewAfter(this.rangeUpdater, this); - editNewLineBefore = new EditNewBefore(this.rangeUpdater, this); + editNew = new EditNew(this.rangeUpdater, this, this.modifierStageFactory); + editNewLineAfter = new EditNewAfter( + this.rangeUpdater, + this, + this.modifierStageFactory, + ); + editNewLineBefore = new EditNewBefore( + this.rangeUpdater, + this, + this.modifierStageFactory, + ); executeCommand = new ExecuteCommand(this.rangeUpdater); extractVariable = new ExtractVariable(this.rangeUpdater); findInWorkspace = new FindInWorkspace(this); @@ -77,12 +90,23 @@ export class Actions implements ActionRecord { getText = new GetText(); highlight = new Highlight(); indentLine = new IndentLine(this.rangeUpdater); - insertCopyAfter = new InsertCopyAfter(this.rangeUpdater); - insertCopyBefore = new InsertCopyBefore(this.rangeUpdater); + insertCopyAfter = new InsertCopyAfter( + this.rangeUpdater, + this.modifierStageFactory, + ); + insertCopyBefore = new InsertCopyBefore( + this.rangeUpdater, + this.modifierStageFactory, + ); insertEmptyLineAfter = new InsertEmptyLineAfter(this.rangeUpdater); insertEmptyLineBefore = new InsertEmptyLineBefore(this.rangeUpdater); insertEmptyLinesAround = new InsertEmptyLinesAround(this.rangeUpdater); - insertSnippet = new InsertSnippet(this.rangeUpdater, this.snippets, this); + insertSnippet = new InsertSnippet( + this.rangeUpdater, + this.snippets, + this, + this.modifierStageFactory, + ); moveToTarget = new Move(this.rangeUpdater); outdentLine = new OutdentLine(this.rangeUpdater); pasteFromClipboard = new PasteFromClipboard(this.rangeUpdater, this); @@ -94,7 +118,10 @@ export class Actions implements ActionRecord { revealDefinition = new RevealDefinition(this.rangeUpdater); revealTypeDefinition = new RevealTypeDefinition(this.rangeUpdater); reverseTargets = new Reverse(this); - rewrapWithPairedDelimiter = new Rewrap(this.rangeUpdater); + rewrapWithPairedDelimiter = new Rewrap( + this.rangeUpdater, + this.modifierStageFactory, + ); scrollToBottom = new ScrollToBottom(); scrollToCenter = new ScrollToCenter(); scrollToTop = new ScrollToTop(); @@ -107,9 +134,13 @@ export class Actions implements ActionRecord { showReferences = new ShowReferences(this.rangeUpdater); sortTargets = new Sort(this); swapTargets = new Swap(this.rangeUpdater); - toggleLineBreakpoint = new ToggleBreakpoint(); + toggleLineBreakpoint = new ToggleBreakpoint(this.modifierStageFactory); toggleLineComment = new ToggleLineComment(this.rangeUpdater); unfoldRegion = new Unfold(this.rangeUpdater); wrapWithPairedDelimiter = new Wrap(this.rangeUpdater); - wrapWithSnippet = new WrapWithSnippet(this.rangeUpdater, this.snippets); + wrapWithSnippet = new WrapWithSnippet( + this.rangeUpdater, + this.snippets, + this.modifierStageFactory, + ); } diff --git a/packages/cursorless-engine/src/actions/EditNew/EditNew.ts b/packages/cursorless-engine/src/actions/EditNew/EditNew.ts index 04c1a7bfab..498d8a6416 100644 --- a/packages/cursorless-engine/src/actions/EditNew/EditNew.ts +++ b/packages/cursorless-engine/src/actions/EditNew/EditNew.ts @@ -1,6 +1,7 @@ import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -import { containingLineIfUntypedStage } from "../../processTargets/modifiers/commonContainingScopeIfUntypedStages"; +import { containingLineIfUntypedModifier } from "../../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; import PositionStage from "../../processTargets/modifiers/PositionStage"; +import { ModifierStageFactory } from "../../processTargets/ModifierStageFactory"; import { ModifierStage } from "../../processTargets/PipelineStages.types"; import { ide } from "../../singletons/ide.singleton"; import { Target } from "../../typings/target.types"; @@ -15,10 +16,14 @@ import { runEditNewNotebookCellTargets } from "./runNotebookCellTargets"; export class EditNew implements Action { getFinalStages(): ModifierStage[] { - return [containingLineIfUntypedStage]; + return [this.modifierStageFactory.create(containingLineIfUntypedModifier)]; } - constructor(private rangeUpdater: RangeUpdater, private actions: Actions) { + constructor( + private rangeUpdater: RangeUpdater, + private actions: Actions, + private modifierStageFactory: ModifierStageFactory, + ) { this.run = this.run.bind(this); } diff --git a/packages/cursorless-engine/src/actions/InsertCopy.ts b/packages/cursorless-engine/src/actions/InsertCopy.ts index ecdd7b177c..18ceacb8ee 100644 --- a/packages/cursorless-engine/src/actions/InsertCopy.ts +++ b/packages/cursorless-engine/src/actions/InsertCopy.ts @@ -8,7 +8,8 @@ import { import { flatten, zip } from "lodash"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelectionsWithBehavior } from "../core/updateSelections/updateSelections"; -import { containingLineIfUntypedStage } from "../processTargets/modifiers/commonContainingScopeIfUntypedStages"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { containingLineIfUntypedModifier } from "../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; @@ -16,9 +17,15 @@ import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; import { Action, ActionReturnValue } from "./actions.types"; class InsertCopy implements Action { - getFinalStages = () => [containingLineIfUntypedStage]; + getFinalStages = () => [ + this.modifierStageFactory.create(containingLineIfUntypedModifier), + ]; - constructor(private rangeUpdater: RangeUpdater, private isBefore: boolean) { + constructor( + private rangeUpdater: RangeUpdater, + private modifierStageFactory: ModifierStageFactory, + private isBefore: boolean, + ) { this.run = this.run.bind(this); this.runForEditor = this.runForEditor.bind(this); } @@ -90,13 +97,19 @@ class InsertCopy implements Action { } export class CopyContentBefore extends InsertCopy { - constructor(rangeUpdater: RangeUpdater) { - super(rangeUpdater, true); + constructor( + rangeUpdater: RangeUpdater, + modifierStageFactory: ModifierStageFactory, + ) { + super(rangeUpdater, modifierStageFactory, true); } } export class CopyContentAfter extends InsertCopy { - constructor(rangeUpdater: RangeUpdater) { - super(rangeUpdater, false); + constructor( + rangeUpdater: RangeUpdater, + modifierStageFactory: ModifierStageFactory, + ) { + super(rangeUpdater, modifierStageFactory, false); } } diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index 28010fe1eb..cb186043d0 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -11,6 +11,7 @@ import { callFunctionAndUpdateSelectionInfos, getSelectionInfo, } from "../core/updateSelections/updateSelections"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedExplicitStage } from "../processTargets/modifiers/ConditionalModifierStages"; import { ide } from "../singletons/ide.singleton"; import { @@ -43,6 +44,7 @@ export default class InsertSnippet implements Action { private rangeUpdater: RangeUpdater, private snippets: Snippets, private actions: Actions, + private modifierStageFactory: ModifierStageFactory, ) { this.run = this.run.bind(this); } @@ -53,7 +55,7 @@ export default class InsertSnippet implements Action { return defaultScopeTypes.length === 0 ? [] : [ - new ModifyIfUntypedExplicitStage({ + new ModifyIfUntypedExplicitStage(this.modifierStageFactory, { type: "cascading", modifiers: defaultScopeTypes.map((scopeType) => ({ type: "containingScope", diff --git a/packages/cursorless-engine/src/actions/Rewrap.ts b/packages/cursorless-engine/src/actions/Rewrap.ts index ffa64c13e1..54431e1455 100644 --- a/packages/cursorless-engine/src/actions/Rewrap.ts +++ b/packages/cursorless-engine/src/actions/Rewrap.ts @@ -1,7 +1,8 @@ import { FlashStyle } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections"; -import { containingSurroundingPairIfUntypedStage } from "../processTargets/modifiers/commonContainingScopeIfUntypedStages"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { containingSurroundingPairIfUntypedModifier } from "../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { @@ -12,9 +13,16 @@ import { import { Action, ActionReturnValue } from "./actions.types"; export default class Rewrap implements Action { - getFinalStages = () => [containingSurroundingPairIfUntypedStage]; + getFinalStages = () => [ + this.modifierStageFactory.create( + containingSurroundingPairIfUntypedModifier, + ), + ]; - constructor(private rangeUpdater: RangeUpdater) { + constructor( + private rangeUpdater: RangeUpdater, + private modifierStageFactory: ModifierStageFactory, + ) { this.run = this.run.bind(this); } diff --git a/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts b/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts index dd91e56536..3b7d4b88cb 100644 --- a/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts +++ b/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts @@ -1,14 +1,17 @@ +import { BreakpointDescriptor, FlashStyle } from "@cursorless/common"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { containingLineIfUntypedModifier } from "../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; import { ide } from "../singletons/ide.singleton"; -import { containingLineIfUntypedStage } from "../processTargets/modifiers/commonContainingScopeIfUntypedStages"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; import { Action, ActionReturnValue } from "./actions.types"; -import { BreakpointDescriptor, FlashStyle } from "@cursorless/common"; export default class ToggleBreakpoint implements Action { - getFinalStages = () => [containingLineIfUntypedStage]; + getFinalStages = () => [ + this.modifierStageFactory.create(containingLineIfUntypedModifier), + ]; - constructor() { + constructor(private modifierStageFactory: ModifierStageFactory) { this.run = this.run.bind(this); } diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index 48f982e166..7b0201288a 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -2,6 +2,7 @@ import { FlashStyle, ScopeType } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedStage } from "../processTargets/modifiers/ConditionalModifierStages"; import { ide } from "../singletons/ide.singleton"; import { @@ -29,7 +30,11 @@ type WrapWithSnippetArg = NamedSnippetArg | CustomSnippetArg; export default class WrapWithSnippet implements Action { private snippetParser = new SnippetParser(); - constructor(private rangeUpdater: RangeUpdater, private snippets: Snippets) { + constructor( + private rangeUpdater: RangeUpdater, + private snippets: Snippets, + private modifierStageFactory: ModifierStageFactory, + ) { this.run = this.run.bind(this); } @@ -41,7 +46,7 @@ export default class WrapWithSnippet implements Action { } return [ - new ModifyIfUntypedStage({ + new ModifyIfUntypedStage(this.modifierStageFactory, { type: "modifyIfUntyped", modifier: { type: "containingScope", diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunner.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunner.ts index 7f04b10aff..c0ce14c4bd 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunner.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunner.ts @@ -9,25 +9,26 @@ import { ActionRecord } from "../../actions/actions.types"; // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports import { Actions } from "../../actions/Actions"; import { TestCaseRecorder } from "../../index"; -import processTargets from "../../processTargets"; +import { TargetPipeline } from "../../processTargets"; +import { MarkStageFactory } from "../../processTargets/MarkStageFactory"; +import { ModifierStageFactory } from "../../processTargets/ModifierStageFactory"; import { ide } from "../../singletons/ide.singleton"; -import { Target } from "../../typings/target.types"; import { TreeSitter } from "../../typings/TreeSitter"; import { ProcessedTargetsContext, SelectionWithEditor, } from "../../typings/Types"; +import { Target } from "../../typings/target.types"; import { isString } from "../../util/type"; +import { Debug } from "../Debug"; +import { ThatMark } from "../ThatMark"; import { canonicalizeAndValidateCommand, checkForOldInference, } from "../commandVersionUpgrades/canonicalizeAndValidateCommand"; -import { Debug } from "../Debug"; import inferFullTargets from "../inferFullTargets"; -import { ThatMark } from "../ThatMark"; import { selectionToThatTarget } from "./selectionToThatTarget"; -// TODO: Do this using the graph once we migrate its dependencies onto the graph export class CommandRunner { constructor( private treeSitter: TreeSitter, @@ -37,6 +38,8 @@ export class CommandRunner { private actions: ActionRecord, private thatMark: ThatMark, private sourceMark: ThatMark, + private modifierStageFactory: ModifierStageFactory, + private markStageFactory: MarkStageFactory, ) { this.runCommandBackwardCompatible = this.runCommandBackwardCompatible.bind(this); @@ -137,11 +140,17 @@ export class CommandRunner { // warning. checkForOldInference(partialTargetDescriptors); - const targets = processTargets( + // FIXME: Construct this on a per-request basis in the composition root. + // Then we don't need `CommandRunner` to depend on these factories and be + // tightly coupled to `TargetPipeline`. + const pipeline = new TargetPipeline( + this.modifierStageFactory, + this.markStageFactory, processedTargetsContext, - targetDescriptors, ); + const targets = pipeline.processTargets(targetDescriptors); + const { returnValue, thatSelections: newThatSelections, diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 0ee89ada54..656a5951e6 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -5,6 +5,9 @@ import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; import { RangeUpdater } from "./core/updateSelections/RangeUpdater"; +import { MarkStageFactoryImpl } from "./processTargets/MarkStageFactoryImpl"; +import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl"; +import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers"; import { injectIde } from "./singletons/ide.singleton"; export function createCursorlessEngine( @@ -33,7 +36,12 @@ export function createCursorlessEngine( const testCaseRecorder = new TestCaseRecorder(hatTokenMap); - const actions = new Actions(snippets, rangeUpdater); + const scopeHandlerFactory = new ScopeHandlerFactoryImpl(); + const markStageFactory = new MarkStageFactoryImpl(); + const modifierStageFactory = new ModifierStageFactoryImpl( + scopeHandlerFactory, + ); + const actions = new Actions(snippets, rangeUpdater, modifierStageFactory); const thatMark = new ThatMark(); const sourceMark = new ThatMark(); @@ -46,6 +54,8 @@ export function createCursorlessEngine( actions, thatMark, sourceMark, + modifierStageFactory, + markStageFactory, ); return { diff --git a/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts b/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts new file mode 100644 index 0000000000..811e94359a --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/MarkStageFactory.ts @@ -0,0 +1,6 @@ +import { Mark } from "@cursorless/common"; +import { MarkStage } from "./PipelineStages.types"; + +export interface MarkStageFactory { + create(mark: Mark): MarkStage; +} diff --git a/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts new file mode 100644 index 0000000000..997153df39 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/MarkStageFactoryImpl.ts @@ -0,0 +1,34 @@ +import { Mark } from "@cursorless/common"; +import { MarkStage } from "./PipelineStages.types"; +import CursorStage from "./marks/CursorStage"; +import DecoratedSymbolStage from "./marks/DecoratedSymbolStage"; +import LineNumberStage from "./marks/LineNumberStage"; +import NothingStage from "./marks/NothingStage"; +import RangeMarkStage from "./marks/RangeMarkStage"; +import { SourceStage, ThatStage } from "./marks/ThatStage"; +import { MarkStageFactory } from "./MarkStageFactory"; + +export class MarkStageFactoryImpl implements MarkStageFactory { + constructor() { + this.create = this.create.bind(this); + } + + create(mark: Mark): MarkStage { + switch (mark.type) { + case "cursor": + return new CursorStage(mark); + case "that": + return new ThatStage(mark); + case "source": + return new SourceStage(mark); + case "decoratedSymbol": + return new DecoratedSymbolStage(mark); + case "lineNumber": + return new LineNumberStage(mark); + case "range": + return new RangeMarkStage(this, mark); + case "nothing": + return new NothingStage(mark); + } + } +} diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactory.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactory.ts new file mode 100644 index 0000000000..35709141b9 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactory.ts @@ -0,0 +1,13 @@ +import { + ContainingScopeModifier, + EveryScopeModifier, + Modifier, +} from "@cursorless/common"; +import { ModifierStage } from "./PipelineStages.types"; + +export interface ModifierStageFactory { + create(modifier: Modifier): ModifierStage; + getLegacyScopeStage( + modifier: ContainingScopeModifier | EveryScopeModifier, + ): ModifierStage; +} diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts new file mode 100644 index 0000000000..0d5fe168b8 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts @@ -0,0 +1,134 @@ +import { + ContainingScopeModifier, + EveryScopeModifier, + Modifier, + SurroundingPairModifier, +} from "@cursorless/common"; +import { ModifierStageFactory } from "./ModifierStageFactory"; +import { ModifierStage } from "./PipelineStages.types"; +import CascadingStage from "./modifiers/CascadingStage"; +import { ModifyIfUntypedStage } from "./modifiers/ConditionalModifierStages"; +import { ContainingScopeStage } from "./modifiers/ContainingScopeStage"; +import { EveryScopeStage } from "./modifiers/EveryScopeStage"; +import { + KeepContentFilterStage, + KeepEmptyFilterStage, +} from "./modifiers/FilterStages"; +import { HeadStage, TailStage } from "./modifiers/HeadTailStage"; +import { + ExcludeInteriorStage, + InteriorOnlyStage, +} from "./modifiers/InteriorStage"; +import ItemStage from "./modifiers/ItemStage"; +import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; +import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; +import PositionStage from "./modifiers/PositionStage"; +import RangeModifierStage from "./modifiers/RangeModifierStage"; +import RawSelectionStage from "./modifiers/RawSelectionStage"; +import RelativeScopeStage from "./modifiers/RelativeScopeStage"; +import SurroundingPairStage from "./modifiers/SurroundingPairStage"; +import { ScopeHandlerFactory } from "./modifiers/scopeHandlers/ScopeHandlerFactory"; +import BoundedNonWhitespaceSequenceStage from "./modifiers/scopeTypeStages/BoundedNonWhitespaceStage"; +import ContainingSyntaxScopeStage, { + SimpleContainingScopeModifier, + SimpleEveryScopeModifier, +} from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; +import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; +import { + CustomRegexModifier, + CustomRegexStage, + NonWhitespaceSequenceStage, + UrlStage, +} from "./modifiers/scopeTypeStages/RegexStage"; + +export class ModifierStageFactoryImpl implements ModifierStageFactory { + constructor(private scopeHandlerFactory: ScopeHandlerFactory) { + this.create = this.create.bind(this); + } + + create(modifier: Modifier): ModifierStage { + switch (modifier.type) { + case "position": + return new PositionStage(modifier); + case "extendThroughStartOf": + return new HeadStage(this, modifier); + case "extendThroughEndOf": + return new TailStage(this, modifier); + case "toRawSelection": + return new RawSelectionStage(modifier); + case "interiorOnly": + return new InteriorOnlyStage(this, modifier); + case "excludeInterior": + return new ExcludeInteriorStage(this, modifier); + case "leading": + return new LeadingStage(this, modifier); + case "trailing": + return new TrailingStage(this, modifier); + case "containingScope": + return new ContainingScopeStage( + this, + this.scopeHandlerFactory, + modifier, + ); + case "everyScope": + return new EveryScopeStage(this, this.scopeHandlerFactory, modifier); + case "ordinalScope": + return new OrdinalScopeStage(this, modifier); + case "relativeScope": + return new RelativeScopeStage(this, this.scopeHandlerFactory, modifier); + case "keepContentFilter": + return new KeepContentFilterStage(modifier); + case "keepEmptyFilter": + return new KeepEmptyFilterStage(modifier); + case "cascading": + return new CascadingStage(this, modifier); + case "modifyIfUntyped": + return new ModifyIfUntypedStage(this, modifier); + case "range": + return new RangeModifierStage(this, modifier); + case "inferPreviousMark": + throw Error( + `Unexpected modifier '${modifier.type}'; it should have been removed during inference`, + ); + } + } + + /** + * Any scope type that has not been fully migrated to the new + * {@link ScopeHandler} setup should have a branch in this `switch` statement. + * Once the scope type is fully migrated, remove the branch and the legacy + * modifier stage. + * + * Note that it is possible for a scope type to be partially migrated. For + * example, we could support modern scope handlers for a certain scope type in + * Ruby, but not yet in Python. + * + * @param modifier The modifier for which to get the modifier stage + * @returns A scope stage implementing the modifier for the given scope type + */ + getLegacyScopeStage( + modifier: ContainingScopeModifier | EveryScopeModifier, + ): ModifierStage { + switch (modifier.scopeType.type) { + case "notebookCell": + return new NotebookCellStage(modifier); + case "nonWhitespaceSequence": + return new NonWhitespaceSequenceStage(modifier); + case "boundedNonWhitespaceSequence": + return new BoundedNonWhitespaceSequenceStage(this, modifier); + case "url": + return new UrlStage(modifier); + case "collectionItem": + return new ItemStage(modifier); + case "customRegex": + return new CustomRegexStage(modifier as CustomRegexModifier); + case "surroundingPair": + return new SurroundingPairStage(modifier as SurroundingPairModifier); + default: + // Default to containing syntax scope using tree sitter + return new ContainingSyntaxScopeStage( + modifier as SimpleContainingScopeModifier | SimpleEveryScopeModifier, + ); + } + } +} diff --git a/packages/cursorless-engine/src/processTargets/TargetPipeline.ts b/packages/cursorless-engine/src/processTargets/TargetPipeline.ts new file mode 100644 index 0000000000..3f98b4a636 --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/TargetPipeline.ts @@ -0,0 +1,276 @@ +import { ImplicitTargetDescriptor, Modifier, Range } from "@cursorless/common"; +import { uniqWith, zip } from "lodash"; +import { + PrimitiveTargetDescriptor, + RangeTargetDescriptor, + TargetDescriptor, +} from "../typings/TargetDescriptor"; +import { ProcessedTargetsContext } from "../typings/Types"; +import { Target } from "../typings/target.types"; +import { MarkStageFactory } from "./MarkStageFactory"; +import { ModifierStageFactory } from "./ModifierStageFactory"; +import { MarkStage, ModifierStage } from "./PipelineStages.types"; +import ImplicitStage from "./marks/ImplicitStage"; +import { ContainingTokenIfUntypedEmptyStage } from "./modifiers/ConditionalModifierStages"; +import { PlainTarget, PositionTarget } from "./targets"; + +export class TargetPipeline { + constructor( + private modifierStageFactory: ModifierStageFactory, + private markStageFactory: MarkStageFactory, + private context: ProcessedTargetsContext, + ) {} + + /** + * Converts the abstract target descriptions provided by the user to a concrete + * representation usable by actions. Conceptually, the input will be something + * like "the function call argument containing the cursor" and the output will be something + * like "line 3, characters 5 through 10". + * @param context Captures the environment needed to convert the abstract target + * description given by the user to a concrete representation usable by + * actions + * @param targets The abstract target representations provided by the user + * @returns A list of lists of typed selections, one list per input target. Each + * typed selection includes the selection, as well the uri of the document + * containing it, and potentially rich context information such as how to remove + * the target + */ + processTargets(targets: TargetDescriptor[]): Target[][] { + return targets.map((target) => uniqTargets(this.processTarget(target))); + } + + processTarget(target: TargetDescriptor): Target[] { + switch (target.type) { + case "list": + return target.elements.flatMap((element) => + this.processTarget(element), + ); + case "range": + return this.processRangeTarget(target); + case "primitive": + case "implicit": + return this.processPrimitiveTarget(target); + } + } + + processRangeTarget(targetDesc: RangeTargetDescriptor): Target[] { + const anchorTargets = this.processPrimitiveTarget(targetDesc.anchor); + const activeTargets = this.processPrimitiveTarget(targetDesc.active); + + return zip(anchorTargets, activeTargets).flatMap( + ([anchorTarget, activeTarget]) => { + if (anchorTarget == null || activeTarget == null) { + throw new Error( + "AnchorTargets and activeTargets lengths don't match", + ); + } + + switch (targetDesc.rangeType) { + case "continuous": + return [ + targetsToContinuousTarget( + anchorTarget, + activeTarget, + targetDesc.excludeAnchor, + targetDesc.excludeActive, + ), + ]; + case "vertical": + return targetsToVerticalTarget( + anchorTarget, + activeTarget, + targetDesc.excludeAnchor, + targetDesc.excludeActive, + ); + } + }, + ); + } + + /** + * This function implements the modifier pipeline that is at the core of Cursorless target processing. + * It proceeds as follows: + * + * 1. It begins by getting the output from the {@link markStage} (eg "air", "this", etc). + * This output is a list of zero or more targets. + * 2. It then constructs a pipeline from the modifiers on the {@link targetDescriptor} + * 3. It then runs each pipeline stage in turn, feeding the first stage with + * the list of targets output from the {@link markStage}. For each pipeline + * stage, it passes the targets from the previous stage to the pipeline stage + * one by one. For each target, the stage will output a list of zero or more output + * targets. It then concatenates all of these lists into the list of targets + * that will be passed to the next pipeline stage. This process is similar to + * the way that [jq](https://stedolan.github.io/jq/) processes its inputs. + * + * @param targetDescriptor The description of the target, consisting of a mark + * and zero or more modifiers + * @returns The output of running the modifier pipeline on the output from the mark + */ + processPrimitiveTarget( + targetDescriptor: PrimitiveTargetDescriptor | ImplicitTargetDescriptor, + ): Target[] { + let markStage: MarkStage; + let nonPositionModifierStages: ModifierStage[]; + let positionModifierStages: ModifierStage[]; + + if (targetDescriptor.type === "implicit") { + markStage = new ImplicitStage(); + nonPositionModifierStages = []; + positionModifierStages = []; + } else { + markStage = this.markStageFactory.create(targetDescriptor.mark); + positionModifierStages = + targetDescriptor.positionModifier == null + ? [] + : [ + this.modifierStageFactory.create( + targetDescriptor.positionModifier, + ), + ]; + nonPositionModifierStages = getModifierStagesFromTargetModifiers( + this.modifierStageFactory, + targetDescriptor.modifiers, + ); + } + + // First, get the targets output by the mark + const markOutputTargets = markStage.run(this.context); + + /** + * The modifier pipeline that will be applied to construct our final targets + */ + const modifierStages = [ + ...nonPositionModifierStages, + ...this.context.actionPrePositionStages, + ...positionModifierStages, + ...this.context.actionFinalStages, + + // This performs auto-expansion to token when you say eg "take this" with an + // empty selection + new ContainingTokenIfUntypedEmptyStage(this.modifierStageFactory), + ]; + + // Run all targets through the modifier stages + return processModifierStages( + this.context, + modifierStages, + markOutputTargets, + ); + } +} + +/** Convert a list of target modifiers to modifier stages */ +export function getModifierStagesFromTargetModifiers( + modifierStageFactory: ModifierStageFactory, + targetModifiers: Modifier[], +) { + // Reverse target modifiers because they are returned in reverse order from + // the api, to match the order in which they are spoken. + return targetModifiers.map(modifierStageFactory.create).reverse(); +} + +/** Run all targets through the modifier stages */ +export function processModifierStages( + context: ProcessedTargetsContext, + modifierStages: ModifierStage[], + targets: Target[], +) { + // First we apply each stage in sequence, letting each stage see the targets + // one-by-one and concatenating the results before passing them on to the + // next stage. + modifierStages.forEach((stage) => { + targets = targets.flatMap((target) => stage.run(context, target)); + }); + + // Then return the output from the final stage + return targets; +} + +function calcIsReversed(anchor: Target, active: Target) { + if (anchor.contentRange.start.isAfter(active.contentRange.start)) { + return true; + } + if (anchor.contentRange.start.isBefore(active.contentRange.start)) { + return false; + } + return anchor.contentRange.end.isAfter(active.contentRange.end); +} + +function uniqTargets(array: Target[]): Target[] { + return uniqWith(array, (a, b) => a.isEqual(b)); +} + +function ensureSingleEditor(anchorTarget: Target, activeTarget: Target) { + if (anchorTarget.editor !== activeTarget.editor) { + throw new Error("Cannot form range between targets in different editors"); + } +} + +export function targetsToContinuousTarget( + anchorTarget: Target, + activeTarget: Target, + excludeAnchor: boolean = false, + excludeActive: boolean = false, +): Target { + ensureSingleEditor(anchorTarget, activeTarget); + + const isReversed = calcIsReversed(anchorTarget, activeTarget); + const startTarget = isReversed ? activeTarget : anchorTarget; + const endTarget = isReversed ? anchorTarget : activeTarget; + const excludeStart = isReversed ? excludeActive : excludeAnchor; + const excludeEnd = isReversed ? excludeAnchor : excludeActive; + + return startTarget.createContinuousRangeTarget( + isReversed, + endTarget, + !excludeStart, + !excludeEnd, + ); +} + +function targetsToVerticalTarget( + anchorTarget: Target, + activeTarget: Target, + excludeAnchor: boolean, + excludeActive: boolean, +): Target[] { + ensureSingleEditor(anchorTarget, activeTarget); + + const isReversed = calcIsReversed(anchorTarget, activeTarget); + const delta = isReversed ? -1 : 1; + + const anchorPosition = isReversed + ? anchorTarget.contentRange.start + : anchorTarget.contentRange.end; + const anchorLine = anchorPosition.line + (excludeAnchor ? delta : 0); + const activePosition = isReversed + ? activeTarget.contentRange.start + : activeTarget.contentRange.end; + const activeLine = activePosition.line - (excludeActive ? delta : 0); + + const results: Target[] = []; + for (let i = anchorLine; true; i += delta) { + const contentRange = new Range( + i, + anchorTarget.contentRange.start.character, + i, + anchorTarget.contentRange.end.character, + ); + + if (anchorTarget instanceof PositionTarget) { + results.push(anchorTarget.withContentRange(contentRange)); + } else { + results.push( + new PlainTarget({ + editor: anchorTarget.editor, + isReversed: anchorTarget.isReversed, + contentRange, + }), + ); + } + + if (i === activeLine) { + return results; + } + } +} diff --git a/packages/cursorless-engine/src/processTargets/getMarkStage.ts b/packages/cursorless-engine/src/processTargets/getMarkStage.ts deleted file mode 100644 index 6a7be867d2..0000000000 --- a/packages/cursorless-engine/src/processTargets/getMarkStage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Mark } from "@cursorless/common"; -import CursorStage from "./marks/CursorStage"; -import DecoratedSymbolStage from "./marks/DecoratedSymbolStage"; -import LineNumberStage from "./marks/LineNumberStage"; -import NothingStage from "./marks/NothingStage"; -import RangeMarkStage from "./marks/RangeMarkStage"; -import { SourceStage, ThatStage } from "./marks/ThatStage"; -import { MarkStage } from "./PipelineStages.types"; - -export default (mark: Mark): MarkStage => { - switch (mark.type) { - case "cursor": - return new CursorStage(mark); - case "that": - return new ThatStage(mark); - case "source": - return new SourceStage(mark); - case "decoratedSymbol": - return new DecoratedSymbolStage(mark); - case "lineNumber": - return new LineNumberStage(mark); - case "range": - return new RangeMarkStage(mark); - case "nothing": - return new NothingStage(mark); - } -}; diff --git a/packages/cursorless-engine/src/processTargets/getModifierStage.ts b/packages/cursorless-engine/src/processTargets/getModifierStage.ts deleted file mode 100644 index 8385bbcc8f..0000000000 --- a/packages/cursorless-engine/src/processTargets/getModifierStage.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Modifier } from "@cursorless/common"; -import CascadingStage from "./modifiers/CascadingStage"; -import { ContainingScopeStage } from "./modifiers/ContainingScopeStage"; -import { EveryScopeStage } from "./modifiers/EveryScopeStage"; -import { - KeepContentFilterStage, - KeepEmptyFilterStage, -} from "./modifiers/FilterStages"; -import { HeadStage, TailStage } from "./modifiers/HeadTailStage"; -import { - ExcludeInteriorStage, - InteriorOnlyStage, -} from "./modifiers/InteriorStage"; -import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; -import { ModifyIfUntypedStage } from "./modifiers/ConditionalModifierStages"; -import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; -import PositionStage from "./modifiers/PositionStage"; -import RangeModifierStage from "./modifiers/RangeModifierStage"; -import RawSelectionStage from "./modifiers/RawSelectionStage"; -import RelativeScopeStage from "./modifiers/RelativeScopeStage"; -import { ModifierStage } from "./PipelineStages.types"; - -export default (modifier: Modifier): ModifierStage => { - switch (modifier.type) { - case "position": - return new PositionStage(modifier); - case "extendThroughStartOf": - return new HeadStage(modifier); - case "extendThroughEndOf": - return new TailStage(modifier); - case "toRawSelection": - return new RawSelectionStage(modifier); - case "interiorOnly": - return new InteriorOnlyStage(modifier); - case "excludeInterior": - return new ExcludeInteriorStage(modifier); - case "leading": - return new LeadingStage(modifier); - case "trailing": - return new TrailingStage(modifier); - case "containingScope": - return new ContainingScopeStage(modifier); - case "everyScope": - return new EveryScopeStage(modifier); - case "ordinalScope": - return new OrdinalScopeStage(modifier); - case "relativeScope": - return new RelativeScopeStage(modifier); - case "keepContentFilter": - return new KeepContentFilterStage(modifier); - case "keepEmptyFilter": - return new KeepEmptyFilterStage(modifier); - case "cascading": - return new CascadingStage(modifier); - case "modifyIfUntyped": - return new ModifyIfUntypedStage(modifier); - case "range": - return new RangeModifierStage(modifier); - case "inferPreviousMark": - throw Error( - `Unexpected modifier '${modifier.type}'; it should have been removed during inference`, - ); - } -}; diff --git a/packages/cursorless-engine/src/processTargets/index.ts b/packages/cursorless-engine/src/processTargets/index.ts index c3ac65a39c..0b6b97ae96 100644 --- a/packages/cursorless-engine/src/processTargets/index.ts +++ b/packages/cursorless-engine/src/processTargets/index.ts @@ -1,2 +1 @@ -import processTargets from "./processTargets"; -export default processTargets; +export * from "./TargetPipeline"; diff --git a/packages/cursorless-engine/src/processTargets/marks/RangeMarkStage.ts b/packages/cursorless-engine/src/processTargets/marks/RangeMarkStage.ts index 357088303c..4ca926c7dd 100644 --- a/packages/cursorless-engine/src/processTargets/marks/RangeMarkStage.ts +++ b/packages/cursorless-engine/src/processTargets/marks/RangeMarkStage.ts @@ -1,16 +1,19 @@ -import { Target } from "../../typings/target.types"; import { RangeMark } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getMarkStage from "../getMarkStage"; +import { Target } from "../../typings/target.types"; +import { MarkStageFactory } from "../MarkStageFactory"; import { MarkStage } from "../PipelineStages.types"; -import { targetsToContinuousTarget } from "../processTargets"; +import { targetsToContinuousTarget } from "../TargetPipeline"; export default class RangeMarkStage implements MarkStage { - constructor(private mark: RangeMark) {} + constructor( + private markStageFactory: MarkStageFactory, + private mark: RangeMark, + ) {} run(context: ProcessedTargetsContext): Target[] { - const anchorStage = getMarkStage(this.mark.anchor); - const activeStage = getMarkStage(this.mark.active); + const anchorStage = this.markStageFactory.create(this.mark.anchor); + const activeStage = this.markStageFactory.create(this.mark.active); const anchorTargets = anchorStage.run(context); const activeTargets = activeStage.run(context); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/CascadingStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/CascadingStage.ts index ee586e8c33..5cee8ee766 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/CascadingStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/CascadingStage.ts @@ -1,7 +1,7 @@ -import { Target } from "../../typings/target.types"; import { CascadingModifier } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getModifierStage from "../getModifierStage"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; /** @@ -11,11 +11,16 @@ import { ModifierStage } from "../PipelineStages.types"; export default class CascadingStage implements ModifierStage { private nestedStages_?: ModifierStage[]; - constructor(private modifier: CascadingModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: CascadingModifier, + ) {} private get nestedStages() { if (this.nestedStages_ == null) { - this.nestedStages_ = this.modifier.modifiers.map(getModifierStage); + this.nestedStages_ = this.modifier.modifiers.map( + this.modifierStageFactory.create, + ); } return this.nestedStages_; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ConditionalModifierStages.ts b/packages/cursorless-engine/src/processTargets/modifiers/ConditionalModifierStages.ts index 6da3b294c2..a9855ad637 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ConditionalModifierStages.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ConditionalModifierStages.ts @@ -1,14 +1,17 @@ import { Modifier, ModifyIfUntypedModifier } from "@cursorless/common"; -import { Target } from "../../typings/target.types"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getModifierStage from "../getModifierStage"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; abstract class ConditionalModifierBaseStage implements ModifierStage { private nestedStage_?: ModifierStage; protected suppressErrors = false; - constructor(private nestedModifier: Modifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private nestedModifier: Modifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { if (this.shouldModify(target)) { @@ -31,7 +34,7 @@ abstract class ConditionalModifierBaseStage implements ModifierStage { private get nestedStage() { if (this.nestedStage_ == null) { - this.nestedStage_ = getModifierStage(this.nestedModifier); + this.nestedStage_ = this.modifierStageFactory.create(this.nestedModifier); } return this.nestedStage_; @@ -45,8 +48,11 @@ abstract class ConditionalModifierBaseStage implements ModifierStage { * scope type, ie if {@link Target.hasExplicitScopeType} is `false`. */ export class ModifyIfUntypedStage extends ConditionalModifierBaseStage { - constructor(modifier: ModifyIfUntypedModifier) { - super(modifier.modifier); + constructor( + modifierStageFactory: ModifierStageFactory, + modifier: ModifyIfUntypedModifier, + ) { + super(modifierStageFactory, modifier.modifier); } protected shouldModify(target: Target): boolean { @@ -77,8 +83,11 @@ export class ModifyIfUntypedExplicitStage extends ConditionalModifierBaseStage { export class ContainingTokenIfUntypedEmptyStage extends ConditionalModifierBaseStage { suppressErrors = true; - constructor() { - super({ type: "containingScope", scopeType: { type: "token" } }); + constructor(modifierStageFactory: ModifierStageFactory) { + super(modifierStageFactory, { + type: "containingScope", + scopeType: { type: "token" }, + }); } protected shouldModify(target: Target): boolean { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts index d440d4cc99..d24247fc32 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/ContainingScopeStage.ts @@ -1,16 +1,16 @@ +import type { ContainingScopeModifier, Direction } from "@cursorless/common"; import { NoContainingScopeError, Position, TextEditor, } from "@cursorless/common"; -import type { ContainingScopeModifier, Direction } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; import type { ProcessedTargetsContext } from "../../typings/Types"; +import type { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getContainingScope } from "./getContainingScope"; -import getLegacyScopeStage from "./getLegacyScopeStage"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { TargetScope } from "./scopeHandlers/scope.types"; import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; @@ -33,7 +33,11 @@ import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; * input target content range. */ export class ContainingScopeStage implements ModifierStage { - constructor(private modifier: ContainingScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + private modifier: ContainingScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { const { @@ -43,13 +47,15 @@ export class ContainingScopeStage implements ModifierStage { } = target; const { scopeType, ancestorIndex = 0 } = this.modifier; - const scopeHandler = getScopeHandler( + const scopeHandler = this.scopeHandlerFactory.create( scopeType, target.editor.document.languageId, ); if (scopeHandler == null) { - return getLegacyScopeStage(this.modifier).run(context, target); + return this.modifierStageFactory + .getLegacyScopeStage(this.modifier) + .run(context, target); } if (end.isEqual(start)) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts index 6f8f18b8dc..997c75bacc 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/EveryScopeStage.ts @@ -1,11 +1,10 @@ -import { NoContainingScopeError, Range } from "@cursorless/common"; import type { EveryScopeModifier } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; +import { NoContainingScopeError, Range } from "@cursorless/common"; import type { ProcessedTargetsContext } from "../../typings/Types"; -import getModifierStage from "../getModifierStage"; +import type { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; -import getLegacyScopeStage from "./getLegacyScopeStage"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import getScopesOverlappingRange from "./scopeHandlers/getScopesOverlappingRange"; import { TargetScope } from "./scopeHandlers/scope.types"; import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; @@ -31,16 +30,25 @@ import { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; * to the expanded target's {@link Target.contentRange}. */ export class EveryScopeStage implements ModifierStage { - constructor(private modifier: EveryScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + private modifier: EveryScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { const { scopeType } = this.modifier; const { editor, isReversed } = target; - const scopeHandler = getScopeHandler(scopeType, editor.document.languageId); + const scopeHandler = this.scopeHandlerFactory.create( + scopeType, + editor.document.languageId, + ); if (scopeHandler == null) { - return getLegacyScopeStage(this.modifier).run(context, target); + return this.modifierStageFactory + .getLegacyScopeStage(this.modifier) + .run(context, target); } let scopes: TargetScope[] | undefined; @@ -85,7 +93,7 @@ export class EveryScopeStage implements ModifierStage { scopeHandler: ScopeHandler, target: Target, ): Range { - const containingIterationScopeModifier = getModifierStage({ + const containingIterationScopeModifier = this.modifierStageFactory.create({ type: "containingScope", scopeType: scopeHandler.iterationScopeType, }); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts index 6ebcf9574b..dc6ee54770 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts @@ -1,16 +1,20 @@ -import { Range } from "@cursorless/common"; -import { Target } from "../../typings/target.types"; -import { HeadTailModifier, Modifier } from "@cursorless/common"; +import { HeadTailModifier, Modifier, Range } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; import { getModifierStagesFromTargetModifiers, processModifierStages, -} from "../processTargets"; +} from "../TargetPipeline"; import { TokenTarget } from "../targets"; abstract class HeadTailStage implements ModifierStage { - constructor(private isReversed: boolean, private modifiers?: Modifier[]) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private isReversed: boolean, + private modifiers?: Modifier[], + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { const modifiers = this.modifiers ?? [ @@ -20,7 +24,10 @@ abstract class HeadTailStage implements ModifierStage { }, ]; - const modifierStages = getModifierStagesFromTargetModifiers(modifiers); + const modifierStages = getModifierStagesFromTargetModifiers( + this.modifierStageFactory, + modifiers, + ); const modifiedTargets = processModifierStages(context, modifierStages, [ target, ]); @@ -46,8 +53,11 @@ abstract class HeadTailStage implements ModifierStage { } export class HeadStage extends HeadTailStage { - constructor(modifier: HeadTailModifier) { - super(true, modifier.modifiers); + constructor( + modifierStageFactory: ModifierStageFactory, + modifier: HeadTailModifier, + ) { + super(modifierStageFactory, true, modifier.modifiers); } protected constructContentRange(originalRange: Range, modifiedRange: Range) { @@ -56,8 +66,11 @@ export class HeadStage extends HeadTailStage { } export class TailStage extends HeadTailStage { - constructor(modifier: HeadTailModifier) { - super(false, modifier.modifiers); + constructor( + modifierStageFactory: ModifierStageFactory, + modifier: HeadTailModifier, + ) { + super(modifierStageFactory, false, modifier.modifiers); } protected constructContentRange(originalRange: Range, modifiedRange: Range) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts index bae32098f1..960d5a4bd6 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/InteriorStage.ts @@ -1,27 +1,36 @@ -import { Target } from "../../typings/target.types"; import { ExcludeInteriorModifier, InteriorOnlyModifier, } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; -import { containingSurroundingPairIfUntypedStage } from "./commonContainingScopeIfUntypedStages"; +import { containingSurroundingPairIfUntypedModifier } from "./commonContainingScopeIfUntypedModifiers"; export class InteriorOnlyStage implements ModifierStage { - constructor(private modifier: InteriorOnlyModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: InteriorOnlyModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - return containingSurroundingPairIfUntypedStage + return this.modifierStageFactory + .create(containingSurroundingPairIfUntypedModifier) .run(context, target) .flatMap((target) => target.getInteriorStrict()); } } export class ExcludeInteriorStage implements ModifierStage { - constructor(private modifier: ExcludeInteriorModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: ExcludeInteriorModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - return containingSurroundingPairIfUntypedStage + return this.modifierStageFactory + .create(containingSurroundingPairIfUntypedModifier) .run(context, target) .flatMap((target) => target.getBoundaryStrict()); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/LeadingTrailingStages.ts b/packages/cursorless-engine/src/processTargets/modifiers/LeadingTrailingStages.ts index b448497b30..cf712c77ab 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/LeadingTrailingStages.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/LeadingTrailingStages.ts @@ -1,8 +1,9 @@ -import { Target } from "../../typings/target.types"; import { LeadingModifier, TrailingModifier } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; -import { containingTokenIfUntypedStage } from "./commonContainingScopeIfUntypedStages"; +import { containingTokenIfUntypedModifier } from "./commonContainingScopeIfUntypedModifiers"; /** * Throw this error if user has requested leading or trailing delimiter but no @@ -16,29 +17,41 @@ class NoDelimiterError extends Error { } export class LeadingStage implements ModifierStage { - constructor(private modifier: LeadingModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: LeadingModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - return containingTokenIfUntypedStage.run(context, target).map((target) => { - const leading = target.getLeadingDelimiterTarget(); - if (leading == null) { - throw new NoDelimiterError("leading"); - } - return leading; - }); + return this.modifierStageFactory + .create(containingTokenIfUntypedModifier) + .run(context, target) + .map((target) => { + const leading = target.getLeadingDelimiterTarget(); + if (leading == null) { + throw new NoDelimiterError("leading"); + } + return leading; + }); } } export class TrailingStage implements ModifierStage { - constructor(private modifier: TrailingModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: TrailingModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - return containingTokenIfUntypedStage.run(context, target).map((target) => { - const trailing = target.getTrailingDelimiterTarget(); - if (trailing == null) { - throw new NoDelimiterError("trailing"); - } - return trailing; - }); + return this.modifierStageFactory + .create(containingTokenIfUntypedModifier) + .run(context, target) + .map((target) => { + const trailing = target.getTrailingDelimiterTarget(); + if (trailing == null) { + throw new NoDelimiterError("trailing"); + } + return trailing; + }); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts index d885e6923a..3df0f7ece7 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/OrdinalScopeStage.ts @@ -1,6 +1,7 @@ -import { Target } from "../../typings/target.types"; import { OrdinalScopeModifier } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; import { createRangeTargetFromIndices, @@ -8,10 +9,14 @@ import { } from "./targetSequenceUtils"; export class OrdinalScopeStage implements ModifierStage { - constructor(private modifier: OrdinalScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: OrdinalScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { const targets = getEveryScopeTargets( + this.modifierStageFactory, context, target, this.modifier.scopeType, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RangeModifierStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RangeModifierStage.ts index 25e1d59f13..39f3ed09d9 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RangeModifierStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RangeModifierStage.ts @@ -1,16 +1,19 @@ -import { Target } from "../../typings/target.types"; import { RangeModifier } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getModifierStage from "../getModifierStage"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; -import { targetsToContinuousTarget } from "../processTargets"; +import { targetsToContinuousTarget } from "../TargetPipeline"; export default class RangeModifierStage implements ModifierStage { - constructor(private modifier: RangeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: RangeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const anchorStage = getModifierStage(this.modifier.anchor); - const activeStage = getModifierStage(this.modifier.active); + const anchorStage = this.modifierStageFactory.create(this.modifier.anchor); + const activeStage = this.modifierStageFactory.create(this.modifier.active); const anchorTargets = anchorStage.run(context, target); const activeTargets = activeStage.run(context, target); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts index 3886c01ee5..edbda2c88e 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeExclusiveScopeStage.ts @@ -1,10 +1,11 @@ -import type { Target } from "../../typings/target.types"; import type { RelativeScopeModifier } from "@cursorless/common"; import type { ProcessedTargetsContext } from "../../typings/Types"; +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 getScopeHandler from "./scopeHandlers/getScopeHandler"; +import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import { TargetScope } from "./scopeHandlers/scope.types"; import type { ContainmentPolicy } from "./scopeHandlers/scopeHandler.types"; import { OutOfRangeError } from "./targetSequenceUtils"; @@ -16,16 +17,25 @@ import { OutOfRangeError } from "./targetSequenceUtils"; * first scope if input range is empty and is at start of that scope. */ export default class RelativeExclusiveScopeStage implements ModifierStage { - constructor(private modifier: RelativeScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + private modifier: RelativeScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const scopeHandler = getScopeHandler( + const scopeHandler = this.scopeHandlerFactory.create( this.modifier.scopeType, target.editor.document.languageId, ); if (scopeHandler == null) { - return runLegacy(this.modifier, context, target); + return runLegacy( + this.modifierStageFactory, + this.modifier, + context, + target, + ); } const { isReversed, editor, contentRange: inputRange } = target; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts index ecceb2da2b..b370c32cf8 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeInclusiveScopeStage.ts @@ -1,17 +1,18 @@ -import { NoContainingScopeError, Range, TextEditor } from "@cursorless/common"; import type { Direction, RelativeScopeModifier } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; +import { NoContainingScopeError, Range, TextEditor } from "@cursorless/common"; import type { ProcessedTargetsContext } from "../../typings/Types"; +import type { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import type { ModifierStage } from "../PipelineStages.types"; +import { TooFewScopesError } from "./TooFewScopesError"; import { constructScopeRangeTarget } from "./constructScopeRangeTarget"; import { getContainingScope } from "./getContainingScope"; import { runLegacy } from "./relativeScopeLegacy"; -import getScopeHandler from "./scopeHandlers/getScopeHandler"; +import { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; import getScopeRelativeToPosition from "./scopeHandlers/getScopeRelativeToPosition"; import getScopesOverlappingRange from "./scopeHandlers/getScopesOverlappingRange"; import type { TargetScope } from "./scopeHandlers/scope.types"; import type { ScopeHandler } from "./scopeHandlers/scopeHandler.types"; -import { TooFewScopesError } from "./TooFewScopesError"; /** * Handles relative modifiers that include targets intersecting with the input, @@ -37,16 +38,25 @@ import { TooFewScopesError } from "./TooFewScopesError"; * direction is backward. */ export class RelativeInclusiveScopeStage implements ModifierStage { - constructor(private modifier: RelativeScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private scopeHandlerFactory: ScopeHandlerFactory, + private modifier: RelativeScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const scopeHandler = getScopeHandler( + const scopeHandler = this.scopeHandlerFactory.create( this.modifier.scopeType, target.editor.document.languageId, ); if (scopeHandler == null) { - return runLegacy(this.modifier, context, target); + return runLegacy( + this.modifierStageFactory, + this.modifier, + context, + target, + ); } const { isReversed, editor, contentRange: inputRange } = target; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts index 6d92d84957..91fe9387ca 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/RelativeScopeStage.ts @@ -1,9 +1,11 @@ -import type { Target } from "../../typings/target.types"; import type { RelativeScopeModifier } from "@cursorless/common"; import type { ProcessedTargetsContext } from "../../typings/Types"; +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 { ScopeHandlerFactory } from "./scopeHandlers/ScopeHandlerFactory"; /** * Implements relative scope modifiers like "next funk", "two tokens", etc. @@ -14,11 +16,23 @@ import { RelativeInclusiveScopeStage } from "./RelativeInclusiveScopeStage"; */ export default class RelativeScopeStage implements ModifierStage { private modiferStage: ModifierStage; - constructor(private modifier: RelativeScopeModifier) { + constructor( + modifierStageFactory: ModifierStageFactory, + scopeHandlerFactory: ScopeHandlerFactory, + modifier: RelativeScopeModifier, + ) { this.modiferStage = - this.modifier.offset === 0 - ? new RelativeInclusiveScopeStage(modifier) - : new RelativeExclusiveScopeStage(modifier); + modifier.offset === 0 + ? new RelativeInclusiveScopeStage( + modifierStageFactory, + scopeHandlerFactory, + modifier, + ) + : new RelativeExclusiveScopeStage( + modifierStageFactory, + scopeHandlerFactory, + modifier, + ); } run(context: ProcessedTargetsContext, target: Target): Target[] { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedStages.ts b/packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedModifiers.ts similarity index 72% rename from packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedStages.ts rename to packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedModifiers.ts index 1718d5879f..01eb757f6a 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedStages.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/commonContainingScopeIfUntypedModifiers.ts @@ -1,4 +1,4 @@ -import { ModifyIfUntypedStage } from "./ConditionalModifierStages"; +import { Modifier } from "@cursorless/common"; // NB: We import `Target` below just so that @link below resolves. Once one of // the following issues are fixed, we can either remove the above line or // switch to `{import("foo")}` syntax in the `{@link}` tag. @@ -12,36 +12,34 @@ import type { Target } from "../../typings/target.types"; * target has no explicit scope type, ie if {@link Target.hasExplicitScopeType} * is `false`. */ -export const containingSurroundingPairIfUntypedStage = new ModifyIfUntypedStage( - { - type: "modifyIfUntyped", - modifier: { - type: "containingScope", - scopeType: { type: "surroundingPair", delimiter: "any" }, - }, +export const containingSurroundingPairIfUntypedModifier: Modifier = { + type: "modifyIfUntyped", + modifier: { + type: "containingScope", + scopeType: { type: "surroundingPair", delimiter: "any" }, }, -); +}; /** * Expands the given target to the nearest containing line if the target has no * explicit scope type, ie if {@link Target.hasExplicitScopeType} is `false`. */ -export const containingLineIfUntypedStage = new ModifyIfUntypedStage({ +export const containingLineIfUntypedModifier: Modifier = { type: "modifyIfUntyped", modifier: { type: "containingScope", scopeType: { type: "line" }, }, -}); +}; /** * Expands the given target to the nearest containing token if the target has no * explicit scope type, ie if {@link Target.hasExplicitScopeType} is `false`. */ -export const containingTokenIfUntypedStage = new ModifyIfUntypedStage({ +export const containingTokenIfUntypedModifier: Modifier = { type: "modifyIfUntyped", modifier: { type: "containingScope", scopeType: { type: "token" }, }, -}); +}; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/getLegacyScopeStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/getLegacyScopeStage.ts deleted file mode 100644 index 7ab223c599..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/getLegacyScopeStage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { - ContainingScopeModifier, - EveryScopeModifier, - SurroundingPairModifier, -} from "@cursorless/common"; -import type { ModifierStage } from "../PipelineStages.types"; -import ItemStage from "./ItemStage"; -import BoundedNonWhitespaceSequenceStage from "./scopeTypeStages/BoundedNonWhitespaceStage"; -import ContainingSyntaxScopeStage, { - SimpleContainingScopeModifier, - SimpleEveryScopeModifier, -} from "./scopeTypeStages/ContainingSyntaxScopeStage"; -import NotebookCellStage from "./scopeTypeStages/NotebookCellStage"; -import { - CustomRegexModifier, - CustomRegexStage, - NonWhitespaceSequenceStage, - UrlStage, -} from "./scopeTypeStages/RegexStage"; -import SurroundingPairStage from "./SurroundingPairStage"; - -/** - * Any scope type that has not been fully migrated to the new - * {@link ScopeHandler} setup should have a branch in this `switch` statement. - * Once the scope type is fully migrated, remove the branch and the legacy - * modifier stage. - * - * Note that it is possible for a scope type to be partially migrated. For - * example, we could support modern scope handlers for a certain scope type in - * Ruby, but not yet in Python. - * - * @param modifier The modifier for which to get the modifier stage - * @returns A scope stage implementing the modifier for the given scope type - */ -export default function getLegacyScopeStage( - modifier: ContainingScopeModifier | EveryScopeModifier, -): ModifierStage { - switch (modifier.scopeType.type) { - case "notebookCell": - return new NotebookCellStage(modifier); - case "nonWhitespaceSequence": - return new NonWhitespaceSequenceStage(modifier); - case "boundedNonWhitespaceSequence": - return new BoundedNonWhitespaceSequenceStage(modifier); - case "url": - return new UrlStage(modifier); - case "collectionItem": - return new ItemStage(modifier); - case "customRegex": - return new CustomRegexStage(modifier as CustomRegexModifier); - case "surroundingPair": - return new SurroundingPairStage(modifier as SurroundingPairModifier); - default: - // Default to containing syntax scope using tree sitter - return new ContainingSyntaxScopeStage( - modifier as SimpleContainingScopeModifier | SimpleEveryScopeModifier, - ); - } -} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts index 90a6d366cb..a67bddca67 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/relativeScopeLegacy.ts @@ -1,8 +1,8 @@ -import { Range } from "@cursorless/common"; +import { Range, RelativeScopeModifier } from "@cursorless/common"; import { findLastIndex } from "lodash"; import { Target } from "../../typings/target.types"; -import { RelativeScopeModifier } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; import { UntypedTarget } from "../targets"; import { createRangeTargetFromIndices, @@ -17,6 +17,7 @@ interface ContainingIndices { } export function runLegacy( + modifierStageFactory: ModifierStageFactory, modifier: RelativeScopeModifier, context: ProcessedTargetsContext, target: Target, @@ -31,6 +32,7 @@ export function runLegacy( * that will rely on #629 */ const targets = getEveryScopeTargets( + modifierStageFactory, context, createTargetWithoutExplicitRange(target), modifier.scopeType, diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts index 0847b552b9..d6a718c34b 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/NestedScopeHandler.ts @@ -1,8 +1,8 @@ +import type { Direction, ScopeType } from "@cursorless/common"; import { Position, TextEditor } from "@cursorless/common"; import { flatmap } from "itertools"; -import { getScopeHandler } from "."; -import type { Direction, ScopeType } from "@cursorless/common"; import BaseScopeHandler from "./BaseScopeHandler"; +import { ScopeHandlerFactory } from "./ScopeHandlerFactory"; import type { TargetScope } from "./scope.types"; import type { ScopeHandler, @@ -51,6 +51,7 @@ export default abstract class NestedScopeHandler extends BaseScopeHandler { private _searchScopeHandler: ScopeHandler | undefined; constructor( + private scopeHandlerFactory: ScopeHandlerFactory, public readonly scopeType: ScopeType, protected languageId: string, ) { @@ -59,7 +60,7 @@ export default abstract class NestedScopeHandler extends BaseScopeHandler { private get searchScopeHandler(): ScopeHandler { if (this._searchScopeHandler == null) { - this._searchScopeHandler = getScopeHandler( + this._searchScopeHandler = this.scopeHandlerFactory.create( this.searchScopeType, this.languageId, )!; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts index 84f1519b8d..d13eb639b2 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts @@ -1,9 +1,13 @@ -import { getScopeHandler } from "."; -import { TextEditor, Position } from "@cursorless/common"; -import { Direction, OneOfScopeType } from "@cursorless/common"; +import { + Direction, + OneOfScopeType, + Position, + TextEditor, +} from "@cursorless/common"; import BaseScopeHandler from "./BaseScopeHandler"; +import { advanceIteratorsUntil, getInitialIteratorInfos } from "./IteratorInfo"; +import { ScopeHandlerFactory } from "./ScopeHandlerFactory"; import { compareTargetScopes } from "./compareTargetScopes"; -import { getInitialIteratorInfos, advanceIteratorsUntil } from "./IteratorInfo"; import type { TargetScope } from "./scope.types"; import { ScopeHandler, ScopeIteratorRequirements } from "./scopeHandler.types"; @@ -12,7 +16,10 @@ export default class OneOfScopeHandler extends BaseScopeHandler { private scopeHandlers: ScopeHandler[] = this.scopeType.scopeTypes.map( (scopeType) => { - const handler = getScopeHandler(scopeType, this.languageId); + const handler = this.scopeHandlerFactory.create( + scopeType, + this.languageId, + ); if (handler == null) { throw new Error(`No available scope handler for '${scopeType.type}'`); } @@ -28,6 +35,7 @@ export default class OneOfScopeHandler extends BaseScopeHandler { }; constructor( + private scopeHandlerFactory: ScopeHandlerFactory, public readonly scopeType: OneOfScopeType, private languageId: string, ) { diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts new file mode 100644 index 0000000000..2aa242f74c --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactory.ts @@ -0,0 +1,6 @@ +import type { ScopeType } from "@cursorless/common"; +import type { ScopeHandler } from "./scopeHandler.types"; + +export interface ScopeHandlerFactory { + create(scopeType: ScopeType, languageId: string): ScopeHandler | undefined; +} diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts similarity index 50% rename from packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts rename to packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts index e368a6fab8..6e2a069af1 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/getScopeHandler.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/ScopeHandlerFactoryImpl.ts @@ -10,6 +10,7 @@ import { } from "."; import type { ScopeType } from "@cursorless/common"; import type { ScopeHandler } from "./scopeHandler.types"; +import { ScopeHandlerFactory } from "./ScopeHandlerFactory"; /** * Returns a scope handler for the given scope type and language id, or @@ -28,28 +29,31 @@ import type { ScopeHandler } from "./scopeHandler.types"; * undefined if the given scope type / language id combination is still using * legacy pathways */ -export default function getScopeHandler( - scopeType: ScopeType, - languageId: string, -): ScopeHandler | undefined { - switch (scopeType.type) { - case "character": - return new CharacterScopeHandler(scopeType, languageId); - case "word": - return new WordScopeHandler(scopeType, languageId); - case "token": - return new TokenScopeHandler(scopeType, languageId); - case "identifier": - return new IdentifierScopeHandler(scopeType, languageId); - case "line": - return new LineScopeHandler(scopeType, languageId); - case "document": - return new DocumentScopeHandler(scopeType, languageId); - case "oneOf": - return new OneOfScopeHandler(scopeType, languageId); - case "paragraph": - return new ParagraphScopeHandler(scopeType, languageId); - default: - return undefined; +export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory { + constructor() { + this.create = this.create.bind(this); + } + + create(scopeType: ScopeType, languageId: string): ScopeHandler | undefined { + switch (scopeType.type) { + case "character": + return new CharacterScopeHandler(this, scopeType, languageId); + case "word": + return new WordScopeHandler(this, scopeType, languageId); + case "token": + return new TokenScopeHandler(this, scopeType, languageId); + case "identifier": + return new IdentifierScopeHandler(this, scopeType, languageId); + case "line": + return new LineScopeHandler(scopeType, languageId); + case "document": + return new DocumentScopeHandler(scopeType, languageId); + case "oneOf": + return new OneOfScopeHandler(this, scopeType, languageId); + case "paragraph": + return new ParagraphScopeHandler(scopeType, languageId); + default: + return undefined; + } } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/index.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/index.ts index 507cf894c0..b3777c6318 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/index.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/index.ts @@ -16,5 +16,4 @@ export * from "./OneOfScopeHandler"; export { default as OneOfScopeHandler } from "./OneOfScopeHandler"; export * from "./ParagraphScopeHandler"; export { default as ParagraphScopeHandler } from "./ParagraphScopeHandler"; -export * from "./getScopeHandler"; -export { default as getScopeHandler } from "./getScopeHandler"; +export * from "./ScopeHandlerFactoryImpl"; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts index 65683b8782..84c0e7fe56 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/scopeTypeStages/BoundedNonWhitespaceStage.ts @@ -1,11 +1,11 @@ -import { NoContainingScopeError } from "@cursorless/common"; import { ContainingScopeModifier, EveryScopeModifier, + NoContainingScopeError, } from "@cursorless/common"; -import { Target } from "../../../typings/target.types"; import { ProcessedTargetsContext } from "../../../typings/Types"; -import getModifierStage from "../../getModifierStage"; +import { Target } from "../../../typings/target.types"; +import { ModifierStageFactory } from "../../ModifierStageFactory"; import { ModifierStage } from "../../PipelineStages.types"; import { TokenTarget } from "../../targets"; import { processSurroundingPair } from "../surroundingPair"; @@ -18,10 +18,13 @@ import { processSurroundingPair } from "../surroundingPair"; export default class BoundedNonWhitespaceSequenceStage implements ModifierStage { - constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} + constructor( + private modifierStageFactory: ModifierStageFactory, + private modifier: ContainingScopeModifier | EveryScopeModifier, + ) {} run(context: ProcessedTargetsContext, target: Target): Target[] { - const paintStage = getModifierStage({ + const paintStage = this.modifierStageFactory.create({ type: this.modifier.type, scopeType: { type: "nonWhitespaceSequence" }, }); diff --git a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts index e7753f7ea7..06d3b6729c 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/targetSequenceUtils.ts @@ -1,7 +1,7 @@ -import { Target } from "../../typings/target.types"; import { ScopeType } from "@cursorless/common"; import { ProcessedTargetsContext } from "../../typings/Types"; -import getModifierStage from "../getModifierStage"; +import { Target } from "../../typings/target.types"; +import { ModifierStageFactory } from "../ModifierStageFactory"; export class OutOfRangeError extends Error { constructor() { @@ -42,11 +42,12 @@ export function createRangeTargetFromIndices( } export function getEveryScopeTargets( + modifierStageFactory: ModifierStageFactory, context: ProcessedTargetsContext, target: Target, scopeType: ScopeType, ): Target[] { - const containingStage = getModifierStage({ + const containingStage = modifierStageFactory.create({ type: "everyScope", scopeType, }); diff --git a/packages/cursorless-engine/src/processTargets/processTargets.ts b/packages/cursorless-engine/src/processTargets/processTargets.ts deleted file mode 100644 index f3fa79ecbc..0000000000 --- a/packages/cursorless-engine/src/processTargets/processTargets.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Range } from "@cursorless/common"; -import { uniqWith, zip } from "lodash"; -import { ImplicitTargetDescriptor, Modifier } from "@cursorless/common"; -import { Target } from "../typings/target.types"; -import { - PrimitiveTargetDescriptor, - RangeTargetDescriptor, - TargetDescriptor, -} from "../typings/TargetDescriptor"; -import { ProcessedTargetsContext } from "../typings/Types"; -import getMarkStage from "./getMarkStage"; -import getModifierStage from "./getModifierStage"; -import ImplicitStage from "./marks/ImplicitStage"; -import { ContainingTokenIfUntypedEmptyStage } from "./modifiers/ConditionalModifierStages"; -import { MarkStage, ModifierStage } from "./PipelineStages.types"; -import { PlainTarget, PositionTarget } from "./targets"; - -/** - * Converts the abstract target descriptions provided by the user to a concrete - * representation usable by actions. Conceptually, the input will be something - * like "the function call argument containing the cursor" and the output will be something - * like "line 3, characters 5 through 10". - * @param context Captures the environment needed to convert the abstract target - * description given by the user to a concrete representation usable by - * actions - * @param targets The abstract target representations provided by the user - * @returns A list of lists of typed selections, one list per input target. Each - * typed selection includes the selection, as well the uri of the document - * containing it, and potentially rich context information such as how to remove - * the target - */ -export default function ( - context: ProcessedTargetsContext, - targets: TargetDescriptor[], -): Target[][] { - return targets.map((target) => uniqTargets(processTarget(context, target))); -} - -function processTarget( - context: ProcessedTargetsContext, - target: TargetDescriptor, -): Target[] { - switch (target.type) { - case "list": - return target.elements.flatMap((element) => - processTarget(context, element), - ); - case "range": - return processRangeTarget(context, target); - case "primitive": - case "implicit": - return processPrimitiveTarget(context, target); - } -} - -function processRangeTarget( - context: ProcessedTargetsContext, - targetDesc: RangeTargetDescriptor, -): Target[] { - const anchorTargets = processPrimitiveTarget(context, targetDesc.anchor); - const activeTargets = processPrimitiveTarget(context, targetDesc.active); - - return zip(anchorTargets, activeTargets).flatMap( - ([anchorTarget, activeTarget]) => { - if (anchorTarget == null || activeTarget == null) { - throw new Error("AnchorTargets and activeTargets lengths don't match"); - } - - switch (targetDesc.rangeType) { - case "continuous": - return [ - targetsToContinuousTarget( - anchorTarget, - activeTarget, - targetDesc.excludeAnchor, - targetDesc.excludeActive, - ), - ]; - case "vertical": - return targetsToVerticalTarget( - anchorTarget, - activeTarget, - targetDesc.excludeAnchor, - targetDesc.excludeActive, - ); - } - }, - ); -} - -export function targetsToContinuousTarget( - anchorTarget: Target, - activeTarget: Target, - excludeAnchor: boolean = false, - excludeActive: boolean = false, -): Target { - ensureSingleEditor(anchorTarget, activeTarget); - - const isReversed = calcIsReversed(anchorTarget, activeTarget); - const startTarget = isReversed ? activeTarget : anchorTarget; - const endTarget = isReversed ? anchorTarget : activeTarget; - const excludeStart = isReversed ? excludeActive : excludeAnchor; - const excludeEnd = isReversed ? excludeAnchor : excludeActive; - - return startTarget.createContinuousRangeTarget( - isReversed, - endTarget, - !excludeStart, - !excludeEnd, - ); -} - -function targetsToVerticalTarget( - anchorTarget: Target, - activeTarget: Target, - excludeAnchor: boolean, - excludeActive: boolean, -): Target[] { - ensureSingleEditor(anchorTarget, activeTarget); - - const isReversed = calcIsReversed(anchorTarget, activeTarget); - const delta = isReversed ? -1 : 1; - - const anchorPosition = isReversed - ? anchorTarget.contentRange.start - : anchorTarget.contentRange.end; - const anchorLine = anchorPosition.line + (excludeAnchor ? delta : 0); - const activePosition = isReversed - ? activeTarget.contentRange.start - : activeTarget.contentRange.end; - const activeLine = activePosition.line - (excludeActive ? delta : 0); - - const results: Target[] = []; - for (let i = anchorLine; true; i += delta) { - const contentRange = new Range( - i, - anchorTarget.contentRange.start.character, - i, - anchorTarget.contentRange.end.character, - ); - - if (anchorTarget instanceof PositionTarget) { - results.push(anchorTarget.withContentRange(contentRange)); - } else { - results.push( - new PlainTarget({ - editor: anchorTarget.editor, - isReversed: anchorTarget.isReversed, - contentRange, - }), - ); - } - - if (i === activeLine) { - return results; - } - } -} - -/** - * This function implements the modifier pipeline that is at the core of Cursorless target processing. - * It proceeds as follows: - * - * 1. It begins by getting the output from the {@link markStage} (eg "air", "this", etc). - * This output is a list of zero or more targets. - * 2. It then constructs a pipeline from the modifiers on the {@link targetDescriptor} - * 3. It then runs each pipeline stage in turn, feeding the first stage with - * the list of targets output from the {@link markStage}. For each pipeline - * stage, it passes the targets from the previous stage to the pipeline stage - * one by one. For each target, the stage will output a list of zero or more output - * targets. It then concatenates all of these lists into the list of targets - * that will be passed to the next pipeline stage. This process is similar to - * the way that [jq](https://stedolan.github.io/jq/) processes its inputs. - * - * @param context The context that captures the state of the environment used - * by each stage to process its input targets - * @param targetDescriptor The description of the target, consisting of a mark - * and zero or more modifiers - * @returns The output of running the modifier pipeline on the output from the mark - */ -function processPrimitiveTarget( - context: ProcessedTargetsContext, - targetDescriptor: PrimitiveTargetDescriptor | ImplicitTargetDescriptor, -): Target[] { - let markStage: MarkStage; - let nonPositionModifierStages: ModifierStage[]; - let positionModifierStages: ModifierStage[]; - - if (targetDescriptor.type === "implicit") { - markStage = new ImplicitStage(); - nonPositionModifierStages = []; - positionModifierStages = []; - } else { - markStage = getMarkStage(targetDescriptor.mark); - positionModifierStages = - targetDescriptor.positionModifier == null - ? [] - : [getModifierStage(targetDescriptor.positionModifier)]; - nonPositionModifierStages = getModifierStagesFromTargetModifiers( - targetDescriptor.modifiers, - ); - } - - // First, get the targets output by the mark - const markOutputTargets = markStage.run(context); - - /** - * The modifier pipeline that will be applied to construct our final targets - */ - const modifierStages = [ - ...nonPositionModifierStages, - ...context.actionPrePositionStages, - ...positionModifierStages, - ...context.actionFinalStages, - - // This performs auto-expansion to token when you say eg "take this" with an - // empty selection - new ContainingTokenIfUntypedEmptyStage(), - ]; - - // Run all targets through the modifier stages - return processModifierStages(context, modifierStages, markOutputTargets); -} - -/** Convert a list of target modifiers to modifier stages */ -export function getModifierStagesFromTargetModifiers( - targetModifiers: Modifier[], -) { - // Reverse target modifiers because they are returned in reverse order from - // the api, to match the order in which they are spoken. - return targetModifiers.map(getModifierStage).reverse(); -} - -/** Run all targets through the modifier stages */ -export function processModifierStages( - context: ProcessedTargetsContext, - modifierStages: ModifierStage[], - targets: Target[], -) { - // First we apply each stage in sequence, letting each stage see the targets - // one-by-one and concatenating the results before passing them on to the - // next stage. - modifierStages.forEach((stage) => { - targets = targets.flatMap((target) => stage.run(context, target)); - }); - - // Then return the output from the final stage - return targets; -} - -function calcIsReversed(anchor: Target, active: Target) { - if (anchor.contentRange.start.isAfter(active.contentRange.start)) { - return true; - } - if (anchor.contentRange.start.isBefore(active.contentRange.start)) { - return false; - } - return anchor.contentRange.end.isAfter(active.contentRange.end); -} - -function uniqTargets(array: Target[]): Target[] { - return uniqWith(array, (a, b) => a.isEqual(b)); -} - -function ensureSingleEditor(anchorTarget: Target, activeTarget: Target) { - if (anchorTarget.editor !== activeTarget.editor) { - throw new Error("Cannot form range between targets in different editors"); - } -} diff --git a/packages/cursorless-engine/src/util/unifyRanges.ts b/packages/cursorless-engine/src/util/unifyRanges.ts index ac32f73be3..bd1053cd48 100644 --- a/packages/cursorless-engine/src/util/unifyRanges.ts +++ b/packages/cursorless-engine/src/util/unifyRanges.ts @@ -1,4 +1,4 @@ -import { targetsToContinuousTarget } from "../processTargets/processTargets"; +import { targetsToContinuousTarget } from "../processTargets/TargetPipeline"; import { Target } from "../typings/target.types"; import { groupTargetsForEachEditor } from "./targetUtils";