diff --git a/cursorless-talon/src/spoken_forms.json b/cursorless-talon/src/spoken_forms.json index 8f1034afe3..249b75abcf 100644 --- a/cursorless-talon/src/spoken_forms.json +++ b/cursorless-talon/src/spoken_forms.json @@ -2,9 +2,12 @@ "NOTE FOR USERS": "Please don't edit this json file; see https://www.cursorless.org/docs/user/customization", "actions.csv": { "simple_action": { + "append post": "addSelectionAfter", + "append pre": "addSelectionBefore", + "append": "addSelection", "bottom": "scrollToBottom", - "break": "breakLine", "break point": "toggleLineBreakpoint", + "break": "breakLine", "carve": "cutToClipboard", "center": "scrollToCenter", "change": "clearAndSetSelection", @@ -22,8 +25,8 @@ "extract": "extractVariable", "float": "insertEmptyLineAfter", "fold": "foldRegion", - "follow": "followLink", "follow split": "followLinkAside", + "follow": "followLink", "give": "deselect", "highlight": "highlight", "hover": "showHover", @@ -39,8 +42,8 @@ "reference": "showReferences", "rename": "rename", "reverse": "reverseTargets", - "scout": "findInDocument", "scout all": "findInWorkspace", + "scout": "findInDocument", "shuffle": "randomizeTargets", "snippet make": "generateSnippet", "sort": "sortTargets", diff --git a/data/fixtures/recorded/actions/appendPostWhale.yml b/data/fixtures/recorded/actions/appendPostWhale.yml new file mode 100644 index 0000000000..dd266c1a93 --- /dev/null +++ b/data/fixtures/recorded/actions/appendPostWhale.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + version: 7 + spokenForm: append post whale + action: + name: addSelectionAfter + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 11} + active: {line: 0, character: 11} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 6} + end: {line: 0, character: 11} + isReversed: false + hasExplicitRange: false diff --git a/data/fixtures/recorded/actions/appendPreWhale.yml b/data/fixtures/recorded/actions/appendPreWhale.yml new file mode 100644 index 0000000000..765c092161 --- /dev/null +++ b/data/fixtures/recorded/actions/appendPreWhale.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + version: 7 + spokenForm: append pre whale + action: + name: addSelectionBefore + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 6} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 6} + end: {line: 0, character: 11} + isReversed: false + hasExplicitRange: false diff --git a/data/fixtures/recorded/actions/appendWhale.yml b/data/fixtures/recorded/actions/appendWhale.yml new file mode 100644 index 0000000000..9acc5078e4 --- /dev/null +++ b/data/fixtures/recorded/actions/appendWhale.yml @@ -0,0 +1,33 @@ +languageId: plaintext +command: + version: 7 + spokenForm: append whale + action: + name: addSelection + target: + type: primitive + mark: {type: decoratedSymbol, symbolColor: default, character: w} + usePrePhraseSnapshot: true +initialState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: + default.w: + start: {line: 0, character: 6} + end: {line: 0, character: 11} +finalState: + documentContents: hello world + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + - anchor: {line: 0, character: 6} + active: {line: 0, character: 11} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 6} + end: {line: 0, character: 11} + isReversed: false + hasExplicitRange: false diff --git a/packages/common/src/types/command/ActionDescriptor.ts b/packages/common/src/types/command/ActionDescriptor.ts index 200918604d..c064f28339 100644 --- a/packages/common/src/types/command/ActionDescriptor.ts +++ b/packages/common/src/types/command/ActionDescriptor.ts @@ -8,6 +8,9 @@ import type { DestinationDescriptor } from "./DestinationDescriptor.types"; * A simple action takes only a single target and no other arguments. */ export const simpleActionNames = [ + "addSelection", + "addSelectionAfter", + "addSelectionBefore", "breakLine", "clearAndSetSelection", "copyToClipboard", @@ -52,9 +55,9 @@ export const simpleActionNames = [ "toggleLineBreakpoint", "toggleLineComment", "unfoldRegion", + "private.getTargets", "private.setKeyboardTarget", "private.showParseTree", - "private.getTargets", ] as const; const complexActionNames = [ diff --git a/packages/cursorless-engine/src/CommandHistory.ts b/packages/cursorless-engine/src/CommandHistory.ts index b68f256b38..65a148539c 100644 --- a/packages/cursorless-engine/src/CommandHistory.ts +++ b/packages/cursorless-engine/src/CommandHistory.ts @@ -130,6 +130,9 @@ function sanitizeActionInPlace(action: ActionDescriptor): void { delete action.options?.commandArgs; break; + case "addSelection": + case "addSelectionAfter": + case "addSelectionBefore": case "breakLine": case "clearAndSetSelection": case "copyToClipboard": diff --git a/packages/cursorless-engine/src/actions/Actions.ts b/packages/cursorless-engine/src/actions/Actions.ts index 18258bf4bb..3bdd24e28c 100644 --- a/packages/cursorless-engine/src/actions/Actions.ts +++ b/packages/cursorless-engine/src/actions/Actions.ts @@ -18,6 +18,7 @@ import GenerateSnippet from "./GenerateSnippet"; import GetTargets from "./GetTargets"; import GetText from "./GetText"; import Highlight from "./Highlight"; +import { IndentLine, OutdentLine } from "./IndentLine"; import { CopyContentAfter as InsertCopyAfter, CopyContentBefore as InsertCopyBefore, @@ -35,13 +36,15 @@ import Replace from "./Replace"; import Rewrap from "./Rewrap"; import { ScrollToBottom, ScrollToCenter, ScrollToTop } from "./Scroll"; import { + AddSelection, + AddSelectionAfter, + AddSelectionBefore, SetSelection, SetSelectionAfter, SetSelectionBefore, } from "./SetSelection"; import { SetSpecialTarget } from "./SetSpecialTarget"; import ShowParseTree from "./ShowParseTree"; -import { IndentLine, OutdentLine } from "./IndentLine"; import { ExtractVariable, Fold, @@ -73,6 +76,9 @@ export class Actions implements ActionRecord { private modifierStageFactory: ModifierStageFactory, ) {} + addSelection = new AddSelection(); + addSelectionBefore = new AddSelectionBefore(); + addSelectionAfter = new AddSelectionAfter(); callAsFunction = new Call(this); clearAndSetSelection = new Clear(this); copyToClipboard = new CopyToClipboard(this, this.rangeUpdater); diff --git a/packages/cursorless-engine/src/actions/SetSelection.ts b/packages/cursorless-engine/src/actions/SetSelection.ts index 2bab32a858..db46311631 100644 --- a/packages/cursorless-engine/src/actions/SetSelection.ts +++ b/packages/cursorless-engine/src/actions/SetSelection.ts @@ -4,19 +4,23 @@ import type { Target } from "../typings/target.types"; import { ensureSingleEditor } from "../util/targetUtils"; import type { SimpleAction, ActionReturnValue } from "./actions.types"; -export class SetSelection implements SimpleAction { - constructor() { +abstract class SetSelectionBase implements SimpleAction { + constructor( + private selectionMode: "set" | "add", + private rangeMode: "content" | "before" | "after", + ) { this.run = this.run.bind(this); } - protected getSelection(target: Target) { - return target.contentSelection; - } - async run(targets: Target[]): Promise { const editor = ensureSingleEditor(targets); + const targetSelections = this.getSelections(targets); + + const selections = + this.selectionMode === "add" + ? editor.selections.concat(targetSelections) + : targetSelections; - const selections = targets.map(this.getSelection); await ide() .getEditableTextEditor(editor) .setSelections(selections, { focusEditor: true }); @@ -25,16 +29,57 @@ export class SetSelection implements SimpleAction { thatTargets: targets, }; } + + private getSelections(targets: Target[]): Selection[] { + switch (this.rangeMode) { + case "content": + return targets.map((target) => target.contentSelection); + case "before": + return targets.map( + (target) => + new Selection(target.contentRange.start, target.contentRange.start), + ); + case "after": + return targets.map( + (target) => + new Selection(target.contentRange.end, target.contentRange.end), + ); + } + } +} + +export class SetSelection extends SetSelectionBase { + constructor() { + super("set", "content"); + } +} + +export class SetSelectionBefore extends SetSelectionBase { + constructor() { + super("set", "before"); + } +} + +export class SetSelectionAfter extends SetSelectionBase { + constructor() { + super("set", "after"); + } } -export class SetSelectionBefore extends SetSelection { - protected getSelection(target: Target) { - return new Selection(target.contentRange.start, target.contentRange.start); +export class AddSelection extends SetSelectionBase { + constructor() { + super("add", "content"); + } +} + +export class AddSelectionBefore extends SetSelectionBase { + constructor() { + super("add", "before"); } } -export class SetSelectionAfter extends SetSelection { - protected getSelection(target: Target) { - return new Selection(target.contentRange.end, target.contentRange.end); +export class AddSelectionAfter extends SetSelectionBase { + constructor() { + super("add", "after"); } } diff --git a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts index 7717823ec9..0aeefabcd8 100644 --- a/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts +++ b/packages/cursorless-engine/src/spokenForms/defaultSpokenFormMapCore.ts @@ -144,6 +144,9 @@ export const defaultSpokenFormMapCore: DefaultSpokenFormMapDefinition = { customRegex: {}, action: { + addSelection: "append", + addSelectionAfter: "append post", + addSelectionBefore: "append pre", breakLine: "break", scrollToBottom: "bottom", toggleLineBreakpoint: "break point", diff --git a/packages/cursorless-org-docs/src/docs/user/README.md b/packages/cursorless-org-docs/src/docs/user/README.md index fbf667ee25..a6b43c6a45 100644 --- a/packages/cursorless-org-docs/src/docs/user/README.md +++ b/packages/cursorless-org-docs/src/docs/user/README.md @@ -531,9 +531,12 @@ Despite the name cursorless, some of the most basic commands in cursorless are f Note that when combined with list targets, `take`/`pre`/`post` commands will result in multiple cursors. +- `"take "`: Selects the given target. - `"pre "`: Places the cursor before the given target. - `"post "`: Places the cursor after the given target. -- `"take "`: Selects the given target. +- `"append "`: Selects the given target, while preserving your existing selections. +- `"append pre "`: Adds a new cursor before the given target, while preserving your existing selections. +- `"append post "`: Adds a new cursor after the given target, while preserving your existing selections. - `"give "`: Deselects the given target. eg: