Skip to content

Commit aaf645a

Browse files
Added increment and decrement actions (#2236)
Fixes #2192 ## 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: Pokey Rule <[email protected]>
1 parent 585c852 commit aaf645a

File tree

12 files changed

+564
-1
lines changed

12 files changed

+564
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
tags: [enhancement]
3+
pullRequest: 2236
4+
---
5+
6+
- Added increment action. Will increment a number. eg `"increment this"` to change `1` to `2`.
7+
8+
- Added decrement action. Will decrement a number. eg `"decrement this"` to change `2` to `1`.

cursorless-talon/src/spoken_forms.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"comment": "toggleLineComment",
1515
"copy": "copyToClipboard",
1616
"crown": "scrollToTop",
17+
"decrement": "decrement",
1718
"dedent": "outdentLine",
1819
"define": "revealDefinition",
1920
"drink": "editNewLineBefore",
@@ -25,6 +26,7 @@
2526
"give": "deselect",
2627
"highlight": "highlight",
2728
"hover": "showHover",
29+
"increment": "increment",
2830
"indent": "indentLine",
2931
"inspect": "showDebugHover",
3032
"join": "joinLines",

docs/user/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,11 @@ For example:
584584
- `"indent air"`
585585
- `"dedent funk bat"`
586586
587+
### Increment / decrement
588+
589+
- `"increment <TARGET>"`: increment number target. eg change `1` to `2`.
590+
- `"decrement <TARGET>"`: decrement number target. eg change `2` to `1`.
591+
587592
### Insert empty lines
588593
589594
- `"drink <TARGET>"`: Inserts a new line above the target line, and moves the cursor to the newly created line

packages/common/src/types/command/ActionDescriptor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const simpleActionNames = [
1212
"clearAndSetSelection",
1313
"copyToClipboard",
1414
"cutToClipboard",
15+
"decrement",
1516
"deselect",
1617
"editNewLineAfter",
1718
"editNewLineBefore",
@@ -21,6 +22,7 @@ const simpleActionNames = [
2122
"findInWorkspace",
2223
"foldRegion",
2324
"followLink",
25+
"increment",
2426
"indentLine",
2527
"insertCopyAfter",
2628
"insertCopyBefore",

packages/cursorless-engine/src/CommandHistory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ function sanitizeActionInPlace(action: ActionDescriptor): void {
144144
case "clearAndSetSelection":
145145
case "copyToClipboard":
146146
case "cutToClipboard":
147+
case "decrement":
147148
case "deselect":
148149
case "editNewLineAfter":
149150
case "editNewLineBefore":
@@ -152,6 +153,7 @@ function sanitizeActionInPlace(action: ActionDescriptor): void {
152153
case "findInWorkspace":
153154
case "foldRegion":
154155
case "followLink":
156+
case "increment":
155157
case "indentLine":
156158
case "insertCopyAfter":
157159
case "insertCopyBefore":

packages/cursorless-engine/src/actions/Actions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ import Remove from "./Remove";
3333
import Replace from "./Replace";
3434
import Rewrap from "./Rewrap";
3535
import { ScrollToBottom, ScrollToCenter, ScrollToTop } from "./Scroll";
36-
import { SetSpecialTarget } from "./SetSpecialTarget";
3736
import {
3837
SetSelection,
3938
SetSelectionAfter,
4039
SetSelectionBefore,
4140
} from "./SetSelection";
41+
import { SetSpecialTarget } from "./SetSpecialTarget";
4242
import ShowParseTree from "./ShowParseTree";
4343
import {
4444
CopyToClipboard,
@@ -61,6 +61,7 @@ import ToggleBreakpoint from "./ToggleBreakpoint";
6161
import Wrap from "./Wrap";
6262
import WrapWithSnippet from "./WrapWithSnippet";
6363
import { ActionRecord } from "./actions.types";
64+
import { Decrement, Increment } from "./incrementDecrement";
6465

6566
/**
6667
* Keeps a map from action names to objects that implement the given action
@@ -77,6 +78,7 @@ export class Actions implements ActionRecord {
7778
clearAndSetSelection = new Clear(this);
7879
copyToClipboard = new CopyToClipboard(this.rangeUpdater);
7980
cutToClipboard = new CutToClipboard(this);
81+
decrement = new Decrement(this);
8082
deselect = new Deselect();
8183
editNew = new EditNew(this.rangeUpdater, this);
8284
editNewLineAfter: EditNewAfter = new EditNewAfter(
@@ -96,6 +98,7 @@ export class Actions implements ActionRecord {
9698
generateSnippet = new GenerateSnippet();
9799
getText = new GetText();
98100
highlight = new Highlight();
101+
increment = new Increment(this);
99102
indentLine = new IndentLine(this.rangeUpdater);
100103
insertCopyAfter = new InsertCopyAfter(
101104
this.rangeUpdater,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Range, TextEditor } from "@cursorless/common";
2+
import { PlainTarget } from "../processTargets/targets";
3+
import { SelectionWithEditor } from "../typings/Types";
4+
import { Destination, Target } from "../typings/target.types";
5+
import { MatchedText, matchText } from "../util/regex";
6+
import { runForEachEditor } from "../util/targetUtils";
7+
import { Actions } from "./Actions";
8+
import { ActionReturnValue } from "./actions.types";
9+
10+
const REGEX = /-?\d+(\.\d+)?/g;
11+
12+
class IncrementDecrement {
13+
constructor(
14+
private actions: Actions,
15+
private isIncrement: boolean,
16+
) {
17+
this.run = this.run.bind(this);
18+
}
19+
20+
async run(targets: Target[]): Promise<ActionReturnValue> {
21+
const thatSelections: SelectionWithEditor[] = [];
22+
23+
await runForEachEditor(
24+
targets,
25+
(target) => target.editor,
26+
async (editor, targets) => {
27+
const selections = await this.runOnEditor(editor, targets);
28+
thatSelections.push(...selections);
29+
},
30+
);
31+
32+
return { thatSelections };
33+
}
34+
35+
private async runOnEditor(
36+
editor: TextEditor,
37+
targets: Target[],
38+
): Promise<SelectionWithEditor[]> {
39+
const { document } = editor;
40+
const destinations: Destination[] = [];
41+
const replaceWith: string[] = [];
42+
43+
for (const target of targets) {
44+
const offset = document.offsetAt(target.contentRange.start);
45+
const text = target.contentText;
46+
const matches = matchText(text, REGEX);
47+
48+
for (const match of matches) {
49+
destinations.push(createDestination(editor, offset, match));
50+
replaceWith.push(updateNumber(this.isIncrement, match.text));
51+
}
52+
}
53+
54+
const { thatSelections } = await this.actions.replace.run(
55+
destinations,
56+
replaceWith,
57+
);
58+
59+
return thatSelections!;
60+
}
61+
}
62+
63+
export class Increment extends IncrementDecrement {
64+
constructor(actions: Actions) {
65+
super(actions, true);
66+
}
67+
}
68+
69+
export class Decrement extends IncrementDecrement {
70+
constructor(actions: Actions) {
71+
super(actions, false);
72+
}
73+
}
74+
75+
function createDestination(
76+
editor: TextEditor,
77+
offset: number,
78+
match: MatchedText,
79+
): Destination {
80+
const target = new PlainTarget({
81+
editor,
82+
isReversed: false,
83+
contentRange: new Range(
84+
editor.document.positionAt(offset + match.index),
85+
editor.document.positionAt(offset + match.index + match.text.length),
86+
),
87+
});
88+
return target.toDestination("to");
89+
}
90+
91+
function updateNumber(isIncrement: boolean, text: string): string {
92+
return text.includes(".")
93+
? updateFloat(isIncrement, text).toString()
94+
: updateInteger(isIncrement, text).toString();
95+
}
96+
97+
function updateInteger(isIncrement: boolean, text: string): number {
98+
const original = parseInt(text);
99+
const diff = 1;
100+
return original + (isIncrement ? diff : -diff);
101+
}
102+
103+
function updateFloat(isIncrement: boolean, text: string): number {
104+
const original = parseFloat(text);
105+
const isPercentage = Math.abs(original) <= 1.0;
106+
const diff = isPercentage ? 0.1 : 1;
107+
const updated = original + (isIncrement ? diff : -diff);
108+
// Remove precision problems that would add a lot of extra digits
109+
return parseFloat(updated.toPrecision(15)) / 1;
110+
}

packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const actions = {
2828
deselect: "give",
2929
highlight: "highlight",
3030
showHover: "hover",
31+
increment: "increment",
32+
decrement: "decrement",
3133
indentLine: "indent",
3234
showDebugHover: "inspect",
3335
setSelectionAfter: "post",
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
languageId: plaintext
2+
command:
3+
version: 6
4+
spokenForm: decrement file
5+
action:
6+
name: decrement
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: document}
12+
usePrePhraseSnapshot: true
13+
initialState:
14+
documentContents: |-
15+
foo
16+
17+
0
18+
1
19+
0.5
20+
1.5
21+
22+
-0
23+
-1
24+
-0.5
25+
-1.5
26+
27+
0rem
28+
1rem
29+
0.5rem
30+
1.5rem
31+
32+
-0rem
33+
-1rem
34+
-0.5rem
35+
-1.5rem
36+
selections:
37+
- anchor: {line: 0, character: 0}
38+
active: {line: 0, character: 0}
39+
marks: {}
40+
finalState:
41+
documentContents: |-
42+
foo
43+
44+
-1
45+
0
46+
0.4
47+
0.5
48+
49+
-1
50+
-2
51+
-0.6
52+
-2.5
53+
54+
-1rem
55+
0rem
56+
0.4rem
57+
0.5rem
58+
59+
-1rem
60+
-2rem
61+
-0.6rem
62+
-2.5rem
63+
selections:
64+
- anchor: {line: 0, character: 0}
65+
active: {line: 0, character: 0}
66+
thatMark:
67+
- type: UntypedTarget
68+
contentRange:
69+
start: {line: 2, character: 0}
70+
end: {line: 2, character: 2}
71+
isReversed: false
72+
hasExplicitRange: true
73+
- type: UntypedTarget
74+
contentRange:
75+
start: {line: 3, character: 0}
76+
end: {line: 3, character: 1}
77+
isReversed: false
78+
hasExplicitRange: true
79+
- type: UntypedTarget
80+
contentRange:
81+
start: {line: 4, character: 0}
82+
end: {line: 4, character: 3}
83+
isReversed: false
84+
hasExplicitRange: true
85+
- type: UntypedTarget
86+
contentRange:
87+
start: {line: 5, character: 0}
88+
end: {line: 5, character: 3}
89+
isReversed: false
90+
hasExplicitRange: true
91+
- type: UntypedTarget
92+
contentRange:
93+
start: {line: 7, character: 0}
94+
end: {line: 7, character: 2}
95+
isReversed: false
96+
hasExplicitRange: true
97+
- type: UntypedTarget
98+
contentRange:
99+
start: {line: 8, character: 0}
100+
end: {line: 8, character: 2}
101+
isReversed: false
102+
hasExplicitRange: true
103+
- type: UntypedTarget
104+
contentRange:
105+
start: {line: 9, character: 0}
106+
end: {line: 9, character: 4}
107+
isReversed: false
108+
hasExplicitRange: true
109+
- type: UntypedTarget
110+
contentRange:
111+
start: {line: 10, character: 0}
112+
end: {line: 10, character: 4}
113+
isReversed: false
114+
hasExplicitRange: true
115+
- type: UntypedTarget
116+
contentRange:
117+
start: {line: 12, character: 0}
118+
end: {line: 12, character: 2}
119+
isReversed: false
120+
hasExplicitRange: true
121+
- type: UntypedTarget
122+
contentRange:
123+
start: {line: 13, character: 0}
124+
end: {line: 13, character: 1}
125+
isReversed: false
126+
hasExplicitRange: true
127+
- type: UntypedTarget
128+
contentRange:
129+
start: {line: 14, character: 0}
130+
end: {line: 14, character: 3}
131+
isReversed: false
132+
hasExplicitRange: true
133+
- type: UntypedTarget
134+
contentRange:
135+
start: {line: 15, character: 0}
136+
end: {line: 15, character: 3}
137+
isReversed: false
138+
hasExplicitRange: true
139+
- type: UntypedTarget
140+
contentRange:
141+
start: {line: 17, character: 0}
142+
end: {line: 17, character: 2}
143+
isReversed: false
144+
hasExplicitRange: true
145+
- type: UntypedTarget
146+
contentRange:
147+
start: {line: 18, character: 0}
148+
end: {line: 18, character: 2}
149+
isReversed: false
150+
hasExplicitRange: true
151+
- type: UntypedTarget
152+
contentRange:
153+
start: {line: 19, character: 0}
154+
end: {line: 19, character: 4}
155+
isReversed: false
156+
hasExplicitRange: true
157+
- type: UntypedTarget
158+
contentRange:
159+
start: {line: 20, character: 0}
160+
end: {line: 20, character: 4}
161+
isReversed: false
162+
hasExplicitRange: true

0 commit comments

Comments
 (0)