Skip to content

Commit 8d9a8e8

Browse files
Support delimiter based edit newline (#684)
* Added delimiter based edit line * Added support for arbitrary delimiters * Added tests * Renamed is above to is before * Better handling of empty lines * Cleanup * Added default delimiters for some common scope types
1 parent b68c16e commit 8d9a8e8

File tree

14 files changed

+327
-69
lines changed

14 files changed

+327
-69
lines changed

src/actions/EditNewLine.ts

+118-45
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,139 @@
1-
import { commands, Range, TextEditor } from "vscode";
1+
import {
2+
commands as vscommands,
3+
Position,
4+
Range,
5+
Selection,
6+
TextEditor,
7+
} from "vscode";
28
import { Target } from "../typings/target.types";
39
import { Graph } from "../typings/Types";
4-
import { getNotebookFromCellDocument } from "../util/notebook";
10+
import { createThatMark, ensureSingleEditor } from "../util/targetUtils";
511
import { Action, ActionReturnValue } from "./actions.types";
612

713
class EditNewLine implements Action {
8-
constructor(private graph: Graph, private isAbove: boolean) {
14+
constructor(private graph: Graph, private isBefore: boolean) {
915
this.run = this.run.bind(this);
1016
}
1117

12-
private correctForParagraph(targets: Target[]) {
13-
targets.forEach((target) => {
14-
let { start, end } = target.contentRange;
15-
if (target.scopeType === "paragraph") {
16-
if (this.isAbove && target.leadingDelimiter != null) {
17-
start = start.translate({ lineDelta: -1 });
18-
} else if (!this.isAbove && target.trailingDelimiter != null) {
19-
end = end.translate({ lineDelta: 1 });
20-
}
21-
target.contentRange = new Range(start, end);
22-
}
23-
});
24-
}
18+
async run([targets]: [Target[]]): Promise<ActionReturnValue> {
19+
const editor = ensureSingleEditor(targets);
2520

26-
private isNotebookEditor(editor: TextEditor) {
27-
return getNotebookFromCellDocument(editor.document) != null;
28-
}
21+
const targetsWithContext = targets.map((target) => ({
22+
target,
23+
context: target.getEditNewLineContext(this.isBefore),
24+
}));
25+
const commandTargets = targetsWithContext.filter(
26+
({ context }) => !!(<any>context).command
27+
);
28+
const delimiterTargets = targetsWithContext.filter(
29+
({ context }) => !!(<any>context).delimiter
30+
);
2931

30-
private getCommand(target: Target) {
31-
if (target.scopeType === "notebookCell") {
32-
if (this.isNotebookEditor(target.editor)) {
33-
return this.isAbove
34-
? "notebook.cell.insertCodeCellAbove"
35-
: "notebook.cell.insertCodeCellBelow";
36-
}
37-
return this.isAbove
38-
? "jupyter.insertCellAbove"
39-
: "jupyter.insertCellBelow";
32+
if (commandTargets.length > 0 && delimiterTargets.length > 0) {
33+
throw new Error("Can't insert edit using command and delimiter at once");
34+
}
35+
36+
if (commandTargets.length > 0) {
37+
const commands = commandTargets.map(
38+
({ context }) => (<any>context).command
39+
);
40+
return {
41+
thatMark: await this.runCommand(targets, commands),
42+
};
4043
}
41-
return this.isAbove
42-
? "editor.action.insertLineBefore"
43-
: "editor.action.insertLineAfter";
44+
45+
return {
46+
thatMark: await this.runDelimiter(targets, editor),
47+
};
4448
}
4549

46-
async run([targets]: [Target[]]): Promise<ActionReturnValue> {
47-
this.correctForParagraph(targets);
50+
async runDelimiter(targets: Target[], editor: TextEditor) {
51+
const edits = targets.map((target) => {
52+
const { contentRange } = target;
53+
const context = target.getEditNewLineContext(this.isBefore);
54+
const delimiter = (<any>context).delimiter as string;
4855

49-
if (this.isAbove) {
56+
// Delimiter is one or more new lines. Handle as lines.
57+
if (delimiter.includes("\n")) {
58+
const lineNumber = this.isBefore
59+
? contentRange.start.line
60+
: contentRange.end.line;
61+
const line = editor.document.lineAt(lineNumber);
62+
const characterIndex = line.isEmptyOrWhitespace
63+
? contentRange.start.character
64+
: line.firstNonWhitespaceCharacterIndex;
65+
const padding = line.text.slice(0, characterIndex);
66+
const positionSelection = new Position(
67+
this.isBefore ? lineNumber : lineNumber + delimiter.length,
68+
characterIndex
69+
);
70+
return {
71+
contentRange,
72+
text: this.isBefore ? padding + delimiter : delimiter + padding,
73+
insertPosition: this.isBefore ? line.range.start : line.range.end,
74+
selection: new Selection(positionSelection, positionSelection),
75+
thatMarkRange: this.isBefore
76+
? new Range(
77+
contentRange.start.translate({
78+
lineDelta: delimiter.length,
79+
}),
80+
contentRange.end.translate({
81+
lineDelta: delimiter.length,
82+
})
83+
)
84+
: contentRange,
85+
};
86+
}
87+
// Delimiter is something else. Handle as inline.
88+
else {
89+
const positionSelection = this.isBefore
90+
? contentRange.start
91+
: contentRange.end.translate({
92+
characterDelta: delimiter.length,
93+
});
94+
return {
95+
contentRange,
96+
text: delimiter,
97+
insertPosition: this.isBefore ? contentRange.start : contentRange.end,
98+
selection: new Selection(positionSelection, positionSelection),
99+
thatMarkRange: this.isBefore
100+
? new Range(
101+
contentRange.start.translate({
102+
characterDelta: delimiter.length,
103+
}),
104+
contentRange.end.translate({
105+
characterDelta: delimiter.length,
106+
})
107+
)
108+
: contentRange,
109+
};
110+
}
111+
});
112+
113+
await editor.edit((editBuilder) => {
114+
edits.forEach((edit) => {
115+
editBuilder.replace(edit.insertPosition, edit.text);
116+
});
117+
});
118+
119+
editor.selections = edits.map((edit) => edit.selection);
120+
121+
const thatMarkRanges = edits.map((edit) => edit.thatMarkRange);
122+
123+
return createThatMark(targets, thatMarkRanges);
124+
}
125+
126+
async runCommand(targets: Target[], commands: string[]) {
127+
if (new Set(commands).size > 1) {
128+
throw new Error("Can't run multiple different commands at once");
129+
}
130+
if (this.isBefore) {
50131
await this.graph.actions.setSelectionBefore.run([targets]);
51132
} else {
52133
await this.graph.actions.setSelectionAfter.run([targets]);
53134
}
54-
55-
const command = this.getCommand(targets[0]);
56-
await commands.executeCommand(command);
57-
58-
return {
59-
thatMark: targets.map((target) => ({
60-
selection: target.editor.selection,
61-
editor: target.editor,
62-
})),
63-
};
135+
await vscommands.executeCommand(commands[0]);
136+
return createThatMark(targets);
64137
}
65138
}
66139

src/processTargets/modifiers/scopeTypeStages/ContainingSyntaxScopeStage.ts

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ export default class implements ModifierStage {
4646
return scopeNodes.map(
4747
(scope) =>
4848
new ScopeTypeTarget({
49-
delimiter: "\n",
5049
...selectionWithEditorWithContextToTarget(scope),
5150
scopeType: this.modifier.scopeType,
5251
isReversed: target.isReversed,

src/processTargets/targets/BaseTarget.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import {
66
Position,
77
RemovalRange,
88
TargetParameters,
9+
EditNewLineContext,
910
} from "../../typings/target.types";
1011

1112
export default class BaseTarget implements Target {
1213
editor: TextEditor;
1314
isReversed: boolean;
15+
contentRange: Range;
16+
delimiter: string;
1417
scopeType?: ScopeType;
1518
position?: Position;
16-
delimiter?: string;
17-
contentRange: Range;
1819
removalRange?: Range;
1920
interiorRange?: Range;
2021
boundary?: [Range, Range];
@@ -25,10 +26,10 @@ export default class BaseTarget implements Target {
2526
constructor(parameters: TargetParameters) {
2627
this.editor = parameters.editor;
2728
this.isReversed = parameters.isReversed;
29+
this.contentRange = parameters.contentRange;
30+
this.delimiter = parameters.delimiter ?? " ";
2831
this.scopeType = parameters.scopeType;
2932
this.position = parameters.position;
30-
this.delimiter = parameters.delimiter;
31-
this.contentRange = parameters.contentRange;
3233
this.removalRange = parameters.removalRange;
3334
this.interiorRange = parameters.interiorRange;
3435
this.boundary = parameters.boundary;
@@ -48,13 +49,11 @@ export default class BaseTarget implements Target {
4849

4950
/** Possibly add delimiter for positions before/after */
5051
maybeAddDelimiter(text: string): string {
51-
if (this.delimiter != null) {
52-
if (this.position === "before") {
53-
return text + this.delimiter;
54-
}
55-
if (this.position === "after") {
56-
return this.delimiter + text;
57-
}
52+
if (this.position === "before") {
53+
return text + this.delimiter;
54+
}
55+
if (this.position === "after") {
56+
return this.delimiter + text;
5857
}
5958
return text;
6059
}
@@ -122,4 +121,10 @@ export default class BaseTarget implements Target {
122121
}
123122
return this.getRemovalContentHighlightRange();
124123
}
124+
125+
getEditNewLineContext(_isBefore: boolean): EditNewLineContext {
126+
return {
127+
delimiter: "\n",
128+
};
129+
}
125130
}

src/processTargets/targets/DocumentTarget.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Range, TextEditor } from "vscode";
2+
import { EditNewLineContext, ScopeType } from "../../typings/target.types";
23
import BaseTarget from "./BaseTarget";
34

45
interface DocumentTargetParameters {
@@ -8,6 +9,10 @@ interface DocumentTargetParameters {
89
}
910

1011
export default class DocumentTarget extends BaseTarget {
12+
scopeType: ScopeType;
13+
delimiter: string;
14+
isLine: boolean;
15+
1116
constructor(parameters: DocumentTargetParameters) {
1217
super(parameters);
1318
this.scopeType = "document";

src/processTargets/targets/LineTarget.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Range, TextEditor } from "vscode";
2-
import { RemovalRange } from "../../typings/target.types";
2+
import { RemovalRange, ScopeType } from "../../typings/target.types";
33
import { parseRemovalRange } from "../../util/targetUtils";
44
import BaseTarget from "./BaseTarget";
55

@@ -12,6 +12,10 @@ interface LineTargetParameters {
1212
}
1313

1414
export default class LineTarget extends BaseTarget {
15+
scopeType: ScopeType;
16+
delimiter: string;
17+
isLine: boolean;
18+
1519
constructor(parameters: LineTargetParameters) {
1620
super(parameters);
1721
this.scopeType = "line";

src/processTargets/targets/NotebookCellTarget.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Range, TextEditor } from "vscode";
2+
import { EditNewLineContext } from "../../typings/target.types";
3+
import { getNotebookFromCellDocument } from "../../util/notebook";
24
import BaseTarget from "./BaseTarget";
35

46
interface NotebookCellTargetParameters {
@@ -13,4 +15,21 @@ export default class NotebookCellTarget extends BaseTarget {
1315
this.scopeType = "notebookCell";
1416
this.delimiter = "\n";
1517
}
18+
19+
getEditNewLineContext(isBefore: boolean): EditNewLineContext {
20+
if (this.isNotebookEditor(this.editor)) {
21+
return {
22+
command: isBefore
23+
? "notebook.cell.insertCodeCellAbove"
24+
: "notebook.cell.insertCodeCellBelow",
25+
};
26+
}
27+
return {
28+
command: isBefore ? "jupyter.insertCellAbove" : "jupyter.insertCellBelow",
29+
};
30+
}
31+
32+
private isNotebookEditor(editor: TextEditor) {
33+
return getNotebookFromCellDocument(editor.document) != null;
34+
}
1635
}

src/processTargets/targets/ParagraphTarget.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Range, TextEditor } from "vscode";
2-
import { RemovalRange } from "../../typings/target.types";
2+
import {
3+
EditNewLineContext,
4+
RemovalRange,
5+
ScopeType,
6+
} from "../../typings/target.types";
37
import { parseRemovalRange } from "../../util/targetUtils";
48
import BaseTarget from "./BaseTarget";
59

@@ -12,6 +16,10 @@ interface ParagraphTargetParameters {
1216
}
1317

1418
export default class ParagraphTarget extends BaseTarget {
19+
scopeType: ScopeType;
20+
delimiter: string;
21+
isLine: boolean;
22+
1523
constructor(parameters: ParagraphTargetParameters) {
1624
super(parameters);
1725
this.scopeType = "paragraph";
@@ -67,4 +75,10 @@ export default class ParagraphTarget extends BaseTarget {
6775
? removalRange.union(delimiterRange)
6876
: removalRange;
6977
}
78+
79+
getEditNewLineContext(_isBefore: boolean): EditNewLineContext {
80+
return {
81+
delimiter: this.delimiter,
82+
};
83+
}
7084
}
+31-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { ScopeType, TargetParameters } from "../../typings/target.types";
1+
import {
2+
EditNewLineContext,
3+
ScopeType,
4+
TargetParameters,
5+
} from "../../typings/target.types";
26
import BaseTarget from "./BaseTarget";
37

48
export interface ScopeTypeTargetParameters extends TargetParameters {
59
scopeType: ScopeType;
6-
delimiter: string;
710
}
811

912
export default class ScopeTypeTarget extends BaseTarget {
@@ -13,6 +16,31 @@ export default class ScopeTypeTarget extends BaseTarget {
1316
constructor(parameters: ScopeTypeTargetParameters) {
1417
super(parameters);
1518
this.scopeType = parameters.scopeType;
16-
this.delimiter = parameters.delimiter;
19+
this.delimiter =
20+
parameters.delimiter ?? this.getDelimiter(parameters.scopeType);
21+
}
22+
23+
private getDelimiter(scopeType: ScopeType): string {
24+
switch (scopeType) {
25+
case "namedFunction":
26+
case "anonymousFunction":
27+
case "statement":
28+
case "ifStatement":
29+
return "\n";
30+
case "class":
31+
return "\n\n";
32+
default:
33+
return " ";
34+
}
35+
}
36+
37+
getEditNewLineContext(isBefore: boolean): EditNewLineContext {
38+
// This is the default and should implement the default version whatever that is.
39+
if (this.delimiter === "\n") {
40+
return super.getEditNewLineContext(isBefore);
41+
}
42+
return {
43+
delimiter: this.delimiter,
44+
};
1745
}
1846
}

0 commit comments

Comments
 (0)