Skip to content

Commit 740e3ef

Browse files
Added join lines action (#1901)
`"join air"` `"join block air"` Fixes #50 ## 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 70cbd86 commit 740e3ef

File tree

15 files changed

+362
-0
lines changed

15 files changed

+362
-0
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: 1901
4+
---
5+
6+
- Added `join` action. This action will join multiple lines together. eg `"join air"` or `"join three lines air"`.

cursorless-talon/src/spoken_forms.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"hover": "showHover",
2727
"indent": "indentLine",
2828
"inspect": "showDebugHover",
29+
"join": "joinLines",
2930
"post": "setSelectionAfter",
3031
"pour": "editNewLineAfter",
3132
"pre": "setSelectionBefore",

docs/user/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,17 @@ eg:
706706
707707
Extracts the function call containing the decorated 'a' into its own variable.
708708
709+
### Join
710+
711+
Join multiple lines together.
712+
713+
- `"join <TARGET>"`
714+
715+
eg:
716+
717+
- `join air`: Join the line with the token containing the letter 'a' with its next line.
718+
- `join block air`: Joines all lines in the paragraph with the token containing the letter 'a' together into a single line.
719+
709720
## Paired delimiters
710721
711722
| 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
@@ -25,6 +25,7 @@ const simpleActionNames = [
2525
"insertEmptyLineAfter",
2626
"insertEmptyLineBefore",
2727
"insertEmptyLinesAround",
28+
"joinLines",
2829
"outdentLine",
2930
"randomizeTargets",
3031
"remove",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
InsertEmptyLinesAround,
2727
} from "./InsertEmptyLines";
2828
import InsertSnippet from "./InsertSnippet";
29+
import JoinLines from "./JoinLines";
2930
import { PasteFromClipboard } from "./PasteFromClipboard";
3031
import ShowParseTree from "./ShowParseTree";
3132
import Remove from "./Remove";
@@ -111,6 +112,7 @@ export class Actions implements ActionRecord {
111112
this,
112113
this.modifierStageFactory,
113114
);
115+
joinLines = new JoinLines(this.rangeUpdater);
114116
moveToTarget = new Move(this.rangeUpdater);
115117
outdentLine = new OutdentLine(this.rangeUpdater);
116118
pasteFromClipboard = new PasteFromClipboard(this.rangeUpdater, this);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { FlashStyle, 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+
import { range as iterRange, map, pairwise } from "itertools";
11+
12+
export default class JoinLines {
13+
constructor(private rangeUpdater: RangeUpdater) {
14+
this.run = this.run.bind(this);
15+
}
16+
17+
async run(targets: Target[]): Promise<ActionReturnValue> {
18+
await flashTargets(ide(), targets, FlashStyle.pendingModification0);
19+
20+
const thatSelections = flatten(
21+
await runOnTargetsForEachEditor(targets, async (editor, targets) => {
22+
const contentRanges = targets.map(({ contentRange }) => contentRange);
23+
const edits = getEdits(editor, contentRanges);
24+
25+
const [updatedRanges] = await performEditsAndUpdateRanges(
26+
this.rangeUpdater,
27+
ide().getEditableTextEditor(editor),
28+
edits,
29+
[contentRanges],
30+
);
31+
32+
return zip(targets, updatedRanges).map(([target, range]) => ({
33+
editor: target!.editor,
34+
selection: range!.toSelection(target!.isReversed),
35+
}));
36+
}),
37+
);
38+
39+
return { thatSelections };
40+
}
41+
}
42+
43+
function getEdits(editor: TextEditor, contentRanges: Range[]): Edit[] {
44+
const { document } = editor;
45+
const edits: Edit[] = [];
46+
47+
for (const range of contentRanges) {
48+
const startLine = range.start.line;
49+
const endLine = range.isSingleLine ? startLine + 1 : range.end.line;
50+
51+
const lineIter = map(iterRange(startLine, endLine + 1), (i) =>
52+
document.lineAt(i),
53+
);
54+
for (const [line1, line2] of pairwise(lineIter)) {
55+
edits.push({
56+
range: new Range(
57+
line1.range.end.line,
58+
line1.lastNonWhitespaceCharacterIndex,
59+
line2.range.start.line,
60+
line2.firstNonWhitespaceCharacterIndex,
61+
),
62+
text: line2.isEmptyOrWhitespace ? "" : " ",
63+
isReplace: true,
64+
});
65+
}
66+
}
67+
68+
return edits;
69+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const actions = {
5353
rewrapWithPairedDelimiter: "repack",
5454
insertSnippet: "snippet",
5555
pasteFromClipboard: "paste",
56+
joinLines: "join",
5657

5758
["private.showParseTree"]: "parse tree",
5859
["experimental.setInstanceReference"]: "from",
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: join air
5+
action:
6+
name: joinLines
7+
target:
8+
type: primitive
9+
mark: {type: decoratedSymbol, symbolColor: default, character: a}
10+
usePrePhraseSnapshot: true
11+
initialState:
12+
documentContents: |-
13+
aaa
14+
bbb
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks:
19+
default.a:
20+
start: {line: 0, character: 0}
21+
end: {line: 0, character: 3}
22+
finalState:
23+
documentContents: aaa bbb
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
27+
thatMark:
28+
- type: UntypedTarget
29+
contentRange:
30+
start: {line: 0, character: 0}
31+
end: {line: 0, character: 3}
32+
isReversed: false
33+
hasExplicitRange: true
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: join air
5+
action:
6+
name: joinLines
7+
target:
8+
type: primitive
9+
mark: {type: decoratedSymbol, symbolColor: default, character: a}
10+
usePrePhraseSnapshot: true
11+
initialState:
12+
documentContents: |-
13+
aaa
14+
bbb
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks:
19+
default.a:
20+
start: {line: 0, character: 0}
21+
end: {line: 0, character: 3}
22+
finalState:
23+
documentContents: aaa bbb
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
27+
thatMark:
28+
- type: UntypedTarget
29+
contentRange:
30+
start: {line: 0, character: 0}
31+
end: {line: 0, character: 3}
32+
isReversed: false
33+
hasExplicitRange: true
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: join air
5+
action:
6+
name: joinLines
7+
target:
8+
type: primitive
9+
mark: {type: decoratedSymbol, symbolColor: default, character: a}
10+
usePrePhraseSnapshot: true
11+
initialState:
12+
documentContents: |-
13+
aaa
14+
bbb
15+
selections:
16+
- anchor: {line: 0, character: 0}
17+
active: {line: 0, character: 0}
18+
marks:
19+
default.a:
20+
start: {line: 0, character: 0}
21+
end: {line: 0, character: 3}
22+
finalState:
23+
documentContents: aaa bbb
24+
selections:
25+
- anchor: {line: 0, character: 0}
26+
active: {line: 0, character: 0}
27+
thatMark:
28+
- type: UntypedTarget
29+
contentRange:
30+
start: {line: 0, character: 0}
31+
end: {line: 0, character: 3}
32+
isReversed: false
33+
hasExplicitRange: true

0 commit comments

Comments
 (0)