Skip to content

Commit b25a14e

Browse files
Added break line action (#2103)
`"break air"` ## 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 cc81dbc commit b25a14e

File tree

12 files changed

+243
-2
lines changed

12 files changed

+243
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
tags: [enhancement]
3+
pullRequest: 2103
4+
---
5+
6+
- Added `break` action. This action will break a line in two. eg `"break air"`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
tags: [enhancement]
3+
pullRequest: 2094
4+
---
5+
6+
- Added `visible` modifier. This modifier returns all visible ranges. In most cases this will just be a single range starting from the first visible line and ending at the last visible line. If there are visible folded regions these break the visible range into po multiple ranges.

cursorless-talon/src/spoken_forms.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"actions.csv": {
44
"simple_action": {
55
"bottom": "scrollToBottom",
6+
"break": "breakLine",
67
"break point": "toggleLineBreakpoint",
78
"carve": "cutToClipboard",
89
"center": "scrollToCenter",

docs/user/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,16 @@ eg:
717717
- `join air`: Join the line with the token containing the letter 'a' with its next line.
718718
- `join block air`: Joines all lines in the paragraph with the token containing the letter 'a' together into a single line.
719719
720+
### Break
721+
722+
Break line in two.
723+
724+
- `"break <TARGET>"`
725+
726+
eg:
727+
728+
- `break air`: Break the line with the token containing the letter 'a'. 'a' is now the first token on the new line.
729+
720730
## Paired delimiters
721731
722732
| Default spoken form | Delimiter name | Symbol inserted before target | Symbol inserted after target | Is wrapper? | Is selectable? |

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DestinationDescriptor } from "./DestinationDescriptor.types";
88
* A simple action takes only a single target and no other arguments.
99
*/
1010
const simpleActionNames = [
11+
"breakLine",
1112
"clearAndSetSelection",
1213
"copyToClipboard",
1314
"cutToClipboard",

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { TreeSitter } from "../typings/TreeSitter";
21
import { Snippets } from "../core/Snippets";
32
import { RangeUpdater } from "../core/updateSelections/RangeUpdater";
43
import { ModifierStageFactory } from "../processTargets/ModifierStageFactory";
4+
import { TreeSitter } from "../typings/TreeSitter";
5+
import { BreakLine } from "./BreakLine";
56
import { Bring, Move, Swap } from "./BringMoveSwap";
67
import Call from "./Call";
78
import Clear from "./Clear";
@@ -28,7 +29,6 @@ import {
2829
import InsertSnippet from "./InsertSnippet";
2930
import JoinLines from "./JoinLines";
3031
import { PasteFromClipboard } from "./PasteFromClipboard";
31-
import ShowParseTree from "./ShowParseTree";
3232
import Remove from "./Remove";
3333
import Replace from "./Replace";
3434
import Rewrap from "./Rewrap";
@@ -39,6 +39,7 @@ import {
3939
SetSelectionAfter,
4040
SetSelectionBefore,
4141
} from "./SetSelection";
42+
import ShowParseTree from "./ShowParseTree";
4243
import {
4344
CopyToClipboard,
4445
ExtractVariable,
@@ -113,6 +114,7 @@ export class Actions implements ActionRecord {
113114
this.modifierStageFactory,
114115
);
115116
joinLines = new JoinLines(this.rangeUpdater);
117+
breakLine = new BreakLine(this.rangeUpdater);
116118
moveToTarget = new Move(this.rangeUpdater);
117119
outdentLine = new OutdentLine(this.rangeUpdater);
118120
pasteFromClipboard = new PasteFromClipboard(this.rangeUpdater, this);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { FlashStyle, Position, Range, TextEditor } from "@cursorless/common";
2+
import { flatten, zip } from "lodash";
3+
import type { RangeUpdater } from "../core/updateSelections/RangeUpdater";
4+
import { performEditsAndUpdateRanges } from "../core/updateSelections/updateSelections";
5+
import { ide } from "../singletons/ide.singleton";
6+
import { Edit } from "../typings/Types";
7+
import { Target } from "../typings/target.types";
8+
import { flashTargets, runOnTargetsForEachEditor } from "../util/targetUtils";
9+
import type { ActionReturnValue } from "./actions.types";
10+
11+
export class BreakLine {
12+
constructor(private rangeUpdater: RangeUpdater) {
13+
this.run = this.run.bind(this);
14+
}
15+
16+
async run(targets: Target[]): Promise<ActionReturnValue> {
17+
await flashTargets(ide(), targets, FlashStyle.pendingModification0);
18+
19+
const thatSelections = flatten(
20+
await runOnTargetsForEachEditor(targets, async (editor, targets) => {
21+
const contentRanges = targets.map(({ contentRange }) => contentRange);
22+
const edits = getEdits(editor, contentRanges);
23+
24+
const [updatedRanges] = await performEditsAndUpdateRanges(
25+
this.rangeUpdater,
26+
ide().getEditableTextEditor(editor),
27+
edits,
28+
[contentRanges],
29+
);
30+
31+
return zip(targets, updatedRanges).map(([target, range]) => ({
32+
editor: target!.editor,
33+
selection: range!.toSelection(target!.isReversed),
34+
}));
35+
}),
36+
);
37+
38+
return { thatSelections };
39+
}
40+
}
41+
42+
function getEdits(editor: TextEditor, contentRanges: Range[]): Edit[] {
43+
const { document } = editor;
44+
const edits: Edit[] = [];
45+
46+
for (const range of contentRanges) {
47+
const position = range.start;
48+
const line = document.lineAt(position);
49+
const indentation = line.text.slice(
50+
0,
51+
line.firstNonWhitespaceCharacterIndex,
52+
);
53+
const characterTrailingWhitespace = line.text
54+
.slice(0, position.character)
55+
.search(/\s+$/);
56+
const replacementRange =
57+
characterTrailingWhitespace > -1
58+
? new Range(
59+
new Position(line.lineNumber, characterTrailingWhitespace),
60+
position,
61+
)
62+
: position.toEmptyRange();
63+
64+
edits.push({
65+
range: replacementRange,
66+
text: "\n" + indentation,
67+
isReplace: !replacementRange.isEmpty,
68+
});
69+
}
70+
71+
return edits;
72+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ActionType } from "@cursorless/common";
55
* if the action has no spoken form.
66
*/
77
export const actions = {
8+
breakLine: "break",
89
scrollToBottom: "bottom",
910
toggleLineBreakpoint: "break point",
1011
cutToClipboard: "carve",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
languageId: plaintext
2+
command:
3+
version: 6
4+
spokenForm: break bat
5+
action:
6+
name: breakLine
7+
target:
8+
type: primitive
9+
mark: {type: decoratedSymbol, symbolColor: default, character: b}
10+
usePrePhraseSnapshot: true
11+
initialState:
12+
documentContents: aaa bbb ccc
13+
selections:
14+
- anchor: {line: 0, character: 0}
15+
active: {line: 0, character: 0}
16+
marks:
17+
default.b:
18+
start: {line: 0, character: 4}
19+
end: {line: 0, character: 7}
20+
finalState:
21+
documentContents: |-
22+
aaa
23+
bbb ccc
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
27+
thatMark:
28+
- type: UntypedTarget
29+
contentRange:
30+
start: {line: 1, character: 0}
31+
end: {line: 1, character: 3}
32+
isReversed: false
33+
hasExplicitRange: true
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
languageId: typescript
2+
command:
3+
version: 6
4+
spokenForm: break comment sit
5+
action:
6+
name: breakLine
7+
target:
8+
type: primitive
9+
mark: {type: decoratedSymbol, symbolColor: default, character: i}
10+
modifiers:
11+
- type: containingScope
12+
scopeType: {type: comment}
13+
usePrePhraseSnapshot: true
14+
initialState:
15+
documentContents: |-
16+
[
17+
0,
18+
1, // interesting
19+
2
20+
]
21+
selections:
22+
- anchor: {line: 0, character: 0}
23+
active: {line: 0, character: 0}
24+
marks:
25+
default.i:
26+
start: {line: 2, character: 8}
27+
end: {line: 2, character: 19}
28+
finalState:
29+
documentContents: |-
30+
[
31+
0,
32+
1,
33+
// interesting
34+
2
35+
]
36+
selections:
37+
- anchor: {line: 0, character: 0}
38+
active: {line: 0, character: 0}
39+
thatMark:
40+
- type: UntypedTarget
41+
contentRange:
42+
start: {line: 3, character: 2}
43+
end: {line: 3, character: 16}
44+
isReversed: false
45+
hasExplicitRange: true

0 commit comments

Comments
 (0)