diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 9949e463f7..a4b6aabba0 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -65,13 +65,17 @@ export * from "./util/CompositeKeyMap"; export * from "./ide/normalized/NormalizedIDE"; export * from "./types/command/command.types"; export * from "./types/command/PartialTargetDescriptor.types"; -export * from "./types/command/ActionCommand"; +export * from "./types/command/DestinationDescriptor.types"; +export * from "./types/command/ActionDescriptor"; export * from "./types/command/legacy/CommandV0V1.types"; export * from "./types/command/legacy/CommandV2.types"; export * from "./types/command/legacy/CommandV3.types"; export * from "./types/command/legacy/CommandV4.types"; export * from "./types/command/legacy/targetDescriptorV2.types"; -export * from "./types/command/CommandV5.types"; +export * from "./types/command/legacy/ActionCommandV5"; +export * from "./types/command/legacy/CommandV5.types"; +export * from "./types/command/legacy/PartialTargetDescriptorV5.types"; +export * from "./types/command/CommandV6.types"; export * from "./types/command/legacy/PartialTargetDescriptorV3.types"; export * from "./types/command/legacy/PartialTargetDescriptorV4.types"; export * from "./types/CommandServerApi"; diff --git a/packages/common/src/types/command/ActionDescriptor.ts b/packages/common/src/types/command/ActionDescriptor.ts new file mode 100644 index 0000000000..690b808ee9 --- /dev/null +++ b/packages/common/src/types/command/ActionDescriptor.ts @@ -0,0 +1,228 @@ +import { + PartialTargetDescriptor, + ScopeType, +} from "./PartialTargetDescriptor.types"; +import { DestinationDescriptor } from "./DestinationDescriptor.types"; + +/** + * A simple action takes only a single target and no other arguments. + */ +const simpleActionNames = [ + "clearAndSetSelection", + "copyToClipboard", + "cutToClipboard", + "deselect", + "editNewLineAfter", + "editNewLineBefore", + "experimental.setInstanceReference", + "extractVariable", + "findInWorkspace", + "foldRegion", + "followLink", + "indentLine", + "insertCopyAfter", + "insertCopyBefore", + "insertEmptyLineAfter", + "insertEmptyLineBefore", + "insertEmptyLinesAround", + "outdentLine", + "randomizeTargets", + "remove", + "rename", + "revealDefinition", + "revealTypeDefinition", + "reverseTargets", + "scrollToBottom", + "scrollToCenter", + "scrollToTop", + "setSelection", + "setSelectionAfter", + "setSelectionBefore", + "showDebugHover", + "showHover", + "showQuickFix", + "showReferences", + "sortTargets", + "toggleLineBreakpoint", + "toggleLineComment", + "unfoldRegion", +] as const; + +const complexActionNames = [ + "callAsFunction", + "editNew", + "executeCommand", + "generateSnippet", + "getText", + "highlight", + "insertSnippet", + "moveToTarget", + "pasteFromClipboard", + "replace", + "replaceWithTarget", + "rewrapWithPairedDelimiter", + "swapTargets", + "wrapWithPairedDelimiter", + "wrapWithSnippet", +] as const; + +export const actionNames = [ + ...simpleActionNames, + ...complexActionNames, +] as const; + +export type SimpleActionName = (typeof simpleActionNames)[number]; +export type ActionType = (typeof actionNames)[number]; + +/** + * A simple action takes only a single target and no other arguments. + */ +export interface SimpleActionDescriptor { + name: SimpleActionName; + target: PartialTargetDescriptor; +} + +export interface BringMoveActionDescriptor { + name: "replaceWithTarget" | "moveToTarget"; + source: PartialTargetDescriptor; + destination: DestinationDescriptor; +} + +export interface CallActionDescriptor { + name: "callAsFunction"; + + /** + * The target to use as the function to be called. + */ + callee: PartialTargetDescriptor; + + /** + * The target to wrap in a function call. + */ + argument: PartialTargetDescriptor; +} + +export interface SwapActionDescriptor { + name: "swapTargets"; + target1: PartialTargetDescriptor; + target2: PartialTargetDescriptor; +} + +export interface WrapWithPairedDelimiterActionDescriptor { + name: "wrapWithPairedDelimiter" | "rewrapWithPairedDelimiter"; + left: string; + right: string; + target: PartialTargetDescriptor; +} + +export interface PasteActionDescriptor { + name: "pasteFromClipboard"; + destination: DestinationDescriptor; +} + +export interface GenerateSnippetActionDescriptor { + name: "generateSnippet"; + snippetName?: string; + target: PartialTargetDescriptor; +} + +interface NamedInsertSnippetArg { + type: "named"; + name: string; + substitutions?: Record; +} +interface CustomInsertSnippetArg { + type: "custom"; + body: string; + scopeType?: ScopeType; + substitutions?: Record; +} +export type InsertSnippetArg = NamedInsertSnippetArg | CustomInsertSnippetArg; + +export interface InsertSnippetActionDescriptor { + name: "insertSnippet"; + snippetDescription: InsertSnippetArg; + destination: DestinationDescriptor; +} + +interface NamedWrapWithSnippetArg { + type: "named"; + name: string; + variableName: string; +} +interface CustomWrapWithSnippetArg { + type: "custom"; + body: string; + variableName?: string; + scopeType?: ScopeType; +} +export type WrapWithSnippetArg = + | NamedWrapWithSnippetArg + | CustomWrapWithSnippetArg; + +export interface WrapWithSnippetActionDescriptor { + name: "wrapWithSnippet"; + snippetDescription: WrapWithSnippetArg; + target: PartialTargetDescriptor; +} + +export interface ExecuteCommandOptions { + commandArgs?: any[]; + ensureSingleEditor?: boolean; + ensureSingleTarget?: boolean; + restoreSelection?: boolean; + showDecorations?: boolean; +} + +export interface ExecuteCommandActionDescriptor { + name: "executeCommand"; + commandId: string; + options?: ExecuteCommandOptions; + target: PartialTargetDescriptor; +} + +export type ReplaceWith = string[] | { start: number }; + +export interface ReplaceActionDescriptor { + name: "replace"; + replaceWith: ReplaceWith; + destination: DestinationDescriptor; +} + +export interface HighlightActionDescriptor { + name: "highlight"; + highlightId?: string; + target: PartialTargetDescriptor; +} + +export interface EditNewActionDescriptor { + name: "editNew"; + destination: DestinationDescriptor; +} + +export interface GetTextActionOptions { + showDecorations?: boolean; + ensureSingleTarget?: boolean; +} + +export interface GetTextActionDescriptor { + name: "getText"; + options?: GetTextActionOptions; + target: PartialTargetDescriptor; +} + +export type ActionDescriptor = + | SimpleActionDescriptor + | BringMoveActionDescriptor + | SwapActionDescriptor + | CallActionDescriptor + | PasteActionDescriptor + | ExecuteCommandActionDescriptor + | ReplaceActionDescriptor + | HighlightActionDescriptor + | GenerateSnippetActionDescriptor + | InsertSnippetActionDescriptor + | WrapWithSnippetActionDescriptor + | WrapWithPairedDelimiterActionDescriptor + | EditNewActionDescriptor + | GetTextActionDescriptor; diff --git a/packages/common/src/types/command/CommandV6.types.ts b/packages/common/src/types/command/CommandV6.types.ts new file mode 100644 index 0000000000..08b6fe1f30 --- /dev/null +++ b/packages/common/src/types/command/CommandV6.types.ts @@ -0,0 +1,27 @@ +import type { ActionDescriptor } from "./ActionDescriptor"; + +export interface CommandV6 { + /** + * The version number of the command API + */ + version: 6; + + /** + * The spoken form of the command if issued from a voice command system + */ + spokenForm?: string; + + /** + * If the command is issued from a voice command system, this boolean indicates + * whether we should use the pre phrase snapshot. Only set this to true if the + * voice command system issues a pre phrase signal at the start of every + * phrase. + */ + usePrePhraseSnapshot: boolean; + + /** + * The action to perform. This field contains everything necessary to actually + * perform the action. The other fields are just metadata. + */ + action: ActionDescriptor; +} diff --git a/packages/common/src/types/command/DestinationDescriptor.types.ts b/packages/common/src/types/command/DestinationDescriptor.types.ts new file mode 100644 index 0000000000..7d04a8b0a6 --- /dev/null +++ b/packages/common/src/types/command/DestinationDescriptor.types.ts @@ -0,0 +1,55 @@ +import { + PartialListTargetDescriptor, + PartialPrimitiveTargetDescriptor, + PartialRangeTargetDescriptor, +} from "./PartialTargetDescriptor.types"; + +/** + * The insertion mode to use when inserting relative to a target. + * - `before` inserts before the target. Depending on the target, a delimiter + * may be inserted after the inserted text. + * - `after` inserts after the target. Depending on the target, a delimiter may + * be inserted before the inserted text. + * - `to` replaces the target. However, this insertion mode may also be used + * when the target is really only a pseudo-target. For example, you could say + * `"bring type air to bat"` even if `bat` doesn't already have a type. In + * that case, `"take type bat"` wouldn't work, so `"type bat"` is really just + * a pseudo-target in that situation. + */ +export type InsertionMode = "before" | "after" | "to"; + +export interface PrimitiveDestinationDescriptor { + type: "primitive"; + + /** + * The insertion mode to use when inserting relative to {@link target}. + */ + insertionMode: InsertionMode; + + target: + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor + | PartialListTargetDescriptor; +} + +/** + * A list of destinations. This is used when the user uses more than one insertion mode + * in a single command. For example, `"bring air after bat and before cap"`. + */ +export interface ListDestinationDescriptor { + type: "list"; + destinations: PrimitiveDestinationDescriptor[]; +} + +/** + * An implicit destination. This is used for e.g. `"bring air"` (note the user + * doesn't explicitly specify the destination), or `"snip funk"`. + */ +export interface ImplicitDestinationDescriptor { + type: "implicit"; +} + +export type DestinationDescriptor = + | ListDestinationDescriptor + | PrimitiveDestinationDescriptor + | ImplicitDestinationDescriptor; diff --git a/packages/common/src/types/command/PartialTargetDescriptor.types.ts b/packages/common/src/types/command/PartialTargetDescriptor.types.ts index e4ce8faf1a..556442bdf6 100644 --- a/packages/common/src/types/command/PartialTargetDescriptor.types.ts +++ b/packages/common/src/types/command/PartialTargetDescriptor.types.ts @@ -268,21 +268,21 @@ export interface InferPreviousMarkModifier { type: "inferPreviousMark"; } -export type TargetPosition = "before" | "after" | "start" | "end"; +export interface StartOfModifier { + type: "startOf"; +} -export interface PositionModifier { - type: "position"; - position: TargetPosition; +export interface EndOfModifier { + type: "endOf"; } -export interface PartialPrimitiveTargetDescriptor { - type: "primitive"; - mark?: PartialMark; +export interface HeadModifier { + type: "extendThroughStartOf"; modifiers?: Modifier[]; } -export interface HeadTailModifier { - type: "extendThroughStartOf" | "extendThroughEndOf"; +export interface TailModifier { + type: "extendThroughEndOf"; modifiers?: Modifier[]; } @@ -326,14 +326,16 @@ export interface RangeModifier { } export type Modifier = - | PositionModifier + | StartOfModifier + | EndOfModifier | InteriorOnlyModifier | ExcludeInteriorModifier | ContainingScopeModifier | EveryScopeModifier | OrdinalScopeModifier | RelativeScopeModifier - | HeadTailModifier + | HeadModifier + | TailModifier | LeadingModifier | TrailingModifier | RawSelectionModifier @@ -348,6 +350,12 @@ export type Modifier = // vertical puts a selection on each line vertically between the two targets export type PartialRangeType = "continuous" | "vertical"; +export interface PartialPrimitiveTargetDescriptor { + type: "primitive"; + mark?: PartialMark; + modifiers?: Modifier[]; +} + export interface PartialRangeTargetDescriptor { type: "range"; anchor: PartialPrimitiveTargetDescriptor | ImplicitTargetDescriptor; diff --git a/packages/common/src/types/command/command.types.ts b/packages/common/src/types/command/command.types.ts index 665a9baca8..6dc0f9cad5 100644 --- a/packages/common/src/types/command/command.types.ts +++ b/packages/common/src/types/command/command.types.ts @@ -1,14 +1,13 @@ -import type { ActionCommand } from "./ActionCommand"; -import type { CommandV5 } from "./CommandV5.types"; +import { CommandV6 } from "./CommandV6.types"; import type { CommandV0, CommandV1 } from "./legacy/CommandV0V1.types"; import type { CommandV2 } from "./legacy/CommandV2.types"; import type { CommandV3 } from "./legacy/CommandV3.types"; import type { CommandV4 } from "./legacy/CommandV4.types"; +import type { CommandV5 } from "./legacy/CommandV5.types"; export type CommandComplete = Required> & - Pick & { action: Required }; - -export const LATEST_VERSION = 5 as const; + Pick; +export const LATEST_VERSION = 6 as const; export type CommandLatest = Command & { version: typeof LATEST_VERSION; @@ -20,4 +19,5 @@ export type Command = | CommandV2 | CommandV3 | CommandV4 - | CommandV5; + | CommandV5 + | CommandV6; diff --git a/packages/common/src/types/command/ActionCommand.ts b/packages/common/src/types/command/legacy/ActionCommandV5.ts similarity index 89% rename from packages/common/src/types/command/ActionCommand.ts rename to packages/common/src/types/command/legacy/ActionCommandV5.ts index d79ce3e468..c41469d4ba 100644 --- a/packages/common/src/types/command/ActionCommand.ts +++ b/packages/common/src/types/command/legacy/ActionCommandV5.ts @@ -1,4 +1,4 @@ -export const actionNames = [ +const actionNames = [ "callAsFunction", "clearAndSetSelection", "copyToClipboard", @@ -54,13 +54,13 @@ export const actionNames = [ "wrapWithSnippet", ] as const; -export type ActionType = (typeof actionNames)[number]; +export type ActionTypeV5 = (typeof actionNames)[number]; -export interface ActionCommand { +export interface ActionCommandV5 { /** * The action to run */ - name: ActionType; + name: ActionTypeV5; /** * A list of arguments expected by the given action. diff --git a/packages/common/src/types/command/CommandV5.types.ts b/packages/common/src/types/command/legacy/CommandV5.types.ts similarity index 74% rename from packages/common/src/types/command/CommandV5.types.ts rename to packages/common/src/types/command/legacy/CommandV5.types.ts index 33bd6c2d79..a7244088e3 100644 --- a/packages/common/src/types/command/CommandV5.types.ts +++ b/packages/common/src/types/command/legacy/CommandV5.types.ts @@ -1,5 +1,5 @@ -import type { PartialTargetDescriptor } from "./PartialTargetDescriptor.types"; -import type { ActionCommand } from "./ActionCommand"; +import type { PartialTargetDescriptorV5 } from "./PartialTargetDescriptorV5.types"; +import type { ActionCommandV5 } from "./ActionCommandV5"; export interface CommandV5 { /** @@ -20,11 +20,11 @@ export interface CommandV5 { */ usePrePhraseSnapshot: boolean; - action: ActionCommand; + action: ActionCommandV5; /** * A list of targets expected by the action. Inference will be run on the * targets */ - targets: PartialTargetDescriptor[]; + targets: PartialTargetDescriptorV5[]; } diff --git a/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts b/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts new file mode 100644 index 0000000000..9226d93874 --- /dev/null +++ b/packages/common/src/types/command/legacy/PartialTargetDescriptorV5.types.ts @@ -0,0 +1,371 @@ +interface CursorMark { + type: "cursor"; +} + +interface ThatMark { + type: "that"; +} + +interface SourceMark { + type: "source"; +} + +interface NothingMark { + type: "nothing"; +} + +interface LastCursorPositionMark { + type: "lastCursorPosition"; +} + +interface DecoratedSymbolMark { + type: "decoratedSymbol"; + symbolColor: string; + character: string; +} + +type LineNumberType = "absolute" | "relative" | "modulo100"; + +interface LineNumberMark { + type: "lineNumber"; + lineNumberType: LineNumberType; + lineNumber: number; +} + +/** + * Constructs a range between {@link anchor} and {@link active} + */ +interface RangeMark { + type: "range"; + anchor: PartialMark; + active: PartialMark; + excludeAnchor?: boolean; + excludeActive?: boolean; +} + +type PartialMark = + | CursorMark + | ThatMark + | SourceMark + | DecoratedSymbolMark + | NothingMark + | LineNumberMark + | RangeMark; + +type SimpleSurroundingPairName = + | "angleBrackets" + | "backtickQuotes" + | "curlyBrackets" + | "doubleQuotes" + | "escapedDoubleQuotes" + | "escapedParentheses" + | "escapedSquareBrackets" + | "escapedSingleQuotes" + | "parentheses" + | "singleQuotes" + | "squareBrackets"; +type ComplexSurroundingPairName = "string" | "any" | "collectionBoundary"; +type SurroundingPairName = + | SimpleSurroundingPairName + | ComplexSurroundingPairName; + +type SimpleScopeTypeType = + | "argumentOrParameter" + | "anonymousFunction" + | "attribute" + | "branch" + | "class" + | "className" + | "collectionItem" + | "collectionKey" + | "comment" + | "functionCall" + | "functionCallee" + | "functionName" + | "ifStatement" + | "instance" + | "list" + | "map" + | "name" + | "namedFunction" + | "regularExpression" + | "statement" + | "string" + | "type" + | "value" + | "condition" + | "section" + | "sectionLevelOne" + | "sectionLevelTwo" + | "sectionLevelThree" + | "sectionLevelFour" + | "sectionLevelFive" + | "sectionLevelSix" + | "selector" + | "switchStatementSubject" + | "unit" + | "xmlBothTags" + | "xmlElement" + | "xmlEndTag" + | "xmlStartTag" + // Latex scope types + | "part" + | "chapter" + | "subSection" + | "subSubSection" + | "namedParagraph" + | "subParagraph" + | "environment" + // Text based scopes + | "token" + | "line" + | "notebookCell" + | "paragraph" + | "document" + | "character" + | "word" + | "identifier" + | "nonWhitespaceSequence" + | "boundedNonWhitespaceSequence" + | "url"; + +interface SimpleScopeType { + type: SimpleScopeTypeType; +} + +interface CustomRegexScopeType { + type: "customRegex"; + regex: string; +} + +type SurroundingPairDirection = "left" | "right"; +interface SurroundingPairScopeType { + type: "surroundingPair"; + delimiter: SurroundingPairName; + forceDirection?: SurroundingPairDirection; + + /** + * If `true`, then only accept pairs where the pair completely contains the + * selection, ie without the edges touching. + */ + requireStrongContainment?: boolean; +} + +interface OneOfScopeType { + type: "oneOf"; + scopeTypes: ScopeType[]; +} + +type ScopeType = + | SimpleScopeType + | SurroundingPairScopeType + | CustomRegexScopeType + | OneOfScopeType; + +interface ContainingSurroundingPairModifier extends ContainingScopeModifier { + scopeType: SurroundingPairScopeType; +} + +interface EverySurroundingPairModifier extends EveryScopeModifier { + scopeType: SurroundingPairScopeType; +} + +type SurroundingPairModifier = + | ContainingSurroundingPairModifier + | EverySurroundingPairModifier; + +interface InteriorOnlyModifier { + type: "interiorOnly"; +} + +interface ExcludeInteriorModifier { + type: "excludeInterior"; +} + +interface ContainingScopeModifier { + type: "containingScope"; + scopeType: ScopeType; + ancestorIndex?: number; +} + +interface EveryScopeModifier { + type: "everyScope"; + scopeType: ScopeType; +} + +/** + * Refer to scopes by absolute index relative to iteration scope, eg "first + * funk" to refer to the first function in a class. + */ +interface OrdinalScopeModifier { + type: "ordinalScope"; + + scopeType: ScopeType; + + /** The start of the range. Start from end of iteration scope if `start` is negative */ + start: number; + + /** The number of scopes to include. Will always be positive. If greater than 1, will include scopes after {@link start} */ + length: number; +} + +type Direction = "forward" | "backward"; + +/** + * Refer to scopes by offset relative to input target, eg "next + * funk" to refer to the first function after the function containing the target input. + */ +interface RelativeScopeModifier { + type: "relativeScope"; + + scopeType: ScopeType; + + /** Indicates how many scopes away to start relative to the input target. + * Note that if {@link direction} is `"backward"`, then this scope will be the + * end of the output range. */ + offset: number; + + /** The number of scopes to include. Will always be positive. If greater + * than 1, will include scopes in the direction of {@link direction} */ + length: number; + + /** Indicates which direction both {@link offset} and {@link length} go + * relative to input target */ + direction: Direction; +} + +/** + * Converts its input to a raw selection with no type information so for + * example if it is the destination of a bring or move it should inherit the + * type information such as delimiters from its source. + */ +interface RawSelectionModifier { + type: "toRawSelection"; +} + +interface LeadingModifier { + type: "leading"; +} + +interface TrailingModifier { + type: "trailing"; +} + +interface KeepContentFilterModifier { + type: "keepContentFilter"; +} + +interface KeepEmptyFilterModifier { + type: "keepEmptyFilter"; +} + +interface InferPreviousMarkModifier { + type: "inferPreviousMark"; +} + +type TargetPosition = "before" | "after" | "start" | "end"; + +export interface PositionModifierV5 { + type: "position"; + position: TargetPosition; +} + +export interface PartialPrimitiveTargetDescriptorV5 { + type: "primitive"; + mark?: PartialMark; + modifiers?: ModifierV5[]; +} + +interface HeadTailModifier { + type: "extendThroughStartOf" | "extendThroughEndOf"; + modifiers?: ModifierV5[]; +} + +/** + * Runs {@link modifier} if the target has no explicit scope type, ie if + * {@link Target.hasExplicitScopeType} is `false`. + */ +interface ModifyIfUntypedModifier { + type: "modifyIfUntyped"; + + /** + * The modifier to apply if the target is untyped + */ + modifier: ModifierV5; +} + +/** + * Tries each of the modifiers in {@link modifiers} in turn until one of them + * doesn't throw an error, returning the output from the first modifier not + * throwing an error. + */ +interface CascadingModifier { + type: "cascading"; + + /** + * The modifiers to try in turn + */ + modifiers: ModifierV5[]; +} + +/** + * First applies {@link anchor} to input, then independently applies + * {@link active}, and forms a range between the two resulting targets + */ +interface RangeModifier { + type: "range"; + anchor: ModifierV5; + active: ModifierV5; + excludeAnchor?: boolean; + excludeActive?: boolean; +} + +export type ModifierV5 = + | PositionModifierV5 + | InteriorOnlyModifier + | ExcludeInteriorModifier + | ContainingScopeModifier + | EveryScopeModifier + | OrdinalScopeModifier + | RelativeScopeModifier + | HeadTailModifier + | LeadingModifier + | TrailingModifier + | RawSelectionModifier + | ModifyIfUntypedModifier + | CascadingModifier + | RangeModifier + | KeepContentFilterModifier + | KeepEmptyFilterModifier + | InferPreviousMarkModifier; + +// continuous is one single continuous selection between the two targets +// vertical puts a selection on each line vertically between the two targets +type PartialRangeType = "continuous" | "vertical"; + +export interface PartialRangeTargetDescriptorV5 { + type: "range"; + anchor: PartialPrimitiveTargetDescriptorV5 | ImplicitTargetDescriptorV5; + active: PartialPrimitiveTargetDescriptorV5; + excludeAnchor: boolean; + excludeActive: boolean; + rangeType?: PartialRangeType; +} + +export interface PartialListTargetDescriptorV5 { + type: "list"; + elements: ( + | PartialPrimitiveTargetDescriptorV5 + | PartialRangeTargetDescriptorV5 + )[]; +} + +export interface ImplicitTargetDescriptorV5 { + type: "implicit"; +} + +export type PartialTargetDescriptorV5 = + | PartialPrimitiveTargetDescriptorV5 + | PartialRangeTargetDescriptorV5 + | PartialListTargetDescriptorV5 + | ImplicitTargetDescriptorV5; diff --git a/packages/cursorless-engine/src/actions/Actions.ts b/packages/cursorless-engine/src/actions/Actions.ts index dde3e0007c..e65cc10de4 100644 --- a/packages/cursorless-engine/src/actions/Actions.ts +++ b/packages/cursorless-engine/src/actions/Actions.ts @@ -6,7 +6,8 @@ import Call from "./Call"; import Clear from "./Clear"; import { CutToClipboard } from "./CutToClipboard"; import Deselect from "./Deselect"; -import { EditNew, EditNewAfter, EditNewBefore } from "./EditNew"; +import { EditNew } from "./EditNew"; +import { EditNewAfter, EditNewBefore } from "./EditNewLineAction"; import ExecuteCommand from "./ExecuteCommand"; import { FindInWorkspace } from "./Find"; import FollowLink from "./FollowLink"; @@ -71,14 +72,12 @@ export class Actions implements ActionRecord { copyToClipboard = new CopyToClipboard(this.rangeUpdater); cutToClipboard = new CutToClipboard(this); deselect = new Deselect(); - editNew = new EditNew(this.rangeUpdater, this, this.modifierStageFactory); - editNewLineAfter = new EditNewAfter( - this.rangeUpdater, + editNew = new EditNew(this.rangeUpdater, this); + editNewLineAfter: EditNewAfter = new EditNewAfter( this, this.modifierStageFactory, ); - editNewLineBefore = new EditNewBefore( - this.rangeUpdater, + editNewLineBefore: EditNewBefore = new EditNewBefore( this, this.modifierStageFactory, ); diff --git a/packages/cursorless-engine/src/actions/BringMoveSwap.ts b/packages/cursorless-engine/src/actions/BringMoveSwap.ts index 03bba75f3d..74698b6f1f 100644 --- a/packages/cursorless-engine/src/actions/BringMoveSwap.ts +++ b/packages/cursorless-engine/src/actions/BringMoveSwap.ts @@ -1,5 +1,6 @@ import { FlashStyle, + Range, RangeExpansionBehavior, Selection, TextEditor, @@ -12,7 +13,7 @@ import { } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; import { EditWithRangeUpdater } from "../typings/Types"; -import { Target } from "../typings/target.types"; +import { Destination, Target } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { flashTargets, @@ -20,7 +21,7 @@ import { runForEachEditor, } from "../util/targetUtils"; import { unifyRemovalTargets } from "../util/unifyRanges"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; type ActionType = "bring" | "move" | "swap"; @@ -38,72 +39,45 @@ interface MarkEntry { target: Target; } -class BringMoveSwap implements Action { - constructor(private rangeUpdater: RangeUpdater, private type: ActionType) { - this.run = this.run.bind(this); - } +abstract class BringMoveSwap { + protected abstract decoration: { + sourceStyle: FlashStyle; + destinationStyle: FlashStyle; + getSourceRangeCallback: (target: Target) => Range; + }; - private broadcastSource(sources: Target[], destinations: Target[]) { - if (sources.length === 1 && this.type !== "swap") { - // If there is only one source target, expand it to same length as - // destination target - return Array(destinations.length).fill(sources[0]); - } - return sources; - } + constructor(private rangeUpdater: RangeUpdater, private type: ActionType) {} - private getDecorationContext() { - let sourceStyle: FlashStyle; - let getSourceRangeCallback; - if (this.type === "bring") { - sourceStyle = FlashStyle.referenced; - getSourceRangeCallback = getContentRange; - } else if (this.type === "move") { - sourceStyle = FlashStyle.pendingDelete; - getSourceRangeCallback = getRemovalHighlightRange; - } - // NB this.type === "swap" - else { - sourceStyle = FlashStyle.pendingModification1; - getSourceRangeCallback = getContentRange; - } - return { - sourceStyle, - destinationStyle: FlashStyle.pendingModification0, - getSourceRangeCallback, - }; - } - - private async decorateTargets(sources: Target[], destinations: Target[]) { - const decorationContext = this.getDecorationContext(); + protected async decorateTargets(sources: Target[], destinations: Target[]) { await Promise.all([ flashTargets( ide(), sources, - decorationContext.sourceStyle, - decorationContext.getSourceRangeCallback, + this.decoration.sourceStyle, + this.decoration.getSourceRangeCallback, ), - flashTargets(ide(), destinations, decorationContext.destinationStyle), + flashTargets(ide(), destinations, this.decoration.destinationStyle), ]); } - private getEdits(sources: Target[], destinations: Target[]): ExtendedEdit[] { + protected getEditsBringMove( + sources: Target[], + destinations: Destination[], + ): ExtendedEdit[] { const usedSources: Target[] = []; const results: ExtendedEdit[] = []; - const zipSources = - sources.length !== destinations.length && - destinations.length === 1 && - this.type !== "swap"; + const shouldJoinSources = + sources.length !== destinations.length && destinations.length === 1; sources.forEach((source, i) => { let destination = destinations[i]; - if ((source == null || destination == null) && !zipSources) { + if ((source == null || destination == null) && !shouldJoinSources) { throw new Error("Targets must have same number of args"); } if (destination != null) { let text: string; - if (zipSources) { + if (shouldJoinSources) { text = sources .map((source, i) => { const text = source.contentText; @@ -120,7 +94,7 @@ class BringMoveSwap implements Action { results.push({ edit: destination.constructChangeEdit(text), editor: destination.editor, - originalTarget: destination, + originalTarget: destination.target, isSource: false, }); } else { @@ -131,9 +105,11 @@ class BringMoveSwap implements Action { // Prevent multiple instances of the same expanded source. if (!usedSources.includes(source)) { usedSources.push(source); - if (this.type !== "move") { + if (this.type === "bring") { results.push({ - edit: source.constructChangeEdit(destination.contentText), + edit: source + .toDestination("to") + .constructChangeEdit(destination.target.contentText), editor: source.editor, originalTarget: source, isSource: true, @@ -157,7 +133,7 @@ class BringMoveSwap implements Action { return results; } - private async performEditsAndComputeThatMark( + protected async performEditsAndComputeThatMark( edits: ExtendedEdit[], ): Promise { return flatten( @@ -270,15 +246,14 @@ class BringMoveSwap implements Action { }); } - private async decorateThatMark(thatMark: MarkEntry[]) { - const decorationContext = this.getDecorationContext(); + protected async decorateThatMark(thatMark: MarkEntry[]) { const getRange = (target: Target) => thatMark.find((t) => t.target === target)!.selection; return Promise.all([ flashTargets( ide(), thatMark.filter(({ isSource }) => isSource).map(({ target }) => target), - decorationContext.sourceStyle, + this.decoration.sourceStyle, getRange, ), flashTargets( @@ -286,41 +261,57 @@ class BringMoveSwap implements Action { thatMark .filter(({ isSource }) => !isSource) .map(({ target }) => target), - decorationContext.destinationStyle, + this.decoration.destinationStyle, getRange, ), ]); } - private calculateMarks(markEntries: MarkEntry[]) { - // Only swap has sources as a "that" mark - const thatMark = - this.type === "swap" - ? markEntries - : markEntries.filter(({ isSource }) => !isSource); + protected calculateMarksBringMove(markEntries: MarkEntry[]) { + return { + thatMark: markEntries.filter(({ isSource }) => !isSource), + sourceMark: markEntries.filter(({ isSource }) => isSource), + }; + } +} - // Only swap doesn't have a source mark - const sourceMark = - this.type === "swap" - ? [] - : markEntries.filter(({ isSource }) => isSource); +function broadcastSource(sources: Target[], destinations: Destination[]) { + if (sources.length === 1) { + // If there is only one source target, expand it to same length as + // destination target + return Array(destinations.length).fill(sources[0]); + } + return sources; +} - return { thatMark, sourceMark }; +export class Bring extends BringMoveSwap { + decoration = { + sourceStyle: FlashStyle.referenced, + destinationStyle: FlashStyle.pendingModification0, + getSourceRangeCallback: getContentRange, + }; + + constructor(rangeUpdater: RangeUpdater) { + super(rangeUpdater, "bring"); + this.run = this.run.bind(this); } - async run([sources, destinations]: [ - Target[], - Target[], - ]): Promise { - sources = this.broadcastSource(sources, destinations); + async run( + sources: Target[], + destinations: Destination[], + ): Promise { + sources = broadcastSource(sources, destinations); - await this.decorateTargets(sources, destinations); + await this.decorateTargets( + sources, + destinations.map((d) => d.target), + ); - const edits = this.getEdits(sources, destinations); + const edits = this.getEditsBringMove(sources, destinations); const markEntries = await this.performEditsAndComputeThatMark(edits); - const { thatMark, sourceMark } = this.calculateMarks(markEntries); + const { thatMark, sourceMark } = this.calculateMarksBringMove(markEntries); await this.decorateThatMark(thatMark); @@ -328,21 +319,99 @@ class BringMoveSwap implements Action { } } -export class Bring extends BringMoveSwap { - constructor(rangeUpdater: RangeUpdater) { - super(rangeUpdater, "bring"); - } -} - export class Move extends BringMoveSwap { + decoration = { + sourceStyle: FlashStyle.pendingDelete, + destinationStyle: FlashStyle.pendingModification0, + getSourceRangeCallback: getRemovalHighlightRange, + }; + constructor(rangeUpdater: RangeUpdater) { super(rangeUpdater, "move"); + this.run = this.run.bind(this); + } + + async run( + sources: Target[], + destinations: Destination[], + ): Promise { + sources = broadcastSource(sources, destinations); + + await this.decorateTargets( + sources, + destinations.map((d) => d.target), + ); + + const edits = this.getEditsBringMove(sources, destinations); + + const markEntries = await this.performEditsAndComputeThatMark(edits); + + const { thatMark, sourceMark } = this.calculateMarksBringMove(markEntries); + + await this.decorateThatMark(thatMark); + + return { thatSelections: thatMark, sourceSelections: sourceMark }; } } export class Swap extends BringMoveSwap { + decoration = { + sourceStyle: FlashStyle.pendingModification1, + destinationStyle: FlashStyle.pendingModification0, + getSourceRangeCallback: getContentRange, + }; + constructor(rangeUpdater: RangeUpdater) { super(rangeUpdater, "swap"); + this.run = this.run.bind(this); + } + + async run( + targets1: Target[], + targets2: Target[], + ): Promise { + await this.decorateTargets(targets1, targets2); + + const edits = this.getEditsSwap(targets1, targets2); + + const markEntries = await this.performEditsAndComputeThatMark(edits); + + await this.decorateThatMark(markEntries); + + return { thatSelections: markEntries, sourceSelections: [] }; + } + + private getEditsSwap(targets1: Target[], targets2: Target[]): ExtendedEdit[] { + const results: ExtendedEdit[] = []; + + targets1.forEach((target1, i) => { + const target2 = targets2[i]; + if (target1 == null || target2 == null) { + throw new Error("Targets must have same number of args"); + } + + // Add destination edit + results.push({ + edit: target2 + .toDestination("to") + .constructChangeEdit(target1.contentText), + editor: target2.editor, + originalTarget: target2, + isSource: false, + }); + + // Add source edit + results.push({ + edit: target1 + .toDestination("to") + .constructChangeEdit(target2.contentText), + editor: target1.editor, + originalTarget: target1, + isSource: true, + }); + }); + + return results; } } diff --git a/packages/cursorless-engine/src/actions/Call.ts b/packages/cursorless-engine/src/actions/Call.ts index e75788fe2c..501bd4dbc6 100644 --- a/packages/cursorless-engine/src/actions/Call.ts +++ b/packages/cursorless-engine/src/actions/Call.ts @@ -1,30 +1,23 @@ import { Target } from "../typings/target.types"; import { ensureSingleTarget } from "../util/targetUtils"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -export default class Call implements Action { +export default class Call { constructor(private actions: Actions) { this.run = this.run.bind(this); } - async run([sources, destinations]: [ - Target[], - Target[], - ]): Promise { - ensureSingleTarget(sources); + async run(callees: Target[], args: Target[]): Promise { + ensureSingleTarget(callees); - const { returnValue: texts } = await this.actions.getText.run([sources], { + const { returnValue: texts } = await this.actions.getText.run(callees, { showDecorations: false, }); // NB: We unwrap and then rewrap the return value here so that we don't include the source mark const { thatSelections: thatMark } = - await this.actions.wrapWithPairedDelimiter.run( - [destinations], - texts[0] + "(", - ")", - ); + await this.actions.wrapWithPairedDelimiter.run(args, texts[0] + "(", ")"); return { thatSelections: thatMark }; } diff --git a/packages/cursorless-engine/src/actions/CallbackAction.ts b/packages/cursorless-engine/src/actions/CallbackAction.ts index ad280629d1..e3b0810292 100644 --- a/packages/cursorless-engine/src/actions/CallbackAction.ts +++ b/packages/cursorless-engine/src/actions/CallbackAction.ts @@ -16,7 +16,7 @@ import { runOnTargetsForEachEditor, runOnTargetsForEachEditorSequentially, } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; interface CallbackOptions { callback: (editor: EditableTextEditor, targets: Target[]) => Promise; @@ -32,13 +32,13 @@ interface CallbackOptions { * It takes a {@link CallbackOptions.callback callback} that is called once for * each editor, receiving all the targets that are in the given editor. */ -export class CallbackAction implements Action { +export class CallbackAction { constructor(private rangeUpdater: RangeUpdater) { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], options: CallbackOptions, ): Promise { if (options.showDecorations) { diff --git a/packages/cursorless-engine/src/actions/Clear.ts b/packages/cursorless-engine/src/actions/Clear.ts index 9d6ca5879c..8540d1c533 100644 --- a/packages/cursorless-engine/src/actions/Clear.ts +++ b/packages/cursorless-engine/src/actions/Clear.ts @@ -4,14 +4,14 @@ import { Target } from "../typings/target.types"; import { setSelectionsAndFocusEditor } from "../util/setSelectionsAndFocusEditor"; import { ensureSingleEditor } from "../util/targetUtils"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export default class Clear implements Action { +export default class Clear implements SimpleAction { constructor(private actions: Actions) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const editor = ensureSingleEditor(targets); // Convert to plain targets so that the remove action just removes the // content range instead of the removal range @@ -24,7 +24,7 @@ export default class Clear implements Action { }), ); - const { thatTargets } = await this.actions.remove.run([plainTargets]); + const { thatTargets } = await this.actions.remove.run(plainTargets); if (thatTargets != null) { await setSelectionsAndFocusEditor( diff --git a/packages/cursorless-engine/src/actions/CutToClipboard.ts b/packages/cursorless-engine/src/actions/CutToClipboard.ts index bde872b38e..5e92832dde 100644 --- a/packages/cursorless-engine/src/actions/CutToClipboard.ts +++ b/packages/cursorless-engine/src/actions/CutToClipboard.ts @@ -8,14 +8,14 @@ import { import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export class CutToClipboard implements Action { +export class CutToClipboard implements SimpleAction { constructor(private actions: Actions) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { await ide().flashRanges( targets.flatMap((target) => { const { editor, contentRange } = target; @@ -55,9 +55,9 @@ export class CutToClipboard implements Action { const options = { showDecorations: false }; - await this.actions.copyToClipboard.run([targets], options); + await this.actions.copyToClipboard.run(targets, options); - const { thatTargets } = await this.actions.remove.run([targets], options); + const { thatTargets } = await this.actions.remove.run(targets, options); return { thatTargets }; } diff --git a/packages/cursorless-engine/src/actions/Deselect.ts b/packages/cursorless-engine/src/actions/Deselect.ts index 9595712a24..7534e77de0 100644 --- a/packages/cursorless-engine/src/actions/Deselect.ts +++ b/packages/cursorless-engine/src/actions/Deselect.ts @@ -2,14 +2,14 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export default class Deselect implements Action { +export default class Deselect implements SimpleAction { constructor() { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { await runOnTargetsForEachEditor(targets, async (editor, targets) => { // Remove selections with a non-empty intersection const newSelections = editor.selections.filter( diff --git a/packages/cursorless-engine/src/actions/EditNew/EditNew.ts b/packages/cursorless-engine/src/actions/EditNew/EditNew.ts index 498d8a6416..4c58c90a81 100644 --- a/packages/cursorless-engine/src/actions/EditNew/EditNew.ts +++ b/packages/cursorless-engine/src/actions/EditNew/EditNew.ts @@ -1,42 +1,30 @@ import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; -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"; +import { Destination } from "../../typings/target.types"; import { setSelectionsAndFocusEditor } from "../../util/setSelectionsAndFocusEditor"; import { createThatMark, ensureSingleEditor } from "../../util/targetUtils"; import { Actions } from "../Actions"; -import { Action, ActionReturnValue } from "../actions.types"; +import { ActionReturnValue } from "../actions.types"; import { State } from "./EditNew.types"; import { runEditTargets } from "./runEditTargets"; import { runInsertLineAfterTargets } from "./runInsertLineAfterTargets"; import { runEditNewNotebookCellTargets } from "./runNotebookCellTargets"; -export class EditNew implements Action { - getFinalStages(): ModifierStage[] { - return [this.modifierStageFactory.create(containingLineIfUntypedModifier)]; - } - - constructor( - private rangeUpdater: RangeUpdater, - private actions: Actions, - private modifierStageFactory: ModifierStageFactory, - ) { +export class EditNew { + constructor(private rangeUpdater: RangeUpdater, private actions: Actions) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { - if (targets.some((target) => target.isNotebookCell)) { + async run(destinations: Destination[]): Promise { + if (destinations.some(({ target }) => target.isNotebookCell)) { // It is not possible to "pour" a notebook cell and something else, // because each notebook cell is its own editor, and you can't have // cursors in multiple editors. - return runEditNewNotebookCellTargets(this.actions, targets); + return runEditNewNotebookCellTargets(this.actions, destinations); } const editableEditor = ide().getEditableTextEditor( - ensureSingleEditor(targets), + ensureSingleEditor(destinations), ); /** @@ -44,9 +32,13 @@ export class EditNew implements Action { * perform the necessary commands and edits. */ let state: State = { - targets, - thatRanges: targets.map(({ thatTarget }) => thatTarget.contentRange), - cursorRanges: new Array(targets.length).fill(undefined) as undefined[], + destinations, + thatRanges: destinations.map( + ({ target }) => target.thatTarget.contentRange, + ), + cursorRanges: new Array(destinations.length).fill( + undefined, + ) as undefined[], }; state = await runInsertLineAfterTargets( @@ -56,37 +48,16 @@ export class EditNew implements Action { ); state = await runEditTargets(this.rangeUpdater, editableEditor, state); - const newSelections = state.targets.map((target, index) => - state.cursorRanges[index]!.toSelection(target.isReversed), + const newSelections = state.destinations.map((destination, index) => + state.cursorRanges[index]!.toSelection(destination.target.isReversed), ); await setSelectionsAndFocusEditor(editableEditor, newSelections); return { - thatSelections: createThatMark(state.targets, state.thatRanges), + thatSelections: createThatMark( + state.destinations.map((d) => d.target), + state.thatRanges, + ), }; } } - -export class EditNewBefore extends EditNew { - getFinalStages() { - return [ - ...super.getFinalStages(), - new PositionStage({ - type: "position", - position: "before", - }), - ]; - } -} - -export class EditNewAfter extends EditNew { - getFinalStages() { - return [ - ...super.getFinalStages(), - new PositionStage({ - type: "position", - position: "after", - }), - ]; - } -} diff --git a/packages/cursorless-engine/src/actions/EditNew/EditNew.types.ts b/packages/cursorless-engine/src/actions/EditNew/EditNew.types.ts index 80abf0c6ff..fc29e2a8d4 100644 --- a/packages/cursorless-engine/src/actions/EditNew/EditNew.types.ts +++ b/packages/cursorless-engine/src/actions/EditNew/EditNew.types.ts @@ -1,12 +1,12 @@ import type { Range } from "@cursorless/common"; -import type { Target } from "../../typings/target.types"; +import type { Destination } from "../../typings/target.types"; /** - * Internal type to be used for storing a reference to a target that will use an - * edit action to insert a new target + * Internal type to be used for storing a reference to a destination that will use an + * edit action to insert a new destination */ -export interface EditTarget { - target: Target; +export interface EditDestination { + destination: Destination; /** * The original index of this target in the original list of targets passed @@ -25,9 +25,9 @@ export interface EditTarget { */ export interface State { /** - * This field stores the original targets. + * This field stores the original destinations. */ - targets: Target[]; + destinations: Destination[]; /** * We use this field to track the desired `thatMark` at the end, updating it diff --git a/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts b/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts index eca669d406..6eb7756924 100644 --- a/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts +++ b/packages/cursorless-engine/src/actions/EditNew/runEditTargets.ts @@ -6,7 +6,7 @@ import { import { zip } from "lodash"; import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelectionsWithBehavior } from "../../core/updateSelections/updateSelections"; -import { EditTarget, State } from "./EditNew.types"; +import { EditDestination, State } from "./EditNew.types"; /** * Handle targets that will use an edit action to insert a new target, and @@ -26,23 +26,25 @@ export async function runEditTargets( editor: EditableTextEditor, state: State, ): Promise { - const targets: EditTarget[] = state.targets - .map((target, index) => { - const actionType = target.getEditNewActionType(); + const destinations: EditDestination[] = state.destinations + .map((destination, index) => { + const actionType = destination.getEditNewActionType(); if (actionType === "edit") { return { - target, + destination, index, }; } }) - .filter((target): target is EditTarget => !!target); + .filter((destination): destination is EditDestination => !!destination); - if (targets.length === 0) { + if (destinations.length === 0) { return state; } - const edits = targets.map((target) => target.target.constructChangeEdit("")); + const edits = destinations.map((destination) => + destination.destination.constructChangeEdit(""), + ); const thatSelections = { selections: state.thatRanges.map((r) => r.toSelection(false)), @@ -86,14 +88,14 @@ export async function runEditTargets( }); // Add cursor positions for our edit targets. - targets.forEach((delimiterTarget, index) => { + destinations.forEach((delimiterTarget, index) => { const edit = edits[index]; const range = edit.updateRange(updatedEditSelections[index]); updatedCursorRanges[delimiterTarget.index] = range; }); return { - targets: state.targets, + destinations: state.destinations, thatRanges: updatedThatSelections, cursorRanges: updatedCursorRanges, }; diff --git a/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts b/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts index 0b4c88f202..b816d79df9 100644 --- a/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts +++ b/packages/cursorless-engine/src/actions/EditNew/runInsertLineAfterTargets.ts @@ -1,7 +1,7 @@ import { EditableTextEditor } from "@cursorless/common"; import { RangeUpdater } from "../../core/updateSelections/RangeUpdater"; import { callFunctionAndUpdateRanges } from "../../core/updateSelections/updateSelections"; -import { EditTarget, State } from "./EditNew.types"; +import { EditDestination, State } from "./EditNew.types"; /** * Handle targets that will use a VSCode command to insert a new target, eg @@ -19,43 +19,50 @@ export async function runInsertLineAfterTargets( editor: EditableTextEditor, state: State, ): Promise { - const targets: EditTarget[] = state.targets - .map((target, index) => { - const actionType = target.getEditNewActionType(); + const destinations: EditDestination[] = state.destinations + .map((destination, index) => { + const actionType = destination.getEditNewActionType(); if (actionType === "insertLineAfter") { return { - target, + destination, index, }; } }) - .filter((target): target is EditTarget => !!target); + .filter((destination): destination is EditDestination => !!destination); - if (targets.length === 0) { + if (destinations.length === 0) { return state; } - const contentRanges = targets.map(({ target }) => target.contentRange); + const contentRanges = destinations.map( + ({ destination }) => destination.contentRange, + ); const [updatedTargetRanges, updatedThatRanges] = await callFunctionAndUpdateRanges( rangeUpdater, () => editor.insertLineAfter(contentRanges), editor.document, - [state.targets.map(({ contentRange }) => contentRange), state.thatRanges], + [ + state.destinations.map(({ contentRange }) => contentRange), + state.thatRanges, + ], ); // For each of the given command targets, the cursor will go where it ended // up after running the command. We add it to the state so that any // potential edit targets can update them after we return from this function. const cursorRanges = [...state.cursorRanges]; - targets.forEach((commandTarget, index) => { + destinations.forEach((commandTarget, index) => { cursorRanges[commandTarget.index] = editor.selections[index]; }); return { - targets: state.targets.map((target, index) => - target.withContentRange(updatedTargetRanges[index]), + destinations: state.destinations.map((destination, index) => + destination.withTarget( + destination.target.withContentRange(updatedTargetRanges[index]), + ), ), thatRanges: updatedThatRanges, cursorRanges, diff --git a/packages/cursorless-engine/src/actions/EditNew/runNotebookCellTargets.ts b/packages/cursorless-engine/src/actions/EditNew/runNotebookCellTargets.ts index 8587d65484..2cfeaf548e 100644 --- a/packages/cursorless-engine/src/actions/EditNew/runNotebookCellTargets.ts +++ b/packages/cursorless-engine/src/actions/EditNew/runNotebookCellTargets.ts @@ -1,22 +1,27 @@ import { Selection } from "@cursorless/common"; -import { NotebookCellPositionTarget } from "../../processTargets/targets"; import { ide } from "../../singletons/ide.singleton"; -import { Target } from "../../typings/target.types"; +import { Destination } from "../../typings/target.types"; import { createThatMark, ensureSingleTarget } from "../../util/targetUtils"; import { Actions } from "../Actions"; import { ActionReturnValue } from "../actions.types"; export async function runEditNewNotebookCellTargets( actions: Actions, - targets: Target[], + destinations: Destination[], ): Promise { // Can only run on one target because otherwise we'd end up with cursors in // multiple cells, which is unsupported in VSCode - const target = ensureSingleTarget(targets) as NotebookCellPositionTarget; - const editor = ide().getEditableTextEditor(target.editor); - const isAbove = target.position === "before"; + const destination = ensureSingleTarget(destinations); + const editor = ide().getEditableTextEditor(destination.editor); + const isAbove = destination.insertionMode === "before"; - await actions.setSelection.run([targets]); + if (destination.insertionMode === "to") { + throw Error( + `Unsupported insertion mode '${destination.insertionMode}' for notebookcapell`, + ); + } + + await actions.setSelection.run([destination.target]); let modifyThatMark = (selection: Selection) => selection; if (isAbove) { @@ -25,7 +30,7 @@ export async function runEditNewNotebookCellTargets( await editor.editNewNotebookCellBelow(); } - const thatMark = createThatMark([target.thatTarget]); + const thatMark = createThatMark([destination.target.thatTarget]); // Apply horrible hack to work around the fact that in vscode the promise // resolves before the edits have actually been performed. diff --git a/packages/cursorless-engine/src/actions/EditNewLineAction.ts b/packages/cursorless-engine/src/actions/EditNewLineAction.ts new file mode 100644 index 0000000000..624bcc786e --- /dev/null +++ b/packages/cursorless-engine/src/actions/EditNewLineAction.ts @@ -0,0 +1,34 @@ +import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; +import { ModifierStage } from "../processTargets/PipelineStages.types"; +import { containingLineIfUntypedModifier } from "../processTargets/modifiers/commonContainingScopeIfUntypedModifiers"; +import { Target } from "../typings/target.types"; +import { ActionRecord, ActionReturnValue, SimpleAction } from "./actions.types"; + +abstract class EditNewLineAction implements SimpleAction { + getFinalStages(): ModifierStage[] { + return [this.modifierStageFactory.create(containingLineIfUntypedModifier)]; + } + + protected abstract insertionMode: "before" | "after"; + + constructor( + private actions: ActionRecord, + private modifierStageFactory: ModifierStageFactory, + ) { + this.run = this.run.bind(this); + } + + run(targets: Target[]): Promise { + return this.actions.editNew.run( + targets.map((target) => target.toDestination(this.insertionMode)), + ); + } +} + +export class EditNewBefore extends EditNewLineAction { + protected insertionMode = "before" as const; +} + +export class EditNewAfter extends EditNewLineAction { + protected insertionMode = "after" as const; +} diff --git a/packages/cursorless-engine/src/actions/ExecuteCommand.ts b/packages/cursorless-engine/src/actions/ExecuteCommand.ts index 5ea9743e08..9a64d6a0b9 100644 --- a/packages/cursorless-engine/src/actions/ExecuteCommand.ts +++ b/packages/cursorless-engine/src/actions/ExecuteCommand.ts @@ -1,25 +1,18 @@ +import { ExecuteCommandOptions } from "@cursorless/common"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; -import { Action, ActionReturnValue } from "./actions.types"; import { CallbackAction } from "./CallbackAction"; - -interface Options { - commandArgs?: any[]; - ensureSingleEditor?: boolean; - ensureSingleTarget?: boolean; - restoreSelection?: boolean; - showDecorations?: boolean; -} +import { ActionReturnValue } from "./actions.types"; /** * This action can be used to execute a built-in ide command on one or more * targets by first setting the selection to those targets and then running the * action, restoring the selections if - * {@link Options.restoreSelection restoreSelection} is `true`. Internally, most + * {@link ExecuteCommandOptions.restoreSelection restoreSelection} is `true`. Internally, most * of the heavy lifting is done by {@link CallbackAction}. */ -export default class ExecuteCommand implements Action { +export default class ExecuteCommand { private callbackAction: CallbackAction; constructor(rangeUpdater: RangeUpdater) { this.callbackAction = new CallbackAction(rangeUpdater); @@ -27,7 +20,7 @@ export default class ExecuteCommand implements Action { } async run( - targets: [Target[]], + targets: Target[], commandId: string, { commandArgs, @@ -35,7 +28,7 @@ export default class ExecuteCommand implements Action { ensureSingleTarget, restoreSelection, showDecorations, - }: Options = {}, + }: ExecuteCommandOptions = {}, ): Promise { const args = commandArgs ?? []; diff --git a/packages/cursorless-engine/src/actions/Find.ts b/packages/cursorless-engine/src/actions/Find.ts index c3318450e7..bca407a903 100644 --- a/packages/cursorless-engine/src/actions/Find.ts +++ b/packages/cursorless-engine/src/actions/Find.ts @@ -3,19 +3,19 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { ensureSingleTarget } from "../util/targetUtils"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export class FindInWorkspace implements Action { +export class FindInWorkspace implements SimpleAction { constructor(private actions: Actions) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { ensureSingleTarget(targets); - const { returnValue, thatTargets } = await this.actions.getText.run([ + const { returnValue, thatTargets } = await this.actions.getText.run( targets, - ]); + ); const [text] = returnValue as [string]; let query: string; diff --git a/packages/cursorless-engine/src/actions/FollowLink.ts b/packages/cursorless-engine/src/actions/FollowLink.ts index 54575b4b4d..2a3509d7c6 100644 --- a/packages/cursorless-engine/src/actions/FollowLink.ts +++ b/packages/cursorless-engine/src/actions/FollowLink.ts @@ -7,14 +7,14 @@ import { flashTargets, } from "../util/targetUtils"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export default class FollowLink implements Action { +export default class FollowLink implements SimpleAction { constructor(private actions: Actions) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const target = ensureSingleTarget(targets); await flashTargets(ide(), targets, FlashStyle.referenced); @@ -25,7 +25,7 @@ export default class FollowLink implements Action { if (!openedLink) { await this.actions.executeCommand.run( - [targets], + targets, "editor.action.revealDefinition", { restoreSelection: false }, ); diff --git a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts index 899ca5e725..b9e78aa141 100644 --- a/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts +++ b/packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts @@ -4,7 +4,7 @@ import { ide } from "../../singletons/ide.singleton"; import { Target } from "../../typings/target.types"; import { matchAll } from "../../util/regex"; import { ensureSingleTarget, flashTargets } from "../../util/targetUtils"; -import { Action, ActionReturnValue } from "../actions.types"; +import { ActionReturnValue } from "../actions.types"; import { constructSnippetBody } from "./constructSnippetBody"; import { editText } from "./editText"; import { openNewSnippetFile } from "./openNewSnippetFile"; @@ -45,13 +45,13 @@ import Substituter from "./Substituter"; * very similar to snippet placeholders, so we would end up with lots of * confusing escaping. */ -export default class GenerateSnippet implements Action { +export default class GenerateSnippet { constructor() { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], snippetName?: string, ): Promise { const target = ensureSingleTarget(targets); diff --git a/packages/cursorless-engine/src/actions/GetText.ts b/packages/cursorless-engine/src/actions/GetText.ts index 2d5121d2c3..fdfb0efe29 100644 --- a/packages/cursorless-engine/src/actions/GetText.ts +++ b/packages/cursorless-engine/src/actions/GetText.ts @@ -1,20 +1,20 @@ -import { FlashStyle } from "@cursorless/common"; +import { FlashStyle, GetTextActionOptions } from "@cursorless/common"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { ensureSingleTarget, flashTargets } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -export default class GetText implements Action { +export default class GetText { constructor() { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], { showDecorations = true, ensureSingleTarget: doEnsureSingleTarget = false, - } = {}, + }: GetTextActionOptions = {}, ): Promise { if (showDecorations) { await flashTargets(ide(), targets, FlashStyle.referenced); diff --git a/packages/cursorless-engine/src/actions/Highlight.ts b/packages/cursorless-engine/src/actions/Highlight.ts index bc59a7df58..ae418ae5bb 100644 --- a/packages/cursorless-engine/src/actions/Highlight.ts +++ b/packages/cursorless-engine/src/actions/Highlight.ts @@ -5,15 +5,15 @@ import { runOnTargetsForEachEditor, toGeneralizedRange, } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -export default class Highlight implements Action { +export default class Highlight { constructor() { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], highlightId?: HighlightId, ): Promise { if (ide().capabilities.commands["highlight"] == null) { diff --git a/packages/cursorless-engine/src/actions/InsertCopy.ts b/packages/cursorless-engine/src/actions/InsertCopy.ts index 67392d62d9..d9caad1366 100644 --- a/packages/cursorless-engine/src/actions/InsertCopy.ts +++ b/packages/cursorless-engine/src/actions/InsertCopy.ts @@ -14,9 +14,9 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { createThatMark, runOnTargetsForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -class InsertCopy implements Action { +class InsertCopy implements SimpleAction { getFinalStages = () => [ this.modifierStageFactory.create(containingLineIfUntypedModifier), ]; @@ -30,7 +30,7 @@ class InsertCopy implements Action { this.runForEditor = this.runForEditor.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const results = flatten( await runOnTargetsForEachEditor(targets, this.runForEditor), ); @@ -55,7 +55,7 @@ class InsertCopy implements Action { // isBefore is inverted because we want the selections to stay with what is to the user the "copy" const position = this.isBefore ? "after" : "before"; const edits = targets.flatMap((target) => - target.toPositionTarget(position).constructChangeEdit(target.contentText), + target.toDestination(position).constructChangeEdit(target.contentText), ); const cursorSelections = { selections: editor.selections }; diff --git a/packages/cursorless-engine/src/actions/InsertEmptyLines.ts b/packages/cursorless-engine/src/actions/InsertEmptyLines.ts index e324917148..109673a9a4 100644 --- a/packages/cursorless-engine/src/actions/InsertEmptyLines.ts +++ b/packages/cursorless-engine/src/actions/InsertEmptyLines.ts @@ -6,9 +6,9 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -class InsertEmptyLines implements Action { +class InsertEmptyLines implements SimpleAction { constructor( private rangeUpdater: RangeUpdater, private insertAbove: boolean, @@ -41,7 +41,7 @@ class InsertEmptyLines implements Action { })); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const results = flatten( await runOnTargetsForEachEditor(targets, async (editor, targets) => { const ranges = this.getRanges(targets); diff --git a/packages/cursorless-engine/src/actions/InsertSnippet.ts b/packages/cursorless-engine/src/actions/InsertSnippet.ts index 948a0a8506..f0fb49dbc8 100644 --- a/packages/cursorless-engine/src/actions/InsertSnippet.ts +++ b/packages/cursorless-engine/src/actions/InsertSnippet.ts @@ -1,4 +1,5 @@ import { + InsertSnippetArg, RangeExpansionBehavior, ScopeType, Snippet, @@ -13,32 +14,19 @@ import { } from "../core/updateSelections/updateSelections"; import { ModifierStageFactory } from "../processTargets/ModifierStageFactory"; import { ModifyIfUntypedExplicitStage } from "../processTargets/modifiers/ConditionalModifierStages"; +import { UntypedTarget } from "../processTargets/targets"; import { ide } from "../singletons/ide.singleton"; import { findMatchingSnippetDefinitionStrict, transformSnippetVariables, } from "../snippets/snippet"; import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser"; -import { Target } from "../typings/target.types"; +import { Destination, Target } from "../typings/target.types"; import { ensureSingleEditor } from "../util/targetUtils"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; -import { UntypedTarget } from "../processTargets/targets"; - -interface NamedSnippetArg { - type: "named"; - name: string; - substitutions?: Record; -} -interface CustomSnippetArg { - type: "custom"; - body: string; - scopeType?: ScopeType; - substitutions?: Record; -} -type InsertSnippetArg = NamedSnippetArg | CustomSnippetArg; +import { ActionReturnValue } from "./actions.types"; -export default class InsertSnippet implements Action { +export default class InsertSnippet { private snippetParser = new SnippetParser(); constructor( @@ -50,7 +38,7 @@ export default class InsertSnippet implements Action { this.run = this.run.bind(this); } - getPrePositionStages(snippetDescription: InsertSnippetArg) { + getFinalStages(snippetDescription: InsertSnippetArg) { const defaultScopeTypes = this.getScopeTypes(snippetDescription); return defaultScopeTypes.length === 0 @@ -121,12 +109,14 @@ export default class InsertSnippet implements Action { } async run( - [targets]: [Target[]], + destinations: Destination[], snippetDescription: InsertSnippetArg, ): Promise { - const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); + const editor = ide().getEditableTextEditor( + ensureSingleEditor(destinations), + ); - await this.actions.editNew.run([targets]); + await this.actions.editNew.run(destinations); const targetSelectionInfos = editor.selections.map((selection) => getSelectionInfo( diff --git a/packages/cursorless-engine/src/actions/PasteFromClipboard.ts b/packages/cursorless-engine/src/actions/PasteFromClipboard.ts index ec8f7e8380..bd7963832f 100644 --- a/packages/cursorless-engine/src/actions/PasteFromClipboard.ts +++ b/packages/cursorless-engine/src/actions/PasteFromClipboard.ts @@ -9,7 +9,7 @@ import { callFunctionAndUpdateSelectionsWithBehavior, } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; -import { Target } from "../typings/target.types"; +import { Destination } from "../typings/target.types"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { ensureSingleEditor } from "../util/targetUtils"; import { Actions } from "./Actions"; @@ -18,8 +18,10 @@ import { ActionReturnValue } from "./actions.types"; export class PasteFromClipboard { constructor(private rangeUpdater: RangeUpdater, private actions: Actions) {} - async run([targets]: [Target[]]): Promise { - const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); + async run(destinations: Destination[]): Promise { + const editor = ide().getEditableTextEditor( + ensureSingleEditor(destinations), + ); const originalEditor = ide().activeEditableTextEditor; // First call editNew in order to insert delimiters if necessary and leave @@ -28,7 +30,7 @@ export class PasteFromClipboard { const [originalCursorSelections] = await callFunctionAndUpdateSelections( this.rangeUpdater, async () => { - await this.actions.editNew.run([targets]); + await this.actions.editNew.run(destinations); }, editor.document, [editor.selections], diff --git a/packages/cursorless-engine/src/actions/Remove.ts b/packages/cursorless-engine/src/actions/Remove.ts index 6650b74bb5..aeaf93d620 100644 --- a/packages/cursorless-engine/src/actions/Remove.ts +++ b/packages/cursorless-engine/src/actions/Remove.ts @@ -7,15 +7,15 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; import { unifyRemovalTargets } from "../util/unifyRanges"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export default class Delete implements Action { +export default class Delete implements SimpleAction { constructor(private rangeUpdater: RangeUpdater) { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], { showDecorations = true } = {}, ): Promise { // Unify overlapping targets because of overlapping leading and trailing delimiters. diff --git a/packages/cursorless-engine/src/actions/Replace.ts b/packages/cursorless-engine/src/actions/Replace.ts index 3678807f83..a4b7a9b192 100644 --- a/packages/cursorless-engine/src/actions/Replace.ts +++ b/packages/cursorless-engine/src/actions/Replace.ts @@ -1,52 +1,54 @@ -import { FlashStyle } from "@cursorless/common"; +import { FlashStyle, ReplaceWith } from "@cursorless/common"; import { flatten, zip } from "lodash"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { performEditsAndUpdateSelections } from "../core/updateSelections/updateSelections"; import { ide } from "../singletons/ide.singleton"; -import { Target } from "../typings/target.types"; +import { Destination } from "../typings/target.types"; import { flashTargets, runForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -type RangeGenerator = { start: number }; - -export default class Replace implements Action { +export default class Replace { constructor(private rangeUpdater: RangeUpdater) { this.run = this.run.bind(this); } private getTexts( - targets: Target[], - replaceWith: string[] | RangeGenerator, + destinations: Destination[], + replaceWith: ReplaceWith, ): string[] { if (Array.isArray(replaceWith)) { // Broadcast single text to each target if (replaceWith.length === 1) { - return Array(targets.length).fill(replaceWith[0]); + return Array(destinations.length).fill(replaceWith[0]); } return replaceWith; } const numbers = []; - for (let i = 0; i < targets.length; ++i) { + for (let i = 0; i < destinations.length; ++i) { numbers[i] = (replaceWith.start + i).toString(); } return numbers; } async run( - [targets]: [Target[]], - replaceWith: string[] | RangeGenerator, + destinations: Destination[], + replaceWith: ReplaceWith, ): Promise { - await flashTargets(ide(), targets, FlashStyle.pendingModification0); + await flashTargets( + ide(), + destinations.map((d) => d.target), + FlashStyle.pendingModification0, + ); - const texts = this.getTexts(targets, replaceWith); + const texts = this.getTexts(destinations, replaceWith); - if (targets.length !== texts.length) { + if (destinations.length !== texts.length) { throw new Error("Targets and texts must have same length"); } - const edits = zip(targets, texts).map(([target, text]) => ({ - edit: target!.constructChangeEdit(text!), - editor: target!.editor, + const edits = zip(destinations, texts).map(([destination, text]) => ({ + edit: destination!.constructChangeEdit(text!), + editor: destination!.editor, })); const thatMark = flatten( @@ -58,7 +60,7 @@ export default class Replace implements Action { this.rangeUpdater, ide().getEditableTextEditor(editor), edits.map(({ edit }) => edit), - [targets.map((target) => target.contentSelection)], + [destinations.map((destination) => destination.contentSelection)], ); return updatedSelections.map((selection) => ({ diff --git a/packages/cursorless-engine/src/actions/Rewrap.ts b/packages/cursorless-engine/src/actions/Rewrap.ts index 54431e1455..878c7f2671 100644 --- a/packages/cursorless-engine/src/actions/Rewrap.ts +++ b/packages/cursorless-engine/src/actions/Rewrap.ts @@ -10,9 +10,9 @@ import { flashTargets, runOnTargetsForEachEditor, } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -export default class Rewrap implements Action { +export default class Rewrap { getFinalStages = () => [ this.modifierStageFactory.create( containingSurroundingPairIfUntypedModifier, @@ -27,7 +27,7 @@ export default class Rewrap implements Action { } async run( - [targets]: [Target[]], + targets: Target[], left: string, right: string, ): Promise { diff --git a/packages/cursorless-engine/src/actions/Scroll.ts b/packages/cursorless-engine/src/actions/Scroll.ts index 93d20c85e7..4be888e1b5 100644 --- a/packages/cursorless-engine/src/actions/Scroll.ts +++ b/packages/cursorless-engine/src/actions/Scroll.ts @@ -6,14 +6,14 @@ import { } from "@cursorless/common"; import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -class Scroll implements Action { +class Scroll implements SimpleAction { constructor(private at: RevealLineAt) { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const selectionGroups = groupBy(targets, (t: Target) => t.editor); const lines = Array.from(selectionGroups, ([editor, targets]) => { diff --git a/packages/cursorless-engine/src/actions/SetInstanceReference.ts b/packages/cursorless-engine/src/actions/SetInstanceReference.ts index a9afae0f9a..da2617b2ae 100644 --- a/packages/cursorless-engine/src/actions/SetInstanceReference.ts +++ b/packages/cursorless-engine/src/actions/SetInstanceReference.ts @@ -1,12 +1,12 @@ import { Target } from "../typings/target.types"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export class SetInstanceReference implements Action { +export class SetInstanceReference implements SimpleAction { constructor() { this.run = this.run.bind(this); } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { return { thatTargets: targets, instanceReferenceTargets: targets, diff --git a/packages/cursorless-engine/src/actions/SetSelection.ts b/packages/cursorless-engine/src/actions/SetSelection.ts index cd55754e44..77baac9822 100644 --- a/packages/cursorless-engine/src/actions/SetSelection.ts +++ b/packages/cursorless-engine/src/actions/SetSelection.ts @@ -3,9 +3,9 @@ import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { setSelectionsAndFocusEditor } from "../util/setSelectionsAndFocusEditor"; import { ensureSingleEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export class SetSelection implements Action { +export class SetSelection implements SimpleAction { constructor() { this.run = this.run.bind(this); } @@ -14,7 +14,7 @@ export class SetSelection implements Action { return target.contentSelection; } - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { const editor = ensureSingleEditor(targets); const selections = targets.map(this.getSelection); diff --git a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts index 08bb56a6b1..ea9a8a494f 100644 --- a/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts +++ b/packages/cursorless-engine/src/actions/SimpleIdeCommandActions.ts @@ -31,7 +31,7 @@ abstract class SimpleIdeCommandAction { } async run( - targets: [Target[]], + targets: Target[], { showDecorations }: Options = {}, ): Promise { const capabilities = ide().capabilities.commands[this.command]; diff --git a/packages/cursorless-engine/src/actions/Sort.ts b/packages/cursorless-engine/src/actions/Sort.ts index b583d04a15..d587ae4351 100644 --- a/packages/cursorless-engine/src/actions/Sort.ts +++ b/packages/cursorless-engine/src/actions/Sort.ts @@ -1,23 +1,23 @@ import { shuffle } from "lodash"; import { Target } from "../typings/target.types"; import { Actions } from "./Actions"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -abstract class SortBase implements Action { +abstract class SortBase implements SimpleAction { constructor(private actions: Actions) { this.run = this.run.bind(this); } protected abstract sortTexts(texts: string[]): string[]; - async run([targets]: [Target[]]): Promise { + async run(targets: Target[]): Promise { // First sort target by document order const sortedTargets = targets .slice() .sort((a, b) => a.contentRange.start.compareTo(b.contentRange.start)); const { returnValue: unsortedTexts } = await this.actions.getText.run( - [sortedTargets], + sortedTargets, { showDecorations: false, }, @@ -25,7 +25,10 @@ abstract class SortBase implements Action { const sortedTexts = this.sortTexts(unsortedTexts); - return this.actions.replace.run([sortedTargets], sortedTexts); + return this.actions.replace.run( + sortedTargets.map((target) => target.toDestination("to")), + sortedTexts, + ); } } diff --git a/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts b/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts index 3b7d4b88cb..b49988ad32 100644 --- a/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts +++ b/packages/cursorless-engine/src/actions/ToggleBreakpoint.ts @@ -4,9 +4,9 @@ import { containingLineIfUntypedModifier } from "../processTargets/modifiers/com import { ide } from "../singletons/ide.singleton"; import { Target } from "../typings/target.types"; import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { SimpleAction, ActionReturnValue } from "./actions.types"; -export default class ToggleBreakpoint implements Action { +export default class ToggleBreakpoint implements SimpleAction { getFinalStages = () => [ this.modifierStageFactory.create(containingLineIfUntypedModifier), ]; @@ -15,7 +15,7 @@ export default class ToggleBreakpoint implements Action { this.run = this.run.bind(this); } - async run([targets]: [Target[], Target[]]): Promise { + async run(targets: Target[]): Promise { const thatTargets = targets.map(({ thatTarget }) => thatTarget); await flashTargets(ide(), thatTargets, FlashStyle.referenced); diff --git a/packages/cursorless-engine/src/actions/Wrap.ts b/packages/cursorless-engine/src/actions/Wrap.ts index d2b2c0997e..53f320b436 100644 --- a/packages/cursorless-engine/src/actions/Wrap.ts +++ b/packages/cursorless-engine/src/actions/Wrap.ts @@ -15,15 +15,15 @@ import { Target } from "../typings/target.types"; import { FullSelectionInfo } from "../typings/updateSelections"; import { setSelectionsWithoutFocusingEditor } from "../util/setSelectionsAndFocusEditor"; import { runOnTargetsForEachEditor } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -export default class Wrap implements Action { +export default class Wrap { constructor(private rangeUpdater: RangeUpdater) { this.run = this.run.bind(this); } async run( - [targets]: [Target[]], + targets: Target[], left: string, right: string, ): Promise { diff --git a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts index 2bec5d6af2..56ca6942ae 100644 --- a/packages/cursorless-engine/src/actions/WrapWithSnippet.ts +++ b/packages/cursorless-engine/src/actions/WrapWithSnippet.ts @@ -1,4 +1,4 @@ -import { FlashStyle, ScopeType } from "@cursorless/common"; +import { FlashStyle, ScopeType, WrapWithSnippetArg } from "@cursorless/common"; import { Snippets } from "../core/Snippets"; import { RangeUpdater } from "../core/updateSelections/RangeUpdater"; import { callFunctionAndUpdateSelections } from "../core/updateSelections/updateSelections"; @@ -12,22 +12,9 @@ import { import { SnippetParser } from "../snippets/vendor/vscodeSnippet/snippetParser"; import { Target } from "../typings/target.types"; import { ensureSingleEditor, flashTargets } from "../util/targetUtils"; -import { Action, ActionReturnValue } from "./actions.types"; +import { ActionReturnValue } from "./actions.types"; -interface NamedSnippetArg { - type: "named"; - name: string; - variableName: string; -} -interface CustomSnippetArg { - type: "custom"; - body: string; - variableName?: string; - scopeType?: ScopeType; -} -type WrapWithSnippetArg = NamedSnippetArg | CustomSnippetArg; - -export default class WrapWithSnippet implements Action { +export default class WrapWithSnippet { private snippetParser = new SnippetParser(); constructor( @@ -98,7 +85,7 @@ export default class WrapWithSnippet implements Action { } async run( - [targets]: [Target[]], + targets: Target[], snippetDescription: WrapWithSnippetArg, ): Promise { const editor = ide().getEditableTextEditor(ensureSingleEditor(targets)); diff --git a/packages/cursorless-engine/src/actions/actions.types.ts b/packages/cursorless-engine/src/actions/actions.types.ts index 39efc19fd5..560c3bdb35 100644 --- a/packages/cursorless-engine/src/actions/actions.types.ts +++ b/packages/cursorless-engine/src/actions/actions.types.ts @@ -1,10 +1,18 @@ +import type { + ExecuteCommandOptions, + GetTextActionOptions, + HighlightId, + InsertSnippetArg, + ReplaceWith, + SimpleActionName, + WrapWithSnippetArg, +} from "@cursorless/common"; import type { ModifierStage } from "../processTargets/PipelineStages.types"; -import type { Target } from "../typings/target.types"; import type { SelectionWithEditor } from "../typings/Types"; -import type { ActionType } from "@cursorless/common"; +import type { Destination, Target } from "../typings/target.types"; /** - * To be returned by {@link Action.run} + * To be returned by {@link SimpleAction.run} */ export interface ActionReturnValue { /** @@ -46,23 +54,113 @@ export interface ActionReturnValue { instanceReferenceTargets?: Target[]; } -export interface Action { - run(targets: Target[][], ...args: any[]): Promise; - - /** - * Used to define stages that should be run before the final positional stage, if there is one - * @param args Extra args to command - */ - getPrePositionStages?(...args: any[]): ModifierStage[]; +export interface SimpleAction { + run(targets: Target[]): Promise; /** * Used to define final stages that should be run at the end of the pipeline before the action * @param args Extra args to command */ - getFinalStages?(...args: any[]): ModifierStage[]; + getFinalStages?(): ModifierStage[]; } /** * Keeps a map from action names to objects that implement the given action */ -export type ActionRecord = Record; +export interface ActionRecord extends Record { + callAsFunction: { + run(callees: Target[], args: Target[]): Promise; + }; + + replaceWithTarget: { + run( + sources: Target[], + destinations: Destination[], + ): Promise; + }; + + moveToTarget: { + run( + sources: Target[], + destinations: Destination[], + ): Promise; + }; + + swapTargets: { + run(targets1: Target[], targets2: Target[]): Promise; + }; + + wrapWithPairedDelimiter: { + run( + targets: Target[], + left: string, + right: string, + ): Promise; + }; + + rewrapWithPairedDelimiter: { + run( + targets: Target[], + left: string, + right: string, + ): Promise; + getFinalStages(): ModifierStage[]; + }; + + pasteFromClipboard: { + run(destinations: Destination[]): Promise; + }; + + generateSnippet: { + run(targets: Target[], snippetName?: string): Promise; + }; + + insertSnippet: { + run( + destinations: Destination[], + snippetDescription: InsertSnippetArg, + ): Promise; + getFinalStages(snippetDescription: InsertSnippetArg): ModifierStage[]; + }; + + wrapWithSnippet: { + run( + targets: Target[], + snippetDescription: WrapWithSnippetArg, + ): Promise; + getFinalStages(snippetDescription: WrapWithSnippetArg): ModifierStage[]; + }; + + editNew: { + run(destinations: Destination[]): Promise; + }; + + executeCommand: { + run( + targets: Target[], + commandId: string, + options?: ExecuteCommandOptions, + ): Promise; + }; + + replace: { + run( + destinations: Destination[], + replaceWith: ReplaceWith, + ): Promise; + }; + + highlight: { + run( + targets: Target[], + highlightId?: HighlightId, + ): Promise; + }; + + getText: { + run( + target: Target[], + options?: GetTextActionOptions, + ): Promise; + }; +} diff --git a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts index 223c7d3c45..90a7ce9a0b 100644 --- a/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts +++ b/packages/cursorless-engine/src/core/commandRunner/CommandRunnerImpl.ts @@ -1,22 +1,32 @@ -import { CommandComplete } from "@cursorless/common"; +import { + CommandComplete, + DestinationDescriptor, + ActionDescriptor, + PartialTargetDescriptor, +} from "@cursorless/common"; import { CommandRunner } from "../../CommandRunner"; -import { ActionRecord } from "../../actions/actions.types"; +import { ActionRecord, ActionReturnValue } from "../../actions/actions.types"; import { StoredTargetMap } from "../../index"; import { TargetPipelineRunner } from "../../processTargets"; +import { ModifierStage } from "../../processTargets/PipelineStages.types"; import { SelectionWithEditor } from "../../typings/Types"; -import { Target } from "../../typings/target.types"; +import { Destination, Target } from "../../typings/target.types"; import { Debug } from "../Debug"; -import { checkForOldInference } from "../commandVersionUpgrades/canonicalizeAndValidateCommand"; -import inferFullTargets from "../inferFullTargets"; +import { inferFullTargetDescriptor } from "../inferFullTargetDescriptor"; import { selectionToStoredTarget } from "./selectionToStoredTarget"; export class CommandRunnerImpl implements CommandRunner { + private inferenceContext: InferenceContext; + private finalStages: ModifierStage[] = []; + constructor( private debug: Debug, private storedTargets: StoredTargetMap, private pipelineRunner: TargetPipelineRunner, private actions: ActionRecord, - ) {} + ) { + this.inferenceContext = new InferenceContext(this.debug); + } /** * Runs a Cursorless command. We proceed as follows: @@ -24,7 +34,7 @@ export class CommandRunnerImpl implements CommandRunner { * 1. Perform inference on targets to fill in details left out using things * like previous targets. For example we would automatically infer that * `"take funk air and bat"` is equivalent to `"take funk air and funk - * bat"`. See {@link inferFullTargets} for details of how this is done. + * bat"`. See {@link inferFullTargetDescriptors} for details of how this is done. * 2. Call {@link processTargets} to map each abstract {@link Target} object * to a concrete list of {@link Target} objects. * 3. Run the requested action on the given selections. The mapping from @@ -35,28 +45,7 @@ export class CommandRunnerImpl implements CommandRunner { * action, and returns the desired return value indicated by the action, if * it has one. */ - async run({ - action: { name: actionName, args: actionArgs }, - targets: partialTargetDescriptors, - }: CommandComplete): Promise { - checkForOldInference(partialTargetDescriptors); - const targetDescriptors = inferFullTargets(partialTargetDescriptors); - - if (this.debug.active) { - this.debug.log("Full targets:"); - this.debug.log(JSON.stringify(targetDescriptors, null, 3)); - } - - const action = this.actions[actionName]; - - const prePositionStages = - action.getPrePositionStages?.(...actionArgs) ?? []; - const finalStages = action.getFinalStages?.(...actionArgs) ?? []; - - const targets = targetDescriptors.map((targetDescriptor) => - this.pipelineRunner.run(targetDescriptor, prePositionStages, finalStages), - ); - + async run({ action }: CommandComplete): Promise { const { returnValue, thatSelections: newThatSelections, @@ -64,7 +53,7 @@ export class CommandRunnerImpl implements CommandRunner { sourceSelections: newSourceSelections, sourceTargets: newSourceTargets, instanceReferenceTargets: newInstanceReferenceTargets, - } = await action.run(targets, ...actionArgs); + } = await this.runAction(action); this.storedTargets.set( "that", @@ -78,6 +67,180 @@ export class CommandRunnerImpl implements CommandRunner { return returnValue; } + + private runAction( + actionDescriptor: ActionDescriptor, + ): Promise { + // Prepare to run the action by resetting the inference context and + // defaulting the final stages to an empty array + this.inferenceContext.reset(); + this.finalStages = []; + + switch (actionDescriptor.name) { + case "replaceWithTarget": + return this.actions.replaceWithTarget.run( + this.getTargets(actionDescriptor.source), + this.getDestinations(actionDescriptor.destination), + ); + + case "moveToTarget": + return this.actions.moveToTarget.run( + this.getTargets(actionDescriptor.source), + this.getDestinations(actionDescriptor.destination), + ); + + case "swapTargets": + return this.actions.swapTargets.run( + this.getTargets(actionDescriptor.target1), + this.getTargets(actionDescriptor.target2), + ); + + case "callAsFunction": + return this.actions.callAsFunction.run( + this.getTargets(actionDescriptor.callee), + this.getTargets(actionDescriptor.argument), + ); + + case "wrapWithPairedDelimiter": + return this.actions.wrapWithPairedDelimiter.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.left, + actionDescriptor.right, + ); + + case "rewrapWithPairedDelimiter": + this.finalStages = + this.actions.rewrapWithPairedDelimiter.getFinalStages(); + return this.actions.rewrapWithPairedDelimiter.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.left, + actionDescriptor.right, + ); + + case "pasteFromClipboard": + return this.actions.pasteFromClipboard.run( + this.getDestinations(actionDescriptor.destination), + ); + + case "executeCommand": + return this.actions.executeCommand.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.commandId, + actionDescriptor.options, + ); + + case "replace": + return this.actions.replace.run( + this.getDestinations(actionDescriptor.destination), + actionDescriptor.replaceWith, + ); + + case "highlight": + return this.actions.highlight.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.highlightId, + ); + + case "generateSnippet": + return this.actions.generateSnippet.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.snippetName, + ); + + case "insertSnippet": + this.finalStages = this.actions.insertSnippet.getFinalStages( + actionDescriptor.snippetDescription, + ); + return this.actions.insertSnippet.run( + this.getDestinations(actionDescriptor.destination), + actionDescriptor.snippetDescription, + ); + + case "wrapWithSnippet": + this.finalStages = this.actions.wrapWithSnippet.getFinalStages( + actionDescriptor.snippetDescription, + ); + return this.actions.wrapWithSnippet.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.snippetDescription, + ); + + case "editNew": + return this.actions.editNew.run( + this.getDestinations(actionDescriptor.destination), + ); + + case "getText": + return this.actions.getText.run( + this.getTargets(actionDescriptor.target), + actionDescriptor.options, + ); + + default: { + const action = this.actions[actionDescriptor.name]; + this.finalStages = action.getFinalStages?.() ?? []; + return action.run(this.getTargets(actionDescriptor.target)); + } + } + } + + private getTargets( + partialTargetsDescriptor: PartialTargetDescriptor, + ): Target[] { + const targetDescriptor = this.inferenceContext.run( + partialTargetsDescriptor, + ); + + return this.pipelineRunner.run(targetDescriptor, this.finalStages); + } + + private getDestinations( + destinationDescriptor: DestinationDescriptor, + ): Destination[] { + switch (destinationDescriptor.type) { + case "list": + return destinationDescriptor.destinations.flatMap((destination) => + this.getDestinations(destination), + ); + case "primitive": + return this.getTargets(destinationDescriptor.target).map((target) => + target.toDestination(destinationDescriptor.insertionMode), + ); + case "implicit": + return this.getTargets({ type: "implicit" }).map((target) => + target.toDestination("to"), + ); + } + } +} + +/** + * Keeps track of the previous targets that have been passed to + * {@link InferenceContext.run} so that we can infer things like `"bring funk + * air to bat"` -> `"bring funk air to funk bat"`. In this case, there will be + * two calls to {@link InferenceContext.run}, first with the source `"funk air"` + * and then with the destination `"bat"`. + */ +class InferenceContext { + private previousTargets: PartialTargetDescriptor[] = []; + + constructor(private debug: Debug) {} + + run(target: PartialTargetDescriptor) { + const ret = inferFullTargetDescriptor(target, this.previousTargets); + + if (this.debug.active) { + this.debug.log("Full target:"); + this.debug.log(JSON.stringify(ret, null, 2)); + } + + this.previousTargets.push(target); + return ret; + } + + reset() { + this.previousTargets = []; + } } function constructStoredTarget( diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts index e428649eda..fdb59af638 100644 --- a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeAndValidateCommand.ts @@ -8,18 +8,18 @@ import { Modifier, OutdatedExtensionError, PartialTargetDescriptor, - showWarning, SimpleScopeTypeType, } from "@cursorless/common"; -import { ide } from "../../singletons/ide.singleton"; +import { getPartialTargetDescriptors } from "../../util/getPartialTargetDescriptors"; import { getPartialPrimitiveTargets } from "../../util/getPrimitiveTargets"; -import canonicalizeActionName from "./canonicalizeActionName"; -import canonicalizeTargets from "./canonicalizeTargets"; +import canonicalizeTargetsInPlace from "./canonicalizeTargetsInPlace"; import { upgradeV0ToV1 } from "./upgradeV0ToV1"; import { upgradeV1ToV2 } from "./upgradeV1ToV2"; import { upgradeV2ToV3 } from "./upgradeV2ToV3"; import { upgradeV3ToV4 } from "./upgradeV3ToV4"; import { upgradeV4ToV5 } from "./upgradeV4ToV5/upgradeV4ToV5"; +import { upgradeV5ToV6 } from "./upgradeV5ToV6"; +import produce from "immer"; /** * Given a command argument which comes from the client, normalize it so that it @@ -32,26 +32,17 @@ export function canonicalizeAndValidateCommand( command: Command, ): EnforceUndefined { const commandUpgraded = upgradeCommand(command); - const { - action, - targets: inputPartialTargets, - usePrePhraseSnapshot = false, - spokenForm, - } = commandUpgraded; - - const actionName = canonicalizeActionName(action.name); - const partialTargets = canonicalizeTargets(inputPartialTargets); - - validateCommand(actionName, partialTargets); + const { action, usePrePhraseSnapshot = false, spokenForm } = commandUpgraded; return { version: LATEST_VERSION, spokenForm, - action: { - name: actionName, - args: action.args ?? [], - }, - targets: partialTargets, + action: produce(action, (draft) => { + const partialTargets = getPartialTargetDescriptors(draft); + + canonicalizeTargetsInPlace(partialTargets); + validateCommand(action.name, partialTargets); + }), usePrePhraseSnapshot, }; } @@ -78,6 +69,9 @@ function upgradeCommand(command: Command): CommandLatest { case 4: command = upgradeV4ToV5(command); break; + case 5: + command = upgradeV5ToV6(command); + break; default: throw new Error( `Can't upgrade from unknown version ${command.version}`, @@ -95,7 +89,7 @@ function upgradeCommand(command: Command): CommandLatest { function validateCommand( actionName: ActionType, partialTargets: PartialTargetDescriptor[], -) { +): void { if ( usesScopeType("notebookCell", partialTargets) && !["editNewLineBefore", "editNewLineAfter"].includes(actionName) @@ -118,34 +112,3 @@ function usesScopeType( ), ); } - -export function checkForOldInference( - partialTargets: PartialTargetDescriptor[], -) { - const hasOldInference = partialTargets.some((target) => { - return ( - target.type === "range" && - target.active.mark == null && - target.active.modifiers?.some((m) => m.type === "position") && - !target.active.modifiers?.some((m) => m.type === "inferPreviousMark") - ); - }); - - if (hasOldInference) { - const { globalState, messages } = ide(); - const hideInferenceWarning = globalState.get("hideInferenceWarning"); - - if (!hideInferenceWarning) { - showWarning( - messages, - "deprecatedPositionInference", - 'The "past start of" / "past end of" form has changed behavior. For the old behavior, update cursorless-talon (https://www.cursorless.org/docs/user/updating/), and then you can now say "past start of its" / "past end of its". For example, "take air past end of its line". You may also consider using "head" / "tail" instead; see https://www.cursorless.org/docs/#head-and-tail', - "Don't show again", - ).then((pressed) => { - if (pressed) { - globalState.set("hideInferenceWarning", true); - } - }); - } - } -} diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargets.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargetsInPlace.ts similarity index 50% rename from packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargets.ts rename to packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargetsInPlace.ts index ee3b3f3430..0e1446dd7d 100644 --- a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargets.ts +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeTargetsInPlace.ts @@ -1,12 +1,10 @@ -import { HatStyleName } from "@cursorless/common"; -import update from "immutability-helper"; -import { flow } from "lodash"; -import { transformPartialPrimitiveTargets } from "../../util/getPrimitiveTargets"; import { + HatStyleName, PartialPrimitiveTargetDescriptor, PartialTargetDescriptor, SimpleScopeTypeType, } from "@cursorless/common"; +import { getPartialPrimitiveTargets } from "../../util/getPrimitiveTargets"; const SCOPE_TYPE_CANONICALIZATION_MAPPING: Record = { @@ -19,9 +17,9 @@ const COLOR_CANONICALIZATION_MAPPING: Record = { purple: "pink", }; -const canonicalizeScopeTypes = ( +function canonicalizeScopeTypesInPlace( target: PartialPrimitiveTargetDescriptor, -): PartialPrimitiveTargetDescriptor => { +): void { target.modifiers?.forEach((mod) => { if (mod.type === "containingScope" || mod.type === "everyScope") { mod.scopeType.type = @@ -29,26 +27,23 @@ const canonicalizeScopeTypes = ( mod.scopeType.type; } }); - return target; -}; +} -const canonicalizeColors = ( +function canonicalizeColorsInPlace( target: PartialPrimitiveTargetDescriptor, -): PartialPrimitiveTargetDescriptor => - target.mark?.type === "decoratedSymbol" - ? update(target, { - mark: { - symbolColor: (symbolColor: string) => - COLOR_CANONICALIZATION_MAPPING[symbolColor] ?? symbolColor, - }, - }) - : target; +): void { + if (target.mark?.type === "decoratedSymbol") { + target.mark.symbolColor = + COLOR_CANONICALIZATION_MAPPING[target.mark.symbolColor] ?? + target.mark.symbolColor; + } +} -export default function canonicalizeTargets( +export default function canonicalizeTargetsInPlace( partialTargets: PartialTargetDescriptor[], -) { - return transformPartialPrimitiveTargets( - partialTargets, - flow(canonicalizeScopeTypes, canonicalizeColors), - ); +): void { + getPartialPrimitiveTargets(partialTargets).forEach((target) => { + canonicalizeScopeTypesInPlace(target); + canonicalizeColorsInPlace(target); + }); } diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV4ToV5/upgradeV4ToV5.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV4ToV5/upgradeV4ToV5.ts index 0c2c74aa56..233bdd3392 100644 --- a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV4ToV5/upgradeV4ToV5.ts +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV4ToV5/upgradeV4ToV5.ts @@ -1,6 +1,6 @@ import { - ActionCommand, ActionCommandV4, + ActionCommandV5, CommandV4, CommandV5, } from "@cursorless/common"; @@ -13,7 +13,7 @@ export function upgradeV4ToV5(command: CommandV4): CommandV5 { }; } -function upgradeAction(action: ActionCommandV4): ActionCommand { +function upgradeAction(action: ActionCommandV4): ActionCommandV5 { switch (action.name) { case "wrapWithSnippet": { const [name, variableName] = parseSnippetLocation( diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeActionName.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/canonicalizeActionName.ts similarity index 100% rename from packages/cursorless-engine/src/core/commandVersionUpgrades/canonicalizeActionName.ts rename to packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/canonicalizeActionName.ts diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts new file mode 100644 index 0000000000..fb04f2a3a5 --- /dev/null +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/index.ts @@ -0,0 +1 @@ +export * from "./upgradeV5ToV6"; diff --git a/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts new file mode 100644 index 0000000000..12d1dfb188 --- /dev/null +++ b/packages/cursorless-engine/src/core/commandVersionUpgrades/upgradeV5ToV6/upgradeV5ToV6.ts @@ -0,0 +1,366 @@ +import { + ActionCommandV5, + ActionDescriptor, + CommandV5, + CommandV6, + DestinationDescriptor, + EnforceUndefined, + ExecuteCommandOptions, + GetTextActionOptions, + HighlightId, + ImplicitDestinationDescriptor, + ImplicitTargetDescriptor, + InsertSnippetArg, + InsertionMode, + ListDestinationDescriptor, + Modifier, + ModifierV5, + PartialListTargetDescriptor, + PartialListTargetDescriptorV5, + PartialPrimitiveTargetDescriptor, + PartialPrimitiveTargetDescriptorV5, + PartialRangeTargetDescriptor, + PartialRangeTargetDescriptorV5, + PartialTargetDescriptor, + PartialTargetDescriptorV5, + PositionModifierV5, + PrimitiveDestinationDescriptor, + ReplaceWith, + WrapWithSnippetArg, +} from "@cursorless/common"; +import canonicalizeActionName from "./canonicalizeActionName"; + +export function upgradeV5ToV6(command: CommandV5): EnforceUndefined { + return { + version: 6, + spokenForm: command.spokenForm, + usePrePhraseSnapshot: command.usePrePhraseSnapshot, + action: upgradeAction(command.action, command.targets), + }; +} + +function upgradeAction( + action: ActionCommandV5, + targets: PartialTargetDescriptorV5[], +): EnforceUndefined { + // We canonicalize once and for all + const name = canonicalizeActionName(action.name); + + switch (name) { + case "replaceWithTarget": + case "moveToTarget": + return { + name, + source: upgradeTarget(targets[0]), + destination: targetToDestination(targets[1]), + }; + case "swapTargets": + return { + name, + target1: upgradeTarget(targets[0]), + target2: upgradeTarget(targets[1]), + }; + case "callAsFunction": + return { + name, + callee: upgradeTarget(targets[0]), + argument: upgradeTarget(targets[1]), + }; + case "pasteFromClipboard": + return { + name, + destination: targetToDestination(targets[0]), + }; + case "wrapWithPairedDelimiter": + case "rewrapWithPairedDelimiter": + return { + name, + left: action.args![0] as string, + right: action.args![1] as string, + target: upgradeTarget(targets[0]), + }; + case "generateSnippet": + return { + name, + snippetName: action.args?.[0] as string | undefined, + target: upgradeTarget(targets[0]), + }; + case "insertSnippet": + return { + name, + snippetDescription: action.args![0] as InsertSnippetArg, + destination: targetToDestination(targets[0]), + }; + case "wrapWithSnippet": + return { + name, + snippetDescription: action.args![0] as WrapWithSnippetArg, + target: upgradeTarget(targets[0]), + }; + case "executeCommand": + return { + name, + commandId: action.args![0] as string, + options: action.args?.[1] as ExecuteCommandOptions | undefined, + target: upgradeTarget(targets[0]), + }; + case "replace": + return { + name, + replaceWith: action.args![0] as ReplaceWith, + destination: targetToDestination(targets[0]), + }; + case "highlight": + return { + name, + highlightId: action.args?.[0] as HighlightId | undefined, + target: upgradeTarget(targets[0]), + }; + case "editNew": + return { + name, + destination: targetToDestination(targets[0]), + }; + case "getText": + return { + name, + options: action.args?.[0] as GetTextActionOptions | undefined, + target: upgradeTarget(targets[0]), + }; + default: + return { + name, + target: upgradeTarget(targets[0]), + }; + } +} + +function upgradeTarget( + target: PartialTargetDescriptorV5, +): PartialTargetDescriptor { + switch (target.type) { + case "list": + case "range": + case "primitive": + return upgradeNonImplicitTarget(target); + case "implicit": + return target; + } +} + +function upgradeNonImplicitTarget( + target: + | PartialPrimitiveTargetDescriptorV5 + | PartialRangeTargetDescriptorV5 + | PartialListTargetDescriptorV5, +) { + switch (target.type) { + case "list": + return upgradeListTarget(target); + case "range": + case "primitive": + return upgradeRangeOrPrimitiveTarget(target); + } +} + +function upgradeListTarget( + target: PartialListTargetDescriptorV5, +): PartialListTargetDescriptor { + return { + ...target, + elements: target.elements.map(upgradeRangeOrPrimitiveTarget), + }; +} + +function upgradeRangeOrPrimitiveTarget( + target: PartialPrimitiveTargetDescriptorV5 | PartialRangeTargetDescriptorV5, +) { + switch (target.type) { + case "range": + return upgradeRangeTarget(target); + case "primitive": + return upgradePrimitiveTarget(target); + } +} + +function upgradeRangeTarget( + target: PartialRangeTargetDescriptorV5, +): PartialRangeTargetDescriptor { + const { anchor, active } = target; + return { + ...target, + anchor: + anchor.type === "implicit" ? anchor : upgradePrimitiveTarget(anchor), + active: upgradePrimitiveTarget(active), + }; +} + +function upgradePrimitiveTarget( + target: PartialPrimitiveTargetDescriptorV5, +): PartialPrimitiveTargetDescriptor { + return { + ...target, + modifiers: upgradeModifiers(target.modifiers), + }; +} + +function targetToDestination( + target: PartialTargetDescriptorV5, +): DestinationDescriptor { + switch (target.type) { + case "list": + return listTargetToDestination(target); + case "range": + return rangeTargetToDestination(target); + case "primitive": + return primitiveTargetToDestination(target); + case "implicit": + return implicitTargetToDestination(); + } +} + +/** + * Converts a list target to a destination. This is a bit tricky because we need + * to split the list into multiple destinations if there is more than one insertion + * mode. + * @param target The target to convert + * @returns The converted destination + */ +function listTargetToDestination( + target: PartialListTargetDescriptorV5, +): DestinationDescriptor { + const destinations: PrimitiveDestinationDescriptor[] = []; + let currentElements: ( + | PartialPrimitiveTargetDescriptor + | PartialRangeTargetDescriptor + )[] = []; + let currentInsertionMode: InsertionMode | undefined = undefined; + + target.elements.forEach((element) => { + const insertionMode = getInsertionMode(element); + + if (insertionMode != null) { + // If the insertion mode has changed, we need to create a new destination + // with the elements and insertion mode seen so far + if (currentElements.length > 0) { + destinations.push({ + type: "primitive", + insertionMode: currentInsertionMode ?? "to", + target: { + type: "list", + elements: currentElements, + }, + }); + } + + currentElements = [upgradeRangeOrPrimitiveTarget(element)]; + currentInsertionMode = insertionMode; + } else { + currentElements.push(upgradeRangeOrPrimitiveTarget(element)); + } + }); + + if (currentElements.length > 0) { + destinations.push({ + type: "primitive", + insertionMode: currentInsertionMode ?? "to", + target: { + type: "list", + elements: currentElements, + }, + }); + } + + if (destinations.length > 1) { + return { + type: "list", + destinations, + } as ListDestinationDescriptor; + } + + return destinations[0]; +} + +function rangeTargetToDestination( + target: PartialRangeTargetDescriptorV5, +): PrimitiveDestinationDescriptor { + return { + type: "primitive", + insertionMode: getInsertionMode(target.anchor) ?? "to", + target: upgradeRangeTarget(target), + }; +} + +function primitiveTargetToDestination( + target: PartialPrimitiveTargetDescriptorV5, +): PrimitiveDestinationDescriptor { + return { + type: "primitive", + insertionMode: getInsertionMode(target) ?? "to", + target: upgradePrimitiveTarget(target), + }; +} + +function implicitTargetToDestination(): ImplicitDestinationDescriptor { + return { type: "implicit" }; +} + +function getInsertionMode( + target: + | PartialPrimitiveTargetDescriptorV5 + | PartialRangeTargetDescriptorV5 + | ImplicitTargetDescriptor, +): InsertionMode | undefined { + switch (target.type) { + case "implicit": + return "to"; + case "primitive": + return getInsertionModeFromPrimitive(target); + case "range": + return getInsertionMode(target.anchor); + } +} + +function getInsertionModeFromPrimitive( + target: PartialPrimitiveTargetDescriptorV5, +): InsertionMode | undefined { + const positionModifier = target.modifiers?.find( + (m): m is PositionModifierV5 => m.type === "position", + ); + if (positionModifier != null) { + if (target.modifiers!.indexOf(positionModifier) !== 0) { + throw Error("Position modifier has to be at first index"); + } + if ( + positionModifier?.position === "before" || + positionModifier?.position === "after" + ) { + return positionModifier.position; + } + // "start" and "end" modifiers don't affect insertion mode; they remain as + // modifiers + } + return undefined; +} + +function upgradeModifiers(modifiers?: ModifierV5[]): Modifier[] | undefined { + const result: Modifier[] = []; + + if (modifiers != null) { + for (const modifier of modifiers) { + if (modifier.type === "position") { + if (modifier.position === "start") { + result.push({ type: "startOf" }); + } else if (modifier.position === "end") { + result.push({ type: "endOf" }); + } + + // Drop "before" and "after" modifiers + } else { + result.push(modifier as Modifier); + } + } + } + + return result.length > 0 ? result : undefined; +} diff --git a/packages/cursorless-engine/src/core/handleHoistedModifiers.ts b/packages/cursorless-engine/src/core/handleHoistedModifiers.ts index 635d25132c..e3d0f7e1c2 100644 --- a/packages/cursorless-engine/src/core/handleHoistedModifiers.ts +++ b/packages/cursorless-engine/src/core/handleHoistedModifiers.ts @@ -84,7 +84,6 @@ export function handleHoistedModifiers( : { type: "primitive", mark: anchor.mark, - positionModifier: undefined, modifiers: unhoistedModifiers, }, // Remove the hoisted modifier (and everything before it) from the @@ -111,7 +110,6 @@ export function handleHoistedModifiers( type: "target", target: pipelineInputDescriptor, }, - positionModifier: anchor.positionModifier, modifiers: hoistedModifiers, }; } diff --git a/packages/cursorless-engine/src/core/inferFullTargets.ts b/packages/cursorless-engine/src/core/inferFullTargetDescriptor.ts similarity index 65% rename from packages/cursorless-engine/src/core/inferFullTargets.ts rename to packages/cursorless-engine/src/core/inferFullTargetDescriptor.ts index 73174e45b2..81e613bdaa 100644 --- a/packages/cursorless-engine/src/core/inferFullTargets.ts +++ b/packages/cursorless-engine/src/core/inferFullTargetDescriptor.ts @@ -1,13 +1,12 @@ import { - ImplicitTargetDescriptor, Modifier, PartialListTargetDescriptor, PartialPrimitiveTargetDescriptor, PartialRangeTargetDescriptor, PartialTargetDescriptor, - PositionModifier, } from "@cursorless/common"; import { + ListTargetDescriptor, Mark, PrimitiveTargetDescriptor, RangeTargetDescriptor, @@ -16,23 +15,20 @@ import { import { handleHoistedModifiers } from "./handleHoistedModifiers"; /** - * Performs inference on the partial targets provided by the user, using - * previous targets, global defaults, and action-specific defaults to fill out - * any details that may have been omitted in the spoken form. - * For example, we would automatically infer that `"take funk air and bat"` is - * equivalent to `"take funk air and funk bat"`. + * Performs inference on the partial target provided by the user, using previous + * targets, global defaults, and action-specific defaults to fill out any + * details that may have been omitted in the spoken form. For example, we would + * automatically infer that `"take funk air and bat"` is equivalent to `"take + * funk air and funk bat"`. * @param targets The partial targets which need to be completed by inference. - * @returns Target objects fully filled out and ready to be processed by {@link processTargets}. + * @param previousTargets The targets that precede the target we are trying to + * infer. We look in these targets and their descendants when doing inference so + * that eg "bring funk air to bat" will be expanded to "bring funk air to funk + * bat". + * @returns Target objects fully filled out and ready to be processed by the + * target pipeline runner. */ -export default function inferFullTargets( - targets: PartialTargetDescriptor[], -): TargetDescriptor[] { - return targets.map((target, index) => - inferTarget(target, targets.slice(0, index)), - ); -} - -function inferTarget( +export function inferFullTargetDescriptor( target: PartialTargetDescriptor, previousTargets: PartialTargetDescriptor[], ): TargetDescriptor { @@ -40,8 +36,9 @@ function inferTarget( case "list": return inferListTarget(target, previousTargets); case "range": + return inferRangeTargetWithHoist(target, previousTargets); case "primitive": - return inferNonListTarget(target, previousTargets); + return inferPrimitiveTarget(target, previousTargets); case "implicit": return target; } @@ -50,70 +47,59 @@ function inferTarget( function inferListTarget( target: PartialListTargetDescriptor, previousTargets: PartialTargetDescriptor[], -): TargetDescriptor { +): ListTargetDescriptor { return { ...target, - elements: target.elements.map((element, index) => - inferNonListTarget( - element, - previousTargets.concat(target.elements.slice(0, index)), - ), - ), + elements: target.elements.map((element, index) => { + const elementPreviousTargets = previousTargets.concat( + target.elements.slice(0, index), + ); + switch (element.type) { + case "range": + return inferRangeTargetWithHoist(element, elementPreviousTargets); + case "primitive": + return inferPrimitiveTarget(element, elementPreviousTargets); + } + }), }; } -function inferNonListTarget( - target: PartialPrimitiveTargetDescriptor | PartialRangeTargetDescriptor, +function inferRangeTargetWithHoist( + target: PartialRangeTargetDescriptor, previousTargets: PartialTargetDescriptor[], -): PrimitiveTargetDescriptor | RangeTargetDescriptor { - switch (target.type) { - case "primitive": - return inferPrimitiveTarget(target, previousTargets); - case "range": - return inferRangeTarget(target, previousTargets); - } +): RangeTargetDescriptor | PrimitiveTargetDescriptor { + const fullTarget = inferRangeTarget(target, previousTargets); + + const isAnchorMarkImplicit = + target.anchor.type === "implicit" || target.anchor.mark == null; + + return handleHoistedModifiers(fullTarget, isAnchorMarkImplicit); } function inferRangeTarget( target: PartialRangeTargetDescriptor, previousTargets: PartialTargetDescriptor[], -): PrimitiveTargetDescriptor | RangeTargetDescriptor { - const fullTarget: RangeTargetDescriptor = { +): RangeTargetDescriptor { + return { type: "range", rangeType: target.rangeType ?? "continuous", excludeAnchor: target.excludeAnchor ?? false, excludeActive: target.excludeActive ?? false, - anchor: inferPossiblyImplicitTarget(target.anchor, previousTargets), + anchor: + target.anchor.type === "implicit" + ? target.anchor + : inferPrimitiveTarget(target.anchor, previousTargets), active: inferPrimitiveTarget( target.active, previousTargets.concat(target.anchor), ), }; - - const isAnchorMarkImplicit = - target.anchor.type === "implicit" || target.anchor.mark == null; - - return handleHoistedModifiers(fullTarget, isAnchorMarkImplicit); -} - -function inferPossiblyImplicitTarget( - target: PartialPrimitiveTargetDescriptor | ImplicitTargetDescriptor, - previousTargets: PartialTargetDescriptor[], -): PrimitiveTargetDescriptor | ImplicitTargetDescriptor { - if (target.type === "implicit") { - return target; - } - - return inferPrimitiveTarget(target, previousTargets); } function inferPrimitiveTarget( target: PartialPrimitiveTargetDescriptor, previousTargets: PartialTargetDescriptor[], ): PrimitiveTargetDescriptor { - const ownPositionModifier = getPositionModifier(target); - const ownModifiers = getPreservedModifiers(target); - const mark = target.mark ?? (shouldInferPreviousMark(target) ? getPreviousMark(previousTargets) @@ -122,39 +108,17 @@ function inferPrimitiveTarget( }; const modifiers = - ownModifiers ?? getPreviousPreservedModifiers(previousTargets) ?? []; - - const positionModifier = - ownPositionModifier ?? getPreviousPositionModifier(previousTargets); + getPreservedModifiers(target) ?? + getPreviousPreservedModifiers(previousTargets) ?? + []; return { type: target.type, mark, modifiers, - positionModifier, }; } -function getPositionModifier( - target: PartialPrimitiveTargetDescriptor, -): PositionModifier | undefined { - if (target.modifiers == null) { - return undefined; - } - - const positionModifierIndex = target.modifiers.findIndex( - (modifier) => modifier.type === "position", - ); - - if (positionModifierIndex > 0) { - throw Error("Position modifiers must be at the start of a modifier chain"); - } - - return positionModifierIndex === -1 - ? undefined - : (target.modifiers[positionModifierIndex] as PositionModifier); -} - function shouldInferPreviousMark( target: PartialPrimitiveTargetDescriptor, ): boolean { @@ -162,10 +126,8 @@ function shouldInferPreviousMark( } /** - * Return a list of modifiers that should not be removed during inference. - * Today, we remove positional modifiers, because they have their own field on - * the full targets. We also remove modifiers that only impact inference, such - * as `inferPreviousMark`. + * Return a list of modifiers that should not be removed during inference. We + * remove modifiers that only impact inference, such as `inferPreviousMark`. * * We return `undefined` if there are no preserved modifiers. Note that we will * never return an empty list; we will always return `undefined` if there are no @@ -178,7 +140,7 @@ function getPreservedModifiers( ): Modifier[] | undefined { const preservedModifiers = target.modifiers?.filter( - (modifier) => !["position", "inferPreviousMark"].includes(modifier.type), + (modifier) => modifier.type !== "inferPreviousMark", ) ?? []; if (preservedModifiers.length !== 0) { return preservedModifiers; @@ -228,12 +190,6 @@ function getPreviousPreservedModifiers( return getPreviousTargetAttribute(previousTargets, getPreservedModifiers); } -function getPreviousPositionModifier( - previousTargets: PartialTargetDescriptor[], -): PositionModifier | undefined { - return getPreviousTargetAttribute(previousTargets, getPositionModifier); -} - /** * Walks backward through the given targets and their descendants trying to find * the first target for which the given attribute extractor returns a diff --git a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts index fb27ebdd17..a656688473 100644 --- a/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts +++ b/packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts @@ -4,6 +4,7 @@ import { Modifier, SurroundingPairModifier, } from "@cursorless/common"; +import { StoredTargetMap } from ".."; import { LanguageDefinitions } from "../languages/LanguageDefinitions"; import { ModifierStageFactory } from "./ModifierStageFactory"; import { ModifierStage } from "./PipelineStages.types"; @@ -24,7 +25,7 @@ import { import ItemStage from "./modifiers/ItemStage"; import { LeadingStage, TrailingStage } from "./modifiers/LeadingTrailingStages"; import { OrdinalScopeStage } from "./modifiers/OrdinalScopeStage"; -import PositionStage from "./modifiers/PositionStage"; +import { EndOfStage, StartOfStage } from "./modifiers/PositionStage"; import RangeModifierStage from "./modifiers/RangeModifierStage"; import RawSelectionStage from "./modifiers/RawSelectionStage"; import RelativeScopeStage from "./modifiers/RelativeScopeStage"; @@ -36,7 +37,6 @@ import ContainingSyntaxScopeStage, { SimpleEveryScopeModifier, } from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage"; import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage"; -import { StoredTargetMap } from ".."; export class ModifierStageFactoryImpl implements ModifierStageFactory { constructor( @@ -49,8 +49,10 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory { create(modifier: Modifier): ModifierStage { switch (modifier.type) { - case "position": - return new PositionStage(modifier); + case "startOf": + return new StartOfStage(); + case "endOf": + return new EndOfStage(); case "extendThroughStartOf": return new HeadStage(this, modifier); case "extendThroughEndOf": diff --git a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts index 0757071290..c5f147d8b0 100644 --- a/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts +++ b/packages/cursorless-engine/src/processTargets/TargetPipelineRunner.ts @@ -1,4 +1,10 @@ -import { ImplicitTargetDescriptor, Modifier, Range } from "@cursorless/common"; +import { + Direction, + ImplicitTargetDescriptor, + Modifier, + Range, + ScopeType, +} from "@cursorless/common"; import { uniqWith, zip } from "lodash"; import { PrimitiveTargetDescriptor, @@ -11,7 +17,7 @@ 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"; +import { PlainTarget } from "./targets"; export class TargetPipelineRunner { constructor( @@ -24,8 +30,7 @@ export class TargetPipelineRunner { * 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 targets The abstract target representations provided by the user - * @param actionPrePositionStages Modifier stages contributed by the action + * @param target The abstract target representations provided by the user * @param actionFinalStages Modifier stages contributed by the action that * should run at the end of the modifier pipeline * @returns A list of lists of typed selections, one list per input target. @@ -33,16 +38,11 @@ export class TargetPipelineRunner { * document containing it, and potentially rich context information such as * how to remove the target */ - run( - target: TargetDescriptor, - actionPrePositionStages?: ModifierStage[], - actionFinalStages?: ModifierStage[], - ): Target[] { + run(target: TargetDescriptor, actionFinalStages?: ModifierStage[]): Target[] { return new TargetPipeline( this.modifierStageFactory, this.markStageFactory, target, - actionPrePositionStages ?? [], actionFinalStages ?? [], ).run(); } @@ -53,7 +53,6 @@ class TargetPipeline { private modifierStageFactory: ModifierStageFactory, private markStageFactory: MarkStageFactory, private target: TargetDescriptor, - private actionPrePositionStages: ModifierStage[], private actionFinalStages: ModifierStage[], ) {} @@ -152,29 +151,20 @@ class TargetPipeline { return [ targetsToContinuousTarget( excludeAnchor - ? this.modifierStageFactory - .create({ - type: "relativeScope", - scopeType: exclusionScopeType, - direction: isReversed ? "backward" : "forward", - length: 1, - offset: 1, - }) - // NB: The following line assumes that content range is always - // contained by domain, so that "every" will properly reconstruct - // the target from the content range. - .run(anchorTarget)[0] + ? getExcludedScope( + this.modifierStageFactory, + anchorTarget, + exclusionScopeType, + isReversed ? "backward" : "forward", + ) : anchorTarget, excludeActive - ? this.modifierStageFactory - .create({ - type: "relativeScope", - scopeType: exclusionScopeType, - direction: isReversed ? "forward" : "backward", - length: 1, - offset: 1, - }) - .run(activeTarget)[0] + ? getExcludedScope( + this.modifierStageFactory, + activeTarget, + exclusionScopeType, + isReversed ? "forward" : "backward", + ) : activeTarget, false, false, @@ -205,24 +195,14 @@ class TargetPipeline { targetDescriptor: PrimitiveTargetDescriptor | ImplicitTargetDescriptor, ): Target[] { let markStage: MarkStage; - let nonPositionModifierStages: ModifierStage[]; - let positionModifierStages: ModifierStage[]; + let targetModifierStages: ModifierStage[]; if (targetDescriptor.type === "implicit") { markStage = new ImplicitStage(); - nonPositionModifierStages = []; - positionModifierStages = []; + targetModifierStages = []; } else { markStage = this.markStageFactory.create(targetDescriptor.mark); - positionModifierStages = - targetDescriptor.positionModifier == null - ? [] - : [ - this.modifierStageFactory.create( - targetDescriptor.positionModifier, - ), - ]; - nonPositionModifierStages = getModifierStagesFromTargetModifiers( + targetModifierStages = getModifierStagesFromTargetModifiers( this.modifierStageFactory, targetDescriptor.modifiers, ); @@ -235,9 +215,7 @@ class TargetPipeline { * The modifier pipeline that will be applied to construct our final targets */ const modifierStages = [ - ...nonPositionModifierStages, - ...this.actionPrePositionStages, - ...positionModifierStages, + ...targetModifierStages, ...this.actionFinalStages, // This performs auto-expansion to token when you say eg "take this" with an @@ -276,6 +254,28 @@ export function processModifierStages( return targets; } +function getExcludedScope( + modifierStageFactory: ModifierStageFactory, + target: Target, + scopeType: ScopeType, + direction: Direction, +): Target { + return ( + modifierStageFactory + .create({ + type: "relativeScope", + scopeType, + direction, + length: 1, + offset: 1, + }) + // NB: The following line assumes that content range is always + // contained by domain, so that "every" will properly reconstruct + // the target from the content range. + .run(target)[0] + ); +} + function calcIsReversed(anchor: Target, active: Target) { if (anchor.contentRange.start.isAfter(active.contentRange.start)) { return true; @@ -347,17 +347,14 @@ function targetsToVerticalTarget( 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, - }), - ); - } + results.push( + new PlainTarget({ + editor: anchorTarget.editor, + isReversed: anchorTarget.isReversed, + contentRange, + insertionDelimiter: anchorTarget.insertionDelimiter, + }), + ); if (i === activeLine) { return results; diff --git a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts index b8353b9c2a..055eec292d 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/HeadTailStage.ts @@ -1,4 +1,9 @@ -import { HeadTailModifier, Modifier, Range } from "@cursorless/common"; +import { + HeadModifier, + Modifier, + Range, + TailModifier, +} from "@cursorless/common"; import { Target } from "../../typings/target.types"; import { ModifierStageFactory } from "../ModifierStageFactory"; import { ModifierStage } from "../PipelineStages.types"; @@ -52,7 +57,7 @@ abstract class HeadTailStage implements ModifierStage { export class HeadStage extends HeadTailStage { constructor( modifierStageFactory: ModifierStageFactory, - modifier: HeadTailModifier, + modifier: HeadModifier, ) { super(modifierStageFactory, true, modifier.modifiers); } @@ -65,7 +70,7 @@ export class HeadStage extends HeadTailStage { export class TailStage extends HeadTailStage { constructor( modifierStageFactory: ModifierStageFactory, - modifier: HeadTailModifier, + modifier: TailModifier, ) { super(modifierStageFactory, false, modifier.modifiers); } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/PositionStage.ts b/packages/cursorless-engine/src/processTargets/modifiers/PositionStage.ts index 6f3982b0bc..a76dc163f8 100644 --- a/packages/cursorless-engine/src/processTargets/modifiers/PositionStage.ts +++ b/packages/cursorless-engine/src/processTargets/modifiers/PositionStage.ts @@ -1,11 +1,38 @@ +import { Range } from "@cursorless/common"; import { Target } from "../../typings/target.types"; -import { PositionModifier } from "@cursorless/common"; import { ModifierStage } from "../PipelineStages.types"; +import { + CommonTargetParameters, + PlainTarget, + RawSelectionTarget, +} from "../targets"; -export default class PositionStage implements ModifierStage { - constructor(private modifier: PositionModifier) {} - +abstract class PositionStage implements ModifierStage { run(target: Target): Target[] { - return [target.toPositionTarget(this.modifier.position)]; + const parameters: CommonTargetParameters = { + editor: target.editor, + isReversed: target.isReversed, + contentRange: this.getContentRange(target.contentRange), + }; + + return [ + target.isRaw + ? new RawSelectionTarget(parameters) + : new PlainTarget({ ...parameters, isToken: false }), + ]; + } + + protected abstract getContentRange(contentRange: Range): Range; +} + +export class StartOfStage extends PositionStage { + protected getContentRange(contentRange: Range): Range { + return contentRange.start.toEmptyRange(); + } +} + +export class EndOfStage extends PositionStage { + protected getContentRange(contentRange: Range): Range { + return contentRange.end.toEmptyRange(); } } diff --git a/packages/cursorless-engine/src/processTargets/modifiers/toPositionTarget.ts b/packages/cursorless-engine/src/processTargets/modifiers/toPositionTarget.ts deleted file mode 100644 index dd06fedca9..0000000000 --- a/packages/cursorless-engine/src/processTargets/modifiers/toPositionTarget.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Range } from "@cursorless/common"; -import { Target } from "../../typings/target.types"; -import { TargetPosition } from "@cursorless/common"; -import { PositionTarget } from "../targets"; - -export function toPositionTarget( - target: Target, - position: TargetPosition, -): Target { - const { start, end } = target.contentRange; - let contentRange: Range; - let insertionDelimiter: string; - - switch (position) { - case "before": - contentRange = new Range(start, start); - insertionDelimiter = target.insertionDelimiter; - break; - - case "after": - contentRange = new Range(end, end); - insertionDelimiter = target.insertionDelimiter; - break; - - case "start": - contentRange = new Range(start, start); - insertionDelimiter = ""; - break; - - case "end": - contentRange = new Range(end, end); - insertionDelimiter = ""; - break; - } - - return new PositionTarget({ - editor: target.editor, - isReversed: target.isReversed, - contentRange, - thatTarget: target, - position, - insertionDelimiter, - isRaw: target.isRaw, - }); -} diff --git a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts index d41dc29a75..da49eb96b2 100644 --- a/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/BaseTarget.ts @@ -1,4 +1,4 @@ -import type { TargetPlainObject, TargetPosition } from "@cursorless/common"; +import type { InsertionMode, TargetPlainObject } from "@cursorless/common"; import { NoContainingScopeError, Range, @@ -8,13 +8,13 @@ import { } from "@cursorless/common"; import { isEqual } from "lodash"; import type { EditWithRangeUpdater } from "../../typings/Types"; -import type { EditNewActionType, Target } from "../../typings/target.types"; +import type { Destination, Target } from "../../typings/target.types"; import { isSameType } from "../../util/typeUtils"; -import { toPositionTarget } from "../modifiers/toPositionTarget"; import { createContinuousRange, createContinuousRangeUntypedTarget, } from "../targetUtil/createContinuousRange"; +import { DestinationImpl } from "./DestinationImpl"; /** Parameters supported by all target classes */ export interface MinimumTargetParameters { @@ -87,14 +87,6 @@ export default abstract class BaseTarget< return this.state.contentRange; } - constructChangeEdit(text: string): EditWithRangeUpdater { - return { - range: this.contentRange, - text, - updateRange: (range) => range, - }; - } - constructRemovalEdit(): EditWithRangeUpdater { return { range: this.getRemovalRange(), @@ -103,10 +95,6 @@ export default abstract class BaseTarget< }; } - getEditNewActionType(): EditNewActionType { - return "edit"; - } - getRemovalHighlightRange(): Range { return this.getRemovalRange(); } @@ -192,8 +180,8 @@ export default abstract class BaseTarget< }; } - toPositionTarget(position: TargetPosition): Target { - return toPositionTarget(this, position); + toDestination(insertionMode: InsertionMode): Destination { + return new DestinationImpl(this, insertionMode); } /** diff --git a/packages/cursorless-engine/src/processTargets/targets/PositionTarget.ts b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts similarity index 58% rename from packages/cursorless-engine/src/processTargets/targets/PositionTarget.ts rename to packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts index f17987bcdf..1b5d787f03 100644 --- a/packages/cursorless-engine/src/processTargets/targets/PositionTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/DestinationImpl.ts @@ -1,59 +1,77 @@ -import { Range, TextEditor, UnsupportedError } from "@cursorless/common"; -import { BaseTarget, CommonTargetParameters } from "."; -import { TargetPosition } from "@cursorless/common"; -import { EditNewActionType } from "../../typings/target.types"; +import { + InsertionMode, + Range, + Selection, + TextEditor, +} from "@cursorless/common"; import { EditWithRangeUpdater } from "../../typings/Types"; +import { + Destination, + EditNewActionType, + Target, +} from "../../typings/target.types"; + +export class DestinationImpl implements Destination { + public readonly contentRange: Range; + private readonly isLineDelimiter: boolean; + private readonly isBefore: boolean; + private readonly indentationString: string; + + constructor( + public readonly target: Target, + public readonly insertionMode: InsertionMode, + indentationString?: string, + ) { + this.contentRange = getContentRange(target.contentRange, insertionMode); + this.isBefore = insertionMode === "before"; + // It's only considered a line if the delimiter is only new line symbols + this.isLineDelimiter = /^(\n)+$/.test(target.insertionDelimiter); + this.indentationString = + indentationString ?? this.isLineDelimiter + ? getIndentationString(target.editor, target.contentRange) + : ""; + } -interface PositionTargetParameters extends CommonTargetParameters { - readonly position: TargetPosition; - readonly insertionDelimiter: string; - readonly isRaw: boolean; -} + get contentSelection(): Selection { + return this.contentRange.toSelection(this.target.isReversed); + } -export default class PositionTarget extends BaseTarget { - type = "PositionTarget"; - insertionDelimiter: string; - isRaw: boolean; - private position: TargetPosition; - private isLineDelimiter: boolean; - private isBefore: boolean; - private indentationString: string; - - constructor(parameters: PositionTargetParameters) { - super(parameters); - this.position = parameters.position; - this.insertionDelimiter = parameters.insertionDelimiter; - this.isRaw = parameters.isRaw; - this.isBefore = parameters.position === "before"; - // It's only considered a line if the delimiter is only new line symbols - this.isLineDelimiter = /^(\n)+$/.test(parameters.insertionDelimiter); - // This calculation must be done here since that that target is not updated by our range updater - this.indentationString = this.isLineDelimiter - ? getIndentationString( - parameters.editor, - parameters.thatTarget!.contentRange, - ) - : ""; + get editor(): TextEditor { + return this.target.editor; } - getLeadingDelimiterTarget = () => undefined; - getTrailingDelimiterTarget = () => undefined; + get insertionDelimiter(): string { + return this.target.insertionDelimiter; + } + + get isRaw(): boolean { + return this.target.isRaw; + } - getRemovalRange = () => removalUnsupportedForPosition(this.position); + /** + * Creates a new destination with the given target while preserving insertion + * mode and indentation string from this destination. This is important + * because our "edit new" code updates the content range of the target when + * multiple edits are performed in the same document, but we want to insert + * the original indentation. + */ + withTarget(target: Target): Destination { + return new DestinationImpl( + target, + this.insertionMode, + this.indentationString, + ); + } getEditNewActionType(): EditNewActionType { if ( this.insertionDelimiter === "\n" && - this.position === "after" && - this.state.thatTarget!.contentRange.isSingleLine + this.insertionMode === "after" && + this.target.contentRange.isSingleLine ) { // If the target that we're wrapping is not a single line, then we // want to compute indentation based on the entire target. Otherwise, // we allow the editor to determine how to perform indentation. - // Note that we use `this.state.thatTarget` rather than `this.thatTarget` - // because we don't really want the transitive `thatTarget` behaviour, as - // it's not really the "that" target that we're after; it's the target that - // we're wrapping. Should rework this stuff as part of #803. return "insertLineAfter"; } @@ -61,20 +79,11 @@ export default class PositionTarget extends BaseTarget } constructChangeEdit(text: string): EditWithRangeUpdater { - return this.position === "before" || this.position === "after" + return this.insertionMode === "before" || this.insertionMode === "after" ? this.constructEditWithDelimiters(text) : this.constructEditWithoutDelimiters(text); } - protected getCloneParameters(): PositionTargetParameters { - return { - ...this.state, - position: this.position, - insertionDelimiter: this.insertionDelimiter, - isRaw: this.isRaw, - }; - } - private constructEditWithDelimiters(text: string): EditWithRangeUpdater { const range = this.getEditRange(); const editText = this.getEditText(text); @@ -86,7 +95,7 @@ export default class PositionTarget extends BaseTarget return { range, text: editText, - isReplace: this.position === "after", + isReplace: this.insertionMode === "after", updateRange, }; } @@ -151,15 +160,6 @@ export default class PositionTarget extends BaseTarget } } -export function removalUnsupportedForPosition(position: string): Range { - const preferredModifier = - position === "after" || position === "end" ? "trailing" : "leading"; - - throw new UnsupportedError( - `Please use "${preferredModifier}" modifier; removal is not supported for "${position}"`, - ); -} - /** Calculate the minimum indentation/padding for a range */ function getIndentationString(editor: TextEditor, range: Range) { let length = Number.MAX_SAFE_INTEGER; @@ -176,3 +176,17 @@ function getIndentationString(editor: TextEditor, range: Range) { } return indentationString; } + +function getContentRange( + contentRange: Range, + insertionMode: InsertionMode, +): Range { + switch (insertionMode) { + case "before": + return contentRange.start.toEmptyRange(); + case "after": + return contentRange.end.toEmptyRange(); + case "to": + return contentRange; + } +} diff --git a/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts b/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts new file mode 100644 index 0000000000..9f28d148bb --- /dev/null +++ b/packages/cursorless-engine/src/processTargets/targets/NotebookCellDestination.ts @@ -0,0 +1,48 @@ +import { + InsertionMode, + Range, + Selection, + TextEditor, +} from "@cursorless/common"; +import { EditWithRangeUpdater } from "../../typings/Types"; +import { Destination, EditNewActionType } from "../../typings/target.types"; +import NotebookCellTarget from "./NotebookCellTarget"; + +export class NotebookCellDestination implements Destination { + constructor( + public target: NotebookCellTarget, + public insertionMode: InsertionMode, + ) {} + + get editor(): TextEditor { + return this.target.editor; + } + + get contentRange(): Range { + return this.target.contentRange; + } + + get contentSelection(): Selection { + return this.target.contentSelection; + } + + get insertionDelimiter(): string { + return this.target.insertionDelimiter; + } + + get isRaw(): boolean { + return this.target.isRaw; + } + + withTarget(target: NotebookCellTarget): NotebookCellDestination { + return new NotebookCellDestination(target, this.insertionMode); + } + + getEditNewActionType(): EditNewActionType { + throw new Error("Method not implemented."); + } + + constructChangeEdit(_text: string): EditWithRangeUpdater { + throw new Error("Method not implemented."); + } +} diff --git a/packages/cursorless-engine/src/processTargets/targets/NotebookCellTarget.ts b/packages/cursorless-engine/src/processTargets/targets/NotebookCellTarget.ts index 436f3a1c37..cd71744ee8 100644 --- a/packages/cursorless-engine/src/processTargets/targets/NotebookCellTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/NotebookCellTarget.ts @@ -1,10 +1,7 @@ -import { TargetPosition } from "@cursorless/common"; -import { - BaseTarget, - CommonTargetParameters, - removalUnsupportedForPosition, -} from "."; -import { Target } from "../../typings/target.types"; +import { InsertionMode } from "@cursorless/common"; +import { BaseTarget, CommonTargetParameters } from "."; +import { Destination } from "../../typings/target.types"; +import { NotebookCellDestination } from "./NotebookCellDestination"; export default class NotebookCellTarget extends BaseTarget { type = "NotebookCellTarget"; @@ -23,38 +20,7 @@ export default class NotebookCellTarget extends BaseTarget { - type = "NotebookCellPositionTarget"; - insertionDelimiter = "\n"; - isNotebookCell = true; - public position: TargetPosition; - - constructor(parameters: NotebookCellPositionTargetParameters) { - super(parameters); - this.position = parameters.position; - } - - getLeadingDelimiterTarget = () => undefined; - getTrailingDelimiterTarget = () => undefined; - getRemovalRange = () => removalUnsupportedForPosition(this.position); - - protected getCloneParameters(): NotebookCellPositionTargetParameters { - return { - ...this.state, - position: this.position, - }; + toDestination(insertionMode: InsertionMode): Destination { + return new NotebookCellDestination(this, insertionMode); } } diff --git a/packages/cursorless-engine/src/processTargets/targets/PlainTarget.ts b/packages/cursorless-engine/src/processTargets/targets/PlainTarget.ts index 4d826524e0..06d993270b 100644 --- a/packages/cursorless-engine/src/processTargets/targets/PlainTarget.ts +++ b/packages/cursorless-engine/src/processTargets/targets/PlainTarget.ts @@ -2,19 +2,22 @@ import { BaseTarget, CommonTargetParameters } from "."; interface PlainTargetParameters extends CommonTargetParameters { readonly isToken?: boolean; + readonly insertionDelimiter?: string; } /** * A target that has no leading or trailing delimiters so it's removal range - * just consists of the content itself. Its insertion delimiter is empty string. + * just consists of the content itself. Its insertion delimiter is empty string, + * unless specified. */ export default class PlainTarget extends BaseTarget { type = "PlainTarget"; - insertionDelimiter = ""; + insertionDelimiter: string; constructor(parameters: PlainTargetParameters) { super(parameters); this.isToken = parameters.isToken ?? true; + this.insertionDelimiter = parameters.insertionDelimiter ?? ""; } getLeadingDelimiterTarget = () => undefined; @@ -25,6 +28,7 @@ export default class PlainTarget extends BaseTarget { return { ...this.state, isToken: this.isToken, + insertionDelimiter: this.insertionDelimiter, }; } } diff --git a/packages/cursorless-engine/src/processTargets/targets/index.ts b/packages/cursorless-engine/src/processTargets/targets/index.ts index bb85ada5be..0154f82e78 100644 --- a/packages/cursorless-engine/src/processTargets/targets/index.ts +++ b/packages/cursorless-engine/src/processTargets/targets/index.ts @@ -6,12 +6,12 @@ export * from "./LineTarget"; export { default as LineTarget } from "./LineTarget"; export * from "./NotebookCellTarget"; export { default as NotebookCellTarget } from "./NotebookCellTarget"; +export * from "./NotebookCellDestination"; export * from "./ParagraphTarget"; export { default as ParagraphTarget } from "./ParagraphTarget"; export * from "./PlainTarget"; export { default as PlainTarget } from "./PlainTarget"; -export * from "./PositionTarget"; -export { default as PositionTarget } from "./PositionTarget"; +export * from "./DestinationImpl"; export * from "./RawSelectionTarget"; export { default as RawSelectionTarget } from "./RawSelectionTarget"; export * from "./ScopeTypeTarget"; diff --git a/packages/cursorless-engine/src/runCommand.ts b/packages/cursorless-engine/src/runCommand.ts index 7ec7eb98b4..d6d0a02a14 100644 --- a/packages/cursorless-engine/src/runCommand.ts +++ b/packages/cursorless-engine/src/runCommand.ts @@ -37,7 +37,7 @@ export async function runCommand( ): Promise { if (debug.active) { debug.log(`command:`); - debug.log(JSON.stringify(command, null, 3)); + debug.log(JSON.stringify(command, null, 2)); } const commandComplete = canonicalizeAndValidateCommand(command); diff --git a/packages/cursorless-engine/src/scripts/transformRecordedTests/checkMarks.ts b/packages/cursorless-engine/src/scripts/transformRecordedTests/checkMarks.ts index d0bb955dfc..e0e5dc0196 100644 --- a/packages/cursorless-engine/src/scripts/transformRecordedTests/checkMarks.ts +++ b/packages/cursorless-engine/src/scripts/transformRecordedTests/checkMarks.ts @@ -1,10 +1,11 @@ import { FakeIDE, TestCaseFixtureLegacy } from "@cursorless/common"; +import { uniq } from "lodash"; +import { injectIde } from "../../singletons/ide.singleton"; +import tokenGraphemeSplitter from "../../singletons/tokenGraphemeSplitter.singleton"; import { extractTargetKeys } from "../../testUtil/extractTargetKeys"; +import { getPartialTargetDescriptors } from "../../util/getPartialTargetDescriptors"; import { upgrade } from "./transformations/upgrade"; import assert = require("assert"); -import { uniq } from "lodash"; -import tokenGraphemeSplitter from "../../singletons/tokenGraphemeSplitter.singleton"; -import { injectIde } from "../../singletons/ide.singleton"; export function checkMarks(originalFixture: TestCaseFixtureLegacy): undefined { const command = upgrade(originalFixture).command; @@ -12,7 +13,9 @@ export function checkMarks(originalFixture: TestCaseFixtureLegacy): undefined { injectIde(new FakeIDE()); const graphemeSplitter = tokenGraphemeSplitter(); - const targetedMarks = command.targets.map(extractTargetKeys).flat(); + const targetedMarks = getPartialTargetDescriptors(command.action) + .map(extractTargetKeys) + .flat(); const normalizeGraphemes = (key: string): string => graphemeSplitter .getTokenGraphemes(key) diff --git a/packages/cursorless-engine/src/scripts/transformRecordedTests/transformations/upgrade.ts b/packages/cursorless-engine/src/scripts/transformRecordedTests/transformations/upgrade.ts index 74fb84b0fd..1926bedc9c 100644 --- a/packages/cursorless-engine/src/scripts/transformRecordedTests/transformations/upgrade.ts +++ b/packages/cursorless-engine/src/scripts/transformRecordedTests/transformations/upgrade.ts @@ -1,7 +1,6 @@ import { TestCaseFixture, TestCaseFixtureLegacy } from "@cursorless/common"; import { flow } from "lodash"; import { canonicalizeAndValidateCommand } from "../../../core/commandVersionUpgrades/canonicalizeAndValidateCommand"; -import { cleanUpTestCaseCommand } from "../../../testUtil/cleanUpTestCaseCommand"; import { reorderFields } from "./reorderFields"; export const upgrade = flow(upgradeCommand, reorderFields); @@ -9,9 +8,6 @@ export const upgrade = flow(upgradeCommand, reorderFields); function upgradeCommand(fixture: TestCaseFixtureLegacy): TestCaseFixture { return { ...fixture, - command: flow( - canonicalizeAndValidateCommand, - cleanUpTestCaseCommand, - )(fixture.command), + command: canonicalizeAndValidateCommand(fixture.command), }; } diff --git a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts index 95eb9d2446..00ab9e6cb7 100644 --- a/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts +++ b/packages/cursorless-engine/src/testCaseRecorder/TestCase.ts @@ -17,11 +17,11 @@ import { Token, } from "@cursorless/common"; import { pick } from "lodash"; +import { StoredTargetMap } from ".."; import { ide } from "../singletons/ide.singleton"; -import { cleanUpTestCaseCommand } from "../testUtil/cleanUpTestCaseCommand"; import { extractTargetKeys } from "../testUtil/extractTargetKeys"; import { takeSnapshot } from "../testUtil/takeSnapshot"; -import { StoredTargetMap } from ".."; +import { getPartialTargetDescriptors } from "../util/getPartialTargetDescriptors"; export class TestCase { private languageId: string; @@ -48,12 +48,12 @@ export class TestCase { private extraSnapshotFields?: ExtraSnapshotField[], ) { const activeEditor = ide().activeTextEditor!; - this.command = cleanUpTestCaseCommand(command); - - this.targetKeys = command.targets.map(extractTargetKeys).flat(); - + this.command = command; + this.partialTargetDescriptors = getPartialTargetDescriptors(command.action); + this.targetKeys = this.partialTargetDescriptors + .map(extractTargetKeys) + .flat(); this.languageId = activeEditor.document.languageId; - this.partialTargetDescriptors = command.targets; this._awaitingFinalMarkInfo = isHatTokenMapTest; } diff --git a/packages/cursorless-engine/src/testUtil/cleanUpTestCaseCommand.ts b/packages/cursorless-engine/src/testUtil/cleanUpTestCaseCommand.ts deleted file mode 100644 index 4eddf5dbf6..0000000000 --- a/packages/cursorless-engine/src/testUtil/cleanUpTestCaseCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CommandLatest } from "@cursorless/common"; -import { merge } from "lodash"; - -export function cleanUpTestCaseCommand(command: CommandLatest): CommandLatest { - const { args } = command.action; - const result = merge({}, command); - if (args == null || args.length === 0) { - result.action.args = undefined; - } - return result; -} diff --git a/packages/cursorless-engine/src/typings/TargetDescriptor.ts b/packages/cursorless-engine/src/typings/TargetDescriptor.ts index 0d4d76c92b..bcf96035fa 100644 --- a/packages/cursorless-engine/src/typings/TargetDescriptor.ts +++ b/packages/cursorless-engine/src/typings/TargetDescriptor.ts @@ -3,7 +3,6 @@ import { Modifier, PartialMark, PartialRangeType, - PositionModifier, ScopeType, } from "@cursorless/common"; @@ -26,13 +25,6 @@ export interface PrimitiveTargetDescriptor { * character of the name. */ modifiers: Modifier[]; - - /** - * We separate the positional modifier from the other modifiers because it - * behaves differently and and makes the target behave like a destination for - * example for bring. This change is the first step toward #803 - */ - positionModifier?: PositionModifier; } /** diff --git a/packages/cursorless-engine/src/typings/target.types.ts b/packages/cursorless-engine/src/typings/target.types.ts index 155006166c..2310901037 100644 --- a/packages/cursorless-engine/src/typings/target.types.ts +++ b/packages/cursorless-engine/src/typings/target.types.ts @@ -7,6 +7,7 @@ import type { ModifyIfUntypedStage } from "../processTargets/modifiers/ConditionalModifierStages"; // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports import type { + InsertionMode, Range, Selection, // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports @@ -14,7 +15,6 @@ import type { // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports SnippetVariable, TargetPlainObject, - TargetPosition, TextEditor, } from "@cursorless/common"; import type { @@ -137,7 +137,6 @@ export interface Target { getTrailingDelimiterTarget(): Target | undefined; getRemovalRange(): Range; getRemovalHighlightRange(): Range; - getEditNewActionType(): EditNewActionType; withThatTarget(thatTarget: Target): Target; withContentRange(contentRange: Range): Target; createContinuousRangeTarget( @@ -146,16 +145,14 @@ export interface Target { includeStart: boolean, includeEnd: boolean, ): Target; - /** Constructs change/insertion edit. Adds delimiter before/after if needed */ - constructChangeEdit(text: string): EditWithRangeUpdater; /** Constructs removal edit */ constructRemovalEdit(): EditWithRangeUpdater; isEqual(target: Target): boolean; /** - * Construct a position target with the given position. - * @param position The position to use, eg `start`, `end`, `before`, `after` + * Construct a destination with the given insertion mode. + * @param position The insertion modes to use, eg `before`, `after`, `to` */ - toPositionTarget(position: TargetPosition): Target; + toDestination(insertionMode: InsertionMode): Destination; /** * Constructs an object suitable for serialization by json. This is used to * capture targets for testing and recording test cases. @@ -164,3 +161,23 @@ export interface Target { */ toPlainObject(): TargetPlainObject; } + +/** + * A destination is a wrapper around a target that can be used for inserting new + * text. It represents things like "after funk", "before air", "to bat", etc. in + * commands like "bring funk air to bat", "paste after line", etc. Destinations + * are also created implicitly for actions like "drink" and "pour". + */ +export interface Destination { + readonly insertionMode: InsertionMode; + readonly editor: TextEditor; + readonly target: Target; + readonly contentRange: Range; + readonly contentSelection: Selection; + readonly isRaw: boolean; + readonly insertionDelimiter: string; + withTarget(target: Target): Destination; + getEditNewActionType(): EditNewActionType; + /** Constructs change/insertion edit. Adds delimiter before/after if needed */ + constructChangeEdit(text: string): EditWithRangeUpdater; +} diff --git a/packages/cursorless-engine/src/util/getPartialTargetDescriptors.ts b/packages/cursorless-engine/src/util/getPartialTargetDescriptors.ts new file mode 100644 index 0000000000..0b088c3aa5 --- /dev/null +++ b/packages/cursorless-engine/src/util/getPartialTargetDescriptors.ts @@ -0,0 +1,42 @@ +import { + ActionDescriptor, + DestinationDescriptor, + PartialTargetDescriptor, +} from "@cursorless/common"; + +export function getPartialTargetDescriptors( + action: ActionDescriptor, +): PartialTargetDescriptor[] { + switch (action.name) { + case "callAsFunction": + return [action.callee, action.argument]; + case "replaceWithTarget": + case "moveToTarget": + return [ + action.source, + ...getPartialTargetDescriptorsFromDestination(action.destination), + ]; + case "swapTargets": + return [action.target1, action.target2]; + case "pasteFromClipboard": + case "insertSnippet": + case "replace": + case "editNew": + return getPartialTargetDescriptorsFromDestination(action.destination); + default: + return [action.target]; + } +} + +function getPartialTargetDescriptorsFromDestination( + destination: DestinationDescriptor, +): PartialTargetDescriptor[] { + switch (destination.type) { + case "list": + return destination.destinations.map(({ target }) => target); + case "primitive": + return [destination.target]; + case "implicit": + return []; + } +} diff --git a/packages/cursorless-engine/src/util/targetUtils.ts b/packages/cursorless-engine/src/util/targetUtils.ts index 4e0e33a701..fc08a1bd4f 100644 --- a/packages/cursorless-engine/src/util/targetUtils.ts +++ b/packages/cursorless-engine/src/util/targetUtils.ts @@ -11,10 +11,12 @@ import { toLineRange, } from "@cursorless/common"; import { zip } from "lodash"; -import { Target } from "../typings/target.types"; +import { Destination, Target } from "../typings/target.types"; import { SelectionWithEditor } from "../typings/Types"; -export function ensureSingleEditor(targets: Target[]): TextEditor { +export function ensureSingleEditor( + targets: Target[] | Destination[], +): TextEditor { if (targets.length === 0) { throw new Error("Require at least one target with this action"); } @@ -28,7 +30,9 @@ export function ensureSingleEditor(targets: Target[]): TextEditor { return editors[0]; } -export function ensureSingleTarget(targets: Target[]) { +export function ensureSingleTarget( + targets: T[], +): T { if (targets.length !== 1) { throw new Error("Can only have one target with this action"); } diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/getTextAir.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/getTextAir.yml new file mode 100644 index 0000000000..bb13cd6740 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/getTextAir.yml @@ -0,0 +1,36 @@ +languageId: plaintext +command: + version: 6 + action: + name: getText + options: {showDecorations: false, ensureSingleTarget: null} + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: a} + usePrePhraseSnapshot: true +initialState: + documentContents: | + foo bar baz + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + marks: + default.a: + start: {line: 0, character: 4} + end: {line: 0, character: 7} +finalState: + documentContents: | + foo bar baz + selections: + - anchor: {line: 1, character: 0} + active: {line: 1, character: 0} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 4} + end: {line: 0, character: 7} + isReversed: false + hasExplicitRange: false +returnValue: [bar] +ide: + flashes: [] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveAfterDot.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveEndOfDot.yml similarity index 69% rename from packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveAfterDot.yml rename to packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveEndOfDot.yml index 9b3da19638..910716c16e 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveAfterDot.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveEndOfDot.yml @@ -1,12 +1,15 @@ -languageId: typescript +languageId: plaintext command: - version: 1 - spokenForm: give after dot - action: deselect - targets: - - type: primitive - position: after + version: 6 + spokenForm: give end of dot + action: + name: deselect + target: + type: primitive + modifiers: + - {type: endOf} mark: {type: decoratedSymbol, symbolColor: default, character: .} + usePrePhraseSnapshot: true initialState: documentContents: a b.c selections: @@ -28,10 +31,9 @@ finalState: - anchor: {line: 0, character: 3} active: {line: 0, character: 3} thatMark: - - type: PositionTarget + - type: PlainTarget contentRange: start: {line: 0, character: 4} end: {line: 0, character: 4} isReversed: false hasExplicitRange: true -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: .}, selectionType: token, position: after, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveBeforeDot.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveStartOfDot.yml similarity index 69% rename from packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveBeforeDot.yml rename to packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveStartOfDot.yml index 7bf8e70d4d..6f88d94bf3 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveBeforeDot.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/actions/giveStartOfDot.yml @@ -1,12 +1,15 @@ -languageId: typescript +languageId: plaintext command: - version: 1 - spokenForm: give before dot - action: deselect - targets: - - type: primitive - position: before + version: 6 + spokenForm: give start of dot + action: + name: deselect + target: + type: primitive + modifiers: + - {type: startOf} mark: {type: decoratedSymbol, symbolColor: default, character: .} + usePrePhraseSnapshot: true initialState: documentContents: a b.c selections: @@ -28,10 +31,9 @@ finalState: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} thatMark: - - type: PositionTarget + - type: PlainTarget contentRange: start: {line: 0, character: 3} end: {line: 0, character: 3} isReversed: false hasExplicitRange: true -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: .}, selectionType: token, position: before, insideOutsideType: inside, modifier: {type: identity}}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastEndOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastEndOfToken.yml index 2d4657dccc..4614fdd395 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastEndOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastEndOfToken.yml @@ -31,6 +31,3 @@ finalState: - anchor: {line: 0, character: 8} active: {line: 0, character: 8} fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: [{type: toRawSelection}]}, active: {type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}], positionModifier: {type: position, position: end}}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastStartOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastStartOfToken.yml index be7f945bd1..0cbded716a 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastStartOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearJustThisPastStartOfToken.yml @@ -31,6 +31,3 @@ finalState: - anchor: {line: 0, character: 6} active: {line: 0, character: 6} fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: [{type: toRawSelection}]}, active: {type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}], positionModifier: {type: position, position: start}}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastEndOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastEndOfToken.yml index 4d44f50520..fab37a1e39 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastEndOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastEndOfToken.yml @@ -21,6 +21,3 @@ finalState: - anchor: {line: 0, character: 8} active: {line: 0, character: 8} fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: inside}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastStartOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastStartOfToken.yml index 1090f47ea5..38db7ab4b0 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastStartOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearPastStartOfToken.yml @@ -21,6 +21,3 @@ finalState: - anchor: {line: 0, character: 6} active: {line: 0, character: 6} fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: before, modifier: {type: identity}, insideOutsideType: inside}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastEndOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastEndOfToken.yml index cf1c0aef79..c84ec95bc7 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastEndOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastEndOfToken.yml @@ -29,6 +29,3 @@ finalState: - anchor: {line: 0, character: 6} active: {line: 0, character: 6} fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: []}, active: {type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}], positionModifier: {type: position, position: end}}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastStartOfToken.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastStartOfToken.yml index 953682de65..4762249e67 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastStartOfToken.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/compoundTargets/clearThisPastStartOfToken.yml @@ -29,6 +29,3 @@ finalState: - anchor: {line: 0, character: 6} active: {line: 0, character: 6} fullTargets: [{type: range, excludeAnchor: false, excludeActive: false, rangeType: continuous, anchor: {type: primitive, mark: {type: cursor}, modifiers: []}, active: {type: primitive, mark: {type: cursor}, modifiers: [{type: containingScope, scopeType: {type: token}}], positionModifier: {type: position, position: start}}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringFineAfterThis.yml similarity index 96% rename from packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml rename to packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringFineAfterThis.yml index 0916a15c40..886dd623b0 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterThis.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringFineAfterThis.yml @@ -19,7 +19,7 @@ initialState: start: {line: 0, character: 0} end: {line: 0, character: 3} finalState: - documentContents: foo fooworld + documentContents: foo world foo selections: - anchor: {line: 0, character: 4} active: {line: 0, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringWhaleBeforeThis.yml similarity index 96% rename from packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml rename to packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringWhaleBeforeThis.yml index a6fd1913c6..f72d034522 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeThis.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/implicitExpansion/bringWhaleBeforeThis.yml @@ -19,7 +19,7 @@ initialState: start: {line: 0, character: 4} end: {line: 0, character: 9} finalState: - documentContents: fooworld world + documentContents: world foo world selections: - anchor: {line: 0, character: 9} active: {line: 0, character: 9} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastAir.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastAir.yml deleted file mode 100644 index 3c387c26e7..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastAir.yml +++ /dev/null @@ -1,42 +0,0 @@ -languageId: typescript -command: - version: 1 - spokenForm: take after vest past air - action: setSelection - targets: - - type: range - start: - type: primitive - position: after - mark: {type: decoratedSymbol, symbolColor: default, character: v} - end: - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: a} - excludeStart: false - excludeEnd: false -initialState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: - default.v: - start: {line: 1, character: 6} - end: {line: 1, character: 11} - default.a: - start: {line: 3, character: 6} - end: {line: 3, character: 11} -finalState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 1, character: 11} - active: {line: 3, character: 11} -fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastBeforeAir.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastBeforeAir.yml deleted file mode 100644 index 392dae5407..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeAfterVestPastBeforeAir.yml +++ /dev/null @@ -1,43 +0,0 @@ -languageId: typescript -command: - version: 1 - spokenForm: take after vest past before air - action: setSelection - targets: - - type: range - start: - type: primitive - position: after - mark: {type: decoratedSymbol, symbolColor: default, character: v} - end: - type: primitive - position: before - mark: {type: decoratedSymbol, symbolColor: default, character: a} - excludeStart: false - excludeEnd: false -initialState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: - default.v: - start: {line: 1, character: 6} - end: {line: 1, character: 11} - default.a: - start: {line: 3, character: 6} - end: {line: 3, character: 11} -finalState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 1, character: 11} - active: {line: 3, character: 6} -fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, selectionType: token, position: before, modifier: {type: identity}, insideOutsideType: inside}}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeVestPastBeforeAir.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeVestPastBeforeAir.yml deleted file mode 100644 index c4243a7715..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/inference/takeVestPastBeforeAir.yml +++ /dev/null @@ -1,42 +0,0 @@ -languageId: typescript -command: - version: 1 - spokenForm: take vest past before air - action: setSelection - targets: - - type: range - start: - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: v} - end: - type: primitive - position: before - mark: {type: decoratedSymbol, symbolColor: default, character: a} - excludeStart: false - excludeEnd: false -initialState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 0, character: 0} - active: {line: 0, character: 0} - marks: - default.v: - start: {line: 1, character: 6} - end: {line: 1, character: 11} - default.a: - start: {line: 3, character: 6} - end: {line: 3, character: 11} -finalState: - documentContents: | - - const value = "Hello world"; - - const value = "Hello world"; - selections: - - anchor: {line: 1, character: 6} - active: {line: 3, character: 6} -fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: v}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: a}, selectionType: token, position: before, modifier: {type: identity}, insideOutsideType: inside}}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckAfterWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckAfterWhale.yml deleted file mode 100644 index 11f62f65be..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckAfterWhale.yml +++ /dev/null @@ -1,22 +0,0 @@ -languageId: plaintext -command: - spokenForm: chuck after whale - version: 2 - targets: - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: w} - modifiers: - - {type: position, position: after} - usePrePhraseSnapshot: true - action: {name: remove} -initialState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} - marks: - default.w: - start: {line: 0, character: 6} - end: {line: 0, character: 11} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: after}]}] -thrownError: {name: UnsupportedError} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckBeforeWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckBeforeWhale.yml deleted file mode 100644 index f81c285046..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckBeforeWhale.yml +++ /dev/null @@ -1,22 +0,0 @@ -languageId: plaintext -command: - spokenForm: chuck before whale - version: 2 - targets: - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: w} - modifiers: - - {type: position, position: before} - usePrePhraseSnapshot: true - action: {name: remove} -initialState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} - marks: - default.w: - start: {line: 0, character: 6} - end: {line: 0, character: 11} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}]}] -thrownError: {name: UnsupportedError} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckEndOfWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckEndOfWhale.yml index 58b7e67cb8..92d17566dd 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckEndOfWhale.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckEndOfWhale.yml @@ -19,4 +19,8 @@ initialState: start: {line: 0, character: 6} end: {line: 0, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: end}]}] -thrownError: {name: UnsupportedError} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml index da9602b742..e6a368963a 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckPastEndOfLine.yml @@ -21,6 +21,3 @@ finalState: - anchor: {line: 0, character: 2} active: {line: 0, character: 2} fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: outside}, end: {type: primitive, mark: {type: cursor}, selectionType: line, position: after, modifier: {type: identity}, insideOutsideType: inside}}] -ide: - messages: - - {type: warning, id: deprecatedPositionInference} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckStartOfWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckStartOfWhale.yml index 27f9d485ad..e7023fdce2 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckStartOfWhale.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/chuckStartOfWhale.yml @@ -19,4 +19,8 @@ initialState: start: {line: 0, character: 6} end: {line: 0, character: 11} fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: start}]}] -thrownError: {name: UnsupportedError} +finalState: + documentContents: hello world whatever + selections: + - anchor: {line: 0, character: 20} + active: {line: 0, character: 20} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeAfterWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeAfterWhale.yml deleted file mode 100644 index c7e221b5c9..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeAfterWhale.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - spokenForm: take after whale - version: 2 - targets: - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: w} - modifiers: - - {type: position, position: after} - usePrePhraseSnapshot: true - action: {name: setSelection} -initialState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} - marks: - default.w: - start: {line: 0, character: 6} - end: {line: 0, character: 11} -finalState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 11} - active: {line: 0, character: 11} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: after}]}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeBeforeWhale.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeBeforeWhale.yml deleted file mode 100644 index 8566200f47..0000000000 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/positions/takeBeforeWhale.yml +++ /dev/null @@ -1,26 +0,0 @@ -languageId: plaintext -command: - spokenForm: take before whale - version: 2 - targets: - - type: primitive - mark: {type: decoratedSymbol, symbolColor: default, character: w} - modifiers: - - {type: position, position: before} - usePrePhraseSnapshot: true - action: {name: setSelection} -initialState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 20} - active: {line: 0, character: 20} - marks: - default.w: - start: {line: 0, character: 6} - end: {line: 0, character: 11} -finalState: - documentContents: hello world whatever - selections: - - anchor: {line: 0, character: 6} - active: {line: 0, character: 6} -fullTargets: [{type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: w}, modifiers: [{type: position, position: before}]}] diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml index 03f5922077..6d55e46b53 100644 --- a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/testCaseRecorder/takeHarp.yml @@ -1,10 +1,11 @@ languageId: plaintext command: - version: 5 + version: 6 spokenForm: take harp - action: {name: setSelection} - targets: - - type: primitive + action: + name: setSelection + target: + type: primitive mark: {type: decoratedSymbol, symbolColor: default, character: h} usePrePhraseSnapshot: false initialState: diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterJustThis.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterJustThis.yml new file mode 100644 index 0000000000..4c02f80f8d --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringFineAfterJustThis.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + version: 6 + spokenForm: bring fine after just this + action: + name: replaceWithTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: f} + destination: + type: primitive + insertionMode: after + target: + type: primitive + modifiers: + - {type: toRawSelection} + mark: {type: cursor} + usePrePhraseSnapshot: true +initialState: + documentContents: foo world + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} + marks: + default.f: + start: {line: 0, character: 0} + end: {line: 0, character: 3} +finalState: + documentContents: foo fooworld + selections: + - anchor: {line: 0, character: 4} + active: {line: 0, character: 4} diff --git a/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeJustThis.yml b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeJustThis.yml new file mode 100644 index 0000000000..40ca5dc850 --- /dev/null +++ b/packages/cursorless-vscode-e2e/src/suite/fixtures/recorded/updateSelections/bringWhaleBeforeJustThis.yml @@ -0,0 +1,32 @@ +languageId: plaintext +command: + version: 6 + spokenForm: bring whale before just this + action: + name: replaceWithTarget + source: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + destination: + type: primitive + insertionMode: before + target: + type: primitive + modifiers: + - {type: toRawSelection} + mark: {type: cursor} + usePrePhraseSnapshot: true +initialState: + documentContents: foo world + selections: + - anchor: {line: 0, character: 3} + active: {line: 0, character: 3} + marks: + default.w: + start: {line: 0, character: 4} + end: {line: 0, character: 9} +finalState: + documentContents: fooworld world + selections: + - anchor: {line: 0, character: 8} + active: {line: 0, character: 8} diff --git a/packages/cursorless-vscode-e2e/src/suite/testCaseRecorder.vscode.test.ts b/packages/cursorless-vscode-e2e/src/suite/testCaseRecorder.vscode.test.ts index 13bb4bed67..b88d9f5c84 100644 --- a/packages/cursorless-vscode-e2e/src/suite/testCaseRecorder.vscode.test.ts +++ b/packages/cursorless-vscode-e2e/src/suite/testCaseRecorder.vscode.test.ts @@ -1,8 +1,9 @@ import { - ActionType, getFixturePath, getRecordedTestsDirPath, HatTokenMap, + LATEST_VERSION, + SimpleActionName, } from "@cursorless/common"; import { getCursorlessApi, @@ -12,12 +13,18 @@ import { import { assert } from "chai"; import * as crypto from "crypto"; import { mkdir, readdir, readFile, rm } from "fs/promises"; -import * as path from "path"; import * as os from "os"; +import * as path from "path"; import { basename } from "path"; import * as vscode from "vscode"; import { endToEndTestSetup } from "../endToEndTestSetup"; +/* + * All tests in this file are running against the latest version of the command + * and needs to be manually updated on every command migration. + * This includes the file: fixtures/recorded/testCaseRecorder/takeHarp + */ + // Ensure that the test case recorder works suite("testCaseRecorder", async function () { endToEndTestSetup(this); @@ -70,18 +77,18 @@ async function testCaseRecorderGracefulError() { try { await runCursorlessCommand({ - version: 5, - action: { name: "badActionName" as ActionType }, - targets: [ - { + version: LATEST_VERSION, + spokenForm: "bad command", + usePrePhraseSnapshot: false, + action: { + name: "badActionName" as SimpleActionName, + target: { type: "primitive", mark: { type: "cursor", }, }, - ], - usePrePhraseSnapshot: false, - spokenForm: "bad command", + }, }); } catch (err) { // Ignore error @@ -129,10 +136,12 @@ async function stopRecording() { async function takeHarp() { await runCursorlessCommand({ - version: 4, - action: { name: "setSelection" }, - targets: [ - { + version: LATEST_VERSION, + spokenForm: "take harp", + usePrePhraseSnapshot: false, + action: { + name: "setSelection", + target: { type: "primitive", mark: { type: "decoratedSymbol", @@ -140,9 +149,7 @@ async function takeHarp() { character: "h", }, }, - ], - usePrePhraseSnapshot: false, - spokenForm: "take harp", + }, }); } diff --git a/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts b/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts index 94b562c9ba..80d44036c2 100644 --- a/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts +++ b/packages/cursorless-vscode/src/keyboard/KeyboardCommandsTargeted.ts @@ -1,7 +1,6 @@ import { + ActionDescriptor, ActionType, - CommandLatest, - ImplicitTargetDescriptor, LATEST_VERSION, PartialPrimitiveTargetDescriptor, PartialTargetDescriptor, @@ -9,8 +8,8 @@ import { } from "@cursorless/common"; import { runCursorlessCommand } from "@cursorless/vscode-common"; import * as vscode from "vscode"; -import { getStyleName } from "../ide/vscode/hats/getStyleName"; import type { HatColor, HatShape } from "../ide/vscode/hatStyles.types"; +import { getStyleName } from "../ide/vscode/hats/getStyleName"; import KeyboardCommandsModal from "./KeyboardCommandsModal"; import KeyboardHandler from "./KeyboardHandler"; @@ -120,10 +119,8 @@ export default class KeyboardCommandsTargeted { } return await executeCursorlessCommand({ - action: { - name: "highlight", - }, - targets: [target], + name: "highlight", + target, }); }; @@ -137,77 +134,108 @@ export default class KeyboardCommandsTargeted { type = "containingScope", }: TargetScopeTypeArgument) => await executeCursorlessCommand({ - action: { - name: "highlight", - }, - targets: [ - { - type: "primitive", - modifiers: [ - { - type, - scopeType: { - type: scopeType, - }, + name: "highlight", + target: { + type: "primitive", + modifiers: [ + { + type, + scopeType: { + type: scopeType, }, - ], - mark: { - type: "that", }, + ], + mark: { + type: "that", }, - ], + }, }); private highlightTarget = () => executeCursorlessCommand({ - action: { - name: "highlight", - }, - targets: [ - { - type: "primitive", - mark: { - type: "that", - }, + name: "highlight", + target: { + type: "primitive", + mark: { + type: "that", }, - ], + }, }); /** - * Performs action {@link action} on the current target - * @param action The action to run + * Performs action {@link name} on the current target + * @param name The action to run * @returns A promise that resolves to the result of the cursorless command */ - performActionOnTarget = async (action: ActionType) => { - const targets: ( - | PartialPrimitiveTargetDescriptor - | ImplicitTargetDescriptor - )[] = [ - { - type: "primitive", - mark: { - type: "that", - }, + performActionOnTarget = async (name: ActionType) => { + const target: PartialPrimitiveTargetDescriptor = { + type: "primitive", + mark: { + type: "that", }, - ]; + }; - if (MULTIPLE_TARGET_ACTIONS.includes(action)) { - // For multi-target actiosn (eg "bring"), we just use implicit destination - targets.push({ - type: "implicit", - }); - } + let returnValue: unknown; - const returnValue = await executeCursorlessCommand({ - action: { - name: action, - }, - targets, - }); + switch (name) { + case "wrapWithPairedDelimiter": + case "rewrapWithPairedDelimiter": + case "insertSnippet": + case "wrapWithSnippet": + case "executeCommand": + case "replace": + case "editNew": + case "getText": + throw Error(`Unsupported keyboard action: ${name}`); + case "replaceWithTarget": + case "moveToTarget": + returnValue = await executeCursorlessCommand({ + name, + source: target, + destination: { type: "implicit" }, + }); + break; + case "swapTargets": + returnValue = await executeCursorlessCommand({ + name, + target1: target, + target2: { type: "implicit" }, + }); + break; + case "callAsFunction": + returnValue = await executeCursorlessCommand({ + name, + callee: target, + argument: { type: "implicit" }, + }); + break; + case "pasteFromClipboard": + returnValue = await executeCursorlessCommand({ + name, + destination: { + type: "primitive", + insertionMode: "to", + target, + }, + }); + break; + case "generateSnippet": + case "highlight": + returnValue = await executeCursorlessCommand({ + name, + target, + }); + break; + default: + returnValue = await executeCursorlessCommand({ + name, + target, + }); + } await this.highlightTarget(); - if (EXIT_CURSORLESS_MODE_ACTIONS.includes(action)) { + if (EXIT_CURSORLESS_MODE_ACTIONS.includes(name)) { // For some Cursorless actions, it is more convenient if we automatically // exit modal mode await this.modal.modeOff(); @@ -222,18 +250,14 @@ export default class KeyboardCommandsTargeted { */ targetSelection = () => executeCursorlessCommand({ - action: { - name: "highlight", - }, - targets: [ - { - type: "primitive", - mark: { - type: "cursor", - }, - modifiers: [{ type: "toRawSelection" }], + name: "highlight", + target: { + type: "primitive", + mark: { + type: "cursor", }, - ], + modifiers: [{ type: "toRawSelection" }], + }, }); /** @@ -242,36 +266,24 @@ export default class KeyboardCommandsTargeted { */ clearTarget = () => executeCursorlessCommand({ - action: { - name: "highlight", - }, - targets: [ - { - type: "primitive", - mark: { - type: "nothing", - }, + name: "highlight", + target: { + type: "primitive", + mark: { + type: "nothing", }, - ], + }, }); } -function executeCursorlessCommand( - command: Omit, -) { +function executeCursorlessCommand(action: ActionDescriptor) { return runCursorlessCommand({ - ...command, + action, version: LATEST_VERSION, usePrePhraseSnapshot: false, }); } -const MULTIPLE_TARGET_ACTIONS: ActionType[] = [ - "replaceWithTarget", - "moveToTarget", - "swapTargets", -]; - const EXIT_CURSORLESS_MODE_ACTIONS: ActionType[] = [ "setSelectionBefore", "setSelectionAfter",