Skip to content

Support delimiter based edit newline #684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 118 additions & 45 deletions src/actions/EditNewLine.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,139 @@
import { commands, Range, TextEditor } from "vscode";
import {
commands as vscommands,
Position,
Range,
Selection,
TextEditor,
} from "vscode";
import { Target } from "../typings/target.types";
import { Graph } from "../typings/Types";
import { getNotebookFromCellDocument } from "../util/notebook";
import { createThatMark, ensureSingleEditor } from "../util/targetUtils";
import { Action, ActionReturnValue } from "./actions.types";

class EditNewLine implements Action {
constructor(private graph: Graph, private isAbove: boolean) {
constructor(private graph: Graph, private isBefore: boolean) {
this.run = this.run.bind(this);
}

private correctForParagraph(targets: Target[]) {
targets.forEach((target) => {
let { start, end } = target.contentRange;
if (target.scopeType === "paragraph") {
if (this.isAbove && target.leadingDelimiter != null) {
start = start.translate({ lineDelta: -1 });
} else if (!this.isAbove && target.trailingDelimiter != null) {
end = end.translate({ lineDelta: 1 });
}
target.contentRange = new Range(start, end);
}
});
}
async run([targets]: [Target[]]): Promise<ActionReturnValue> {
const editor = ensureSingleEditor(targets);

private isNotebookEditor(editor: TextEditor) {
return getNotebookFromCellDocument(editor.document) != null;
}
const targetsWithContext = targets.map((target) => ({
target,
context: target.getEditNewLineContext(this.isBefore),
}));
const commandTargets = targetsWithContext.filter(
({ context }) => !!(<any>context).command
);
const delimiterTargets = targetsWithContext.filter(
({ context }) => !!(<any>context).delimiter
);

private getCommand(target: Target) {
if (target.scopeType === "notebookCell") {
if (this.isNotebookEditor(target.editor)) {
return this.isAbove
? "notebook.cell.insertCodeCellAbove"
: "notebook.cell.insertCodeCellBelow";
}
return this.isAbove
? "jupyter.insertCellAbove"
: "jupyter.insertCellBelow";
if (commandTargets.length > 0 && delimiterTargets.length > 0) {
throw new Error("Can't insert edit using command and delimiter at once");
}

if (commandTargets.length > 0) {
const commands = commandTargets.map(
({ context }) => (<any>context).command
);
return {
thatMark: await this.runCommand(targets, commands),
};
}
return this.isAbove
? "editor.action.insertLineBefore"
: "editor.action.insertLineAfter";

return {
thatMark: await this.runDelimiter(targets, editor),
};
}

async run([targets]: [Target[]]): Promise<ActionReturnValue> {
this.correctForParagraph(targets);
async runDelimiter(targets: Target[], editor: TextEditor) {
const edits = targets.map((target) => {
const { contentRange } = target;
const context = target.getEditNewLineContext(this.isBefore);
const delimiter = (<any>context).delimiter as string;

if (this.isAbove) {
// Delimiter is one or more new lines. Handle as lines.
if (delimiter.includes("\n")) {
const lineNumber = this.isBefore
? contentRange.start.line
: contentRange.end.line;
const line = editor.document.lineAt(lineNumber);
const characterIndex = line.isEmptyOrWhitespace
? contentRange.start.character
: line.firstNonWhitespaceCharacterIndex;
Comment on lines +62 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always just be same indentation as previous line, right? The built-in VSCode indentation code will automatically indent if, for example, you're on the guard of an if statement. I don't think we can recover this behaviour without running the VSCode command or doing something like #685

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Look in comment 2. We could just default to existing vscode command and only use delimiter for other than single line if you like.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would just override the context in the Line class, the way you've done for notebook cells

const padding = line.text.slice(0, characterIndex);
const positionSelection = new Position(
this.isBefore ? lineNumber : lineNumber + delimiter.length,
characterIndex
);
return {
contentRange,
text: this.isBefore ? padding + delimiter : delimiter + padding,
insertPosition: this.isBefore ? line.range.start : line.range.end,
selection: new Selection(positionSelection, positionSelection),
thatMarkRange: this.isBefore
? new Range(
contentRange.start.translate({
lineDelta: delimiter.length,
}),
contentRange.end.translate({
lineDelta: delimiter.length,
})
)
: contentRange,
};
}
// Delimiter is something else. Handle as inline.
else {
const positionSelection = this.isBefore
? contentRange.start
: contentRange.end.translate({
characterDelta: delimiter.length,
});
return {
contentRange,
text: delimiter,
insertPosition: this.isBefore ? contentRange.start : contentRange.end,
selection: new Selection(positionSelection, positionSelection),
thatMarkRange: this.isBefore
? new Range(
contentRange.start.translate({
characterDelta: delimiter.length,
}),
contentRange.end.translate({
characterDelta: delimiter.length,
})
)
: contentRange,
};
}
});

await editor.edit((editBuilder) => {
edits.forEach((edit) => {
editBuilder.replace(edit.insertPosition, edit.text);
});
});

editor.selections = edits.map((edit) => edit.selection);

const thatMarkRanges = edits.map((edit) => edit.thatMarkRange);

return createThatMark(targets, thatMarkRanges);
}

async runCommand(targets: Target[], commands: string[]) {
if (new Set(commands).size > 1) {
throw new Error("Can't run multiple different commands at once");
}
if (this.isBefore) {
await this.graph.actions.setSelectionBefore.run([targets]);
} else {
await this.graph.actions.setSelectionAfter.run([targets]);
}

const command = this.getCommand(targets[0]);
await commands.executeCommand(command);

return {
thatMark: targets.map((target) => ({
selection: target.editor.selection,
editor: target.editor,
})),
};
await vscommands.executeCommand(commands[0]);
return createThatMark(targets);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export default class implements ModifierStage {
return scopeNodes.map(
(scope) =>
new ScopeTypeTarget({
delimiter: "\n",
...selectionWithEditorWithContextToTarget(scope),
scopeType: this.modifier.scopeType,
isReversed: target.isReversed,
Expand Down
27 changes: 16 additions & 11 deletions src/processTargets/targets/BaseTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import {
Position,
RemovalRange,
TargetParameters,
EditNewLineContext,
} from "../../typings/target.types";

export default class BaseTarget implements Target {
editor: TextEditor;
isReversed: boolean;
contentRange: Range;
delimiter: string;
scopeType?: ScopeType;
position?: Position;
delimiter?: string;
contentRange: Range;
removalRange?: Range;
interiorRange?: Range;
boundary?: [Range, Range];
Expand All @@ -25,10 +26,10 @@ export default class BaseTarget implements Target {
constructor(parameters: TargetParameters) {
this.editor = parameters.editor;
this.isReversed = parameters.isReversed;
this.contentRange = parameters.contentRange;
this.delimiter = parameters.delimiter ?? " ";
this.scopeType = parameters.scopeType;
this.position = parameters.position;
this.delimiter = parameters.delimiter;
this.contentRange = parameters.contentRange;
this.removalRange = parameters.removalRange;
this.interiorRange = parameters.interiorRange;
this.boundary = parameters.boundary;
Expand All @@ -48,13 +49,11 @@ export default class BaseTarget implements Target {

/** Possibly add delimiter for positions before/after */
maybeAddDelimiter(text: string): string {
if (this.delimiter != null) {
if (this.position === "before") {
return text + this.delimiter;
}
if (this.position === "after") {
return this.delimiter + text;
}
if (this.position === "before") {
return text + this.delimiter;
}
if (this.position === "after") {
return this.delimiter + text;
}
return text;
}
Expand Down Expand Up @@ -122,4 +121,10 @@ export default class BaseTarget implements Target {
}
return this.getRemovalContentHighlightRange();
}

getEditNewLineContext(_isBefore: boolean): EditNewLineContext {
return {
delimiter: "\n",
};
}
}
5 changes: 5 additions & 0 deletions src/processTargets/targets/DocumentTarget.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Range, TextEditor } from "vscode";
import { EditNewLineContext, ScopeType } from "../../typings/target.types";
import BaseTarget from "./BaseTarget";

interface DocumentTargetParameters {
Expand All @@ -8,6 +9,10 @@ interface DocumentTargetParameters {
}

export default class DocumentTarget extends BaseTarget {
scopeType: ScopeType;
delimiter: string;
isLine: boolean;

constructor(parameters: DocumentTargetParameters) {
super(parameters);
this.scopeType = "document";
Expand Down
6 changes: 5 additions & 1 deletion src/processTargets/targets/LineTarget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Range, TextEditor } from "vscode";
import { RemovalRange } from "../../typings/target.types";
import { RemovalRange, ScopeType } from "../../typings/target.types";
import { parseRemovalRange } from "../../util/targetUtils";
import BaseTarget from "./BaseTarget";

Expand All @@ -12,6 +12,10 @@ interface LineTargetParameters {
}

export default class LineTarget extends BaseTarget {
scopeType: ScopeType;
delimiter: string;
isLine: boolean;

constructor(parameters: LineTargetParameters) {
super(parameters);
this.scopeType = "line";
Expand Down
19 changes: 19 additions & 0 deletions src/processTargets/targets/NotebookCellTarget.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Range, TextEditor } from "vscode";
import { EditNewLineContext } from "../../typings/target.types";
import { getNotebookFromCellDocument } from "../../util/notebook";
import BaseTarget from "./BaseTarget";

interface NotebookCellTargetParameters {
Expand All @@ -13,4 +15,21 @@ export default class NotebookCellTarget extends BaseTarget {
this.scopeType = "notebookCell";
this.delimiter = "\n";
}

getEditNewLineContext(isBefore: boolean): EditNewLineContext {
if (this.isNotebookEditor(this.editor)) {
return {
command: isBefore
? "notebook.cell.insertCodeCellAbove"
: "notebook.cell.insertCodeCellBelow",
};
}
return {
command: isBefore ? "jupyter.insertCellAbove" : "jupyter.insertCellBelow",
};
}

private isNotebookEditor(editor: TextEditor) {
return getNotebookFromCellDocument(editor.document) != null;
}
}
16 changes: 15 additions & 1 deletion src/processTargets/targets/ParagraphTarget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Range, TextEditor } from "vscode";
import { RemovalRange } from "../../typings/target.types";
import {
EditNewLineContext,
RemovalRange,
ScopeType,
} from "../../typings/target.types";
import { parseRemovalRange } from "../../util/targetUtils";
import BaseTarget from "./BaseTarget";

Expand All @@ -12,6 +16,10 @@ interface ParagraphTargetParameters {
}

export default class ParagraphTarget extends BaseTarget {
scopeType: ScopeType;
delimiter: string;
isLine: boolean;

constructor(parameters: ParagraphTargetParameters) {
super(parameters);
this.scopeType = "paragraph";
Expand Down Expand Up @@ -67,4 +75,10 @@ export default class ParagraphTarget extends BaseTarget {
? removalRange.union(delimiterRange)
: removalRange;
}

getEditNewLineContext(_isBefore: boolean): EditNewLineContext {
return {
delimiter: this.delimiter,
};
}
}
34 changes: 31 additions & 3 deletions src/processTargets/targets/ScopeTypeTarget.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ScopeType, TargetParameters } from "../../typings/target.types";
import {
EditNewLineContext,
ScopeType,
TargetParameters,
} from "../../typings/target.types";
import BaseTarget from "./BaseTarget";

export interface ScopeTypeTargetParameters extends TargetParameters {
scopeType: ScopeType;
delimiter: string;
}

export default class ScopeTypeTarget extends BaseTarget {
Expand All @@ -13,6 +16,31 @@ export default class ScopeTypeTarget extends BaseTarget {
constructor(parameters: ScopeTypeTargetParameters) {
super(parameters);
this.scopeType = parameters.scopeType;
this.delimiter = parameters.delimiter;
this.delimiter =
parameters.delimiter ?? this.getDelimiter(parameters.scopeType);
}

private getDelimiter(scopeType: ScopeType): string {
switch (scopeType) {
case "namedFunction":
case "anonymousFunction":
case "statement":
case "ifStatement":
return "\n";
case "class":
return "\n\n";
default:
return " ";
}
}

getEditNewLineContext(isBefore: boolean): EditNewLineContext {
// This is the default and should implement the default version whatever that is.
if (this.delimiter === "\n") {
return super.getEditNewLineContext(isBefore);
}
return {
delimiter: this.delimiter,
};
}
}
Loading