Skip to content

Commit

Permalink
Add action to select new targets (#2707)
Browse files Browse the repository at this point in the history
Adds a new selection while preserving existing ones
`"append <target>"` replaces `"take this and <target>"`

You can also use `append pre` and `append post` to add a cursor before
or after the given target (rather than selecting that target), while
keeping your current selections unchanged. (Previously you could do
something like `pre this and <target>` but this would also reduce your
current selection to a cursor before the current target.)

## Checklist

- [x] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [x] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [x] I have not broken the cheatsheet

---------

Co-authored-by: Phil Cohen <[email protected]>
  • Loading branch information
AndreasArvidsson and phillco authored Jan 9, 2025
1 parent c69a57a commit 43e9e21
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 19 deletions.
9 changes: 6 additions & 3 deletions cursorless-talon/src/spoken_forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -22,8 +25,8 @@
"extract": "extractVariable",
"float": "insertEmptyLineAfter",
"fold": "foldRegion",
"follow": "followLink",
"follow split": "followLinkAside",
"follow": "followLink",
"give": "deselect",
"highlight": "highlight",
"hover": "showHover",
Expand All @@ -39,8 +42,8 @@
"reference": "showReferences",
"rename": "rename",
"reverse": "reverseTargets",
"scout": "findInDocument",
"scout all": "findInWorkspace",
"scout": "findInDocument",
"shuffle": "randomizeTargets",
"snippet make": "generateSnippet",
"sort": "sortTargets",
Expand Down
33 changes: 33 additions & 0 deletions data/fixtures/recorded/actions/appendPostWhale.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions data/fixtures/recorded/actions/appendPreWhale.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions data/fixtures/recorded/actions/appendWhale.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion packages/common/src/types/command/ActionDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -52,9 +55,9 @@ export const simpleActionNames = [
"toggleLineBreakpoint",
"toggleLineComment",
"unfoldRegion",
"private.getTargets",
"private.setKeyboardTarget",
"private.showParseTree",
"private.getTargets",
] as const;

const complexActionNames = [
Expand Down
3 changes: 3 additions & 0 deletions packages/cursorless-engine/src/CommandHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
8 changes: 7 additions & 1 deletion packages/cursorless-engine/src/actions/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
71 changes: 58 additions & 13 deletions packages/cursorless-engine/src/actions/SetSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActionReturnValue> {
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 });
Expand All @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion packages/cursorless-org-docs/src/docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TARGET>"`: Selects the given target.
- `"pre <TARGET>"`: Places the cursor before the given target.
- `"post <TARGET>"`: Places the cursor after the given target.
- `"take <TARGET>"`: Selects the given target.
- `"append <TARGET>"`: Selects the given target, while preserving your existing selections.
- `"append pre <TARGET>"`: Adds a new cursor before the given target, while preserving your existing selections.
- `"append post <TARGET>"`: Adds a new cursor after the given target, while preserving your existing selections.
- `"give <TARGET>"`: Deselects the given target.
eg:
Expand Down

0 comments on commit 43e9e21

Please sign in to comment.