diff --git a/src/CommandAction.ts b/src/CommandAction.ts index cd6f3a1059..63e8ec6067 100644 --- a/src/CommandAction.ts +++ b/src/CommandAction.ts @@ -12,12 +12,19 @@ import { runOnTargetsForEachEditor } from "./targetUtils"; import { focusEditor } from "./setSelectionsAndFocusEditor"; import { flatten } from "lodash"; import { callFunctionAndUpdateSelections } from "./updateSelections"; +import { ensureSingleEditor } from "./targetUtils"; export default class CommandAction implements Action { targetPreferences: ActionPreferences[] = [{ insideOutsideType: "inside" }]; + private ensureSingleEditor: boolean; - constructor(private graph: Graph, private command: string) { + constructor( + private graph: Graph, + private command: string, + { ensureSingleEditor = false } = {} + ) { this.run = this.run.bind(this); + this.ensureSingleEditor = ensureSingleEditor; } private async runCommandAndUpdateSelections(targets: TypedSelection[]) { @@ -57,14 +64,20 @@ export default class CommandAction implements Action { ); } - async run([targets]: [ - TypedSelection[], - TypedSelection[] - ]): Promise { - await displayPendingEditDecorations( - targets, - this.graph.editStyles.referenced - ); + async run( + [targets]: [TypedSelection[]], + { showDecorations = true } = {} + ): Promise { + if (showDecorations) { + await displayPendingEditDecorations( + targets, + this.graph.editStyles.referenced + ); + } + + if (this.ensureSingleEditor) { + ensureSingleEditor(targets); + } const originalEditor = window.activeTextEditor; diff --git a/src/actions/CutCopyPaste.ts b/src/actions/CutCopyPaste.ts new file mode 100644 index 0000000000..db093b097e --- /dev/null +++ b/src/actions/CutCopyPaste.ts @@ -0,0 +1,70 @@ +import { + Action, + ActionPreferences, + ActionReturnValue, + Graph, + TypedSelection, +} from "../Types"; +import { performInsideOutsideAdjustment } from "../performInsideOutsideAdjustment"; +import CommandAction from "../CommandAction"; +import displayPendingEditDecorations from "../editDisplayUtils"; +import { getOutsideOverflow } from "../targetUtils"; +import { zip } from "lodash"; + +export class Cut implements Action { + targetPreferences: ActionPreferences[] = [{ insideOutsideType: null }]; + + constructor(private graph: Graph) { + this.run = this.run.bind(this); + } + + async run([targets]: [TypedSelection[]]): Promise { + const insideTargets = targets.map((target) => + performInsideOutsideAdjustment(target, "inside") + ); + const outsideTargets = targets.map((target) => + performInsideOutsideAdjustment(target, "outside") + ); + const outsideTargetDecorations = zip(insideTargets, outsideTargets).flatMap( + ([inside, outside]) => getOutsideOverflow(inside!, outside!) + ); + const options = { showDecorations: false }; + + await Promise.all([ + displayPendingEditDecorations( + insideTargets, + this.graph.editStyles.referenced + ), + displayPendingEditDecorations( + outsideTargetDecorations, + this.graph.editStyles.pendingDelete + ), + ]); + + await this.graph.actions.copy.run([insideTargets], options); + + const { thatMark } = await this.graph.actions.delete.run( + [outsideTargets], + options + ); + + return { + returnValue: null, + thatMark, + }; + } +} + +const OPTIONS = { ensureSingleEditor: true }; + +export class Copy extends CommandAction { + constructor(graph: Graph) { + super(graph, "editor.action.clipboardCopyAction", OPTIONS); + } +} + +export class Paste extends CommandAction { + constructor(graph: Graph) { + super(graph, "editor.action.clipboardPasteAction", OPTIONS); + } +} diff --git a/src/actions/Paste.ts b/src/actions/Paste.ts deleted file mode 100644 index 3329c9eb89..0000000000 --- a/src/actions/Paste.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../Types"; -import displayPendingEditDecorations from "../editDisplayUtils"; -import { env } from "vscode"; -import { runForEachEditor } from "../targetUtils"; -import { flatten } from "lodash"; -import update from "immutability-helper"; -import { performEditsAndUpdateSelections } from "../updateSelections"; - -export default class Paste implements Action { - targetPreferences: ActionPreferences[] = [{ insideOutsideType: "inside" }]; - - constructor(private graph: Graph) { - this.run = this.run.bind(this); - } - - async run([targets]: [TypedSelection[]]): Promise { - await displayPendingEditDecorations( - targets, - this.graph.editStyles.pendingModification0, - ); - - const text = await env.clipboard.readText(); - - if (text.length === 0) { - throw new Error("Can't paste empty clipboard"); - } - - const lines = text.trim().split("\n"); - - const getText = - targets.length === lines.length - ? // Paste each line on each target - (index: number) => lines[index] - : // Paste entire clipboard on each target - () => text; - - const edits = targets.map((target, index) => ({ - editor: target.selection.editor, - range: target.selection.selection, - text: getText(index), - originalSelection: target, - })); - - const thatMark = flatten( - await runForEachEditor( - edits, - (edit) => edit.editor, - async (editor, edits) => { - const [updatedSelections] = await performEditsAndUpdateSelections( - editor, - edits, - [targets.map((target) => target.selection.selection)] - ); - - return edits.map((edit, index) => { - const selection = updatedSelections[index]; - return { - editor, - selection, - typedSelection: update(edit.originalSelection, { - selection: { - selection: { $set: selection }, - }, - }), - }; - }); - } - ) - ); - - await displayPendingEditDecorations( - thatMark.map(({ typedSelection }) => typedSelection), - this.graph.editStyles.pendingModification0, - ); - - return { returnValue: null, thatMark }; - } -} diff --git a/src/actions/copy.ts b/src/actions/copy.ts deleted file mode 100644 index e082c4bfe2..0000000000 --- a/src/actions/copy.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../Types"; -import { env } from "vscode"; - -export default class Copy implements Action { - targetPreferences: ActionPreferences[] = [{ insideOutsideType: "inside" }]; - - constructor(private graph: Graph) { - this.run = this.run.bind(this); - } - - async run([targets]: [TypedSelection[]]): Promise { - const { returnValue, thatMark } = await this.graph.actions.getText.run([ - targets, - ]); - const text = returnValue.join("\n"); - - await env.clipboard.writeText(text); - - return { returnValue: null, thatMark }; - } -} diff --git a/src/actions/cut.ts b/src/actions/cut.ts deleted file mode 100644 index c3b94d0d75..0000000000 --- a/src/actions/cut.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - Action, - ActionPreferences, - ActionReturnValue, - Graph, - TypedSelection, -} from "../Types"; -import { - performInsideAdjustment, - performOutsideAdjustment, -} from "../performInsideOutsideAdjustment"; - -export default class Cut implements Action { - targetPreferences: ActionPreferences[] = [{ insideOutsideType: null }]; - - constructor(private graph: Graph) { - this.run = this.run.bind(this); - } - - async run([targets]: [TypedSelection[]]): Promise { - await this.graph.actions.copy.run([targets.map(performInsideAdjustment)]); - - const { thatMark } = await this.graph.actions.delete.run([ - targets.map(performOutsideAdjustment), - ]); - - return { - returnValue: null, - thatMark, - }; - } -} diff --git a/src/actions/delete.ts b/src/actions/delete.ts index fab12244b3..d675799a45 100644 --- a/src/actions/delete.ts +++ b/src/actions/delete.ts @@ -17,11 +17,16 @@ export default class Delete implements Action { this.run = this.run.bind(this); } - async run([targets]: [TypedSelection[]]): Promise { - await displayPendingEditDecorations( - targets, - this.graph.editStyles.pendingDelete, - ); + async run( + [targets]: [TypedSelection[]], + { showDecorations = true } = {} + ): Promise { + if (showDecorations) { + await displayPendingEditDecorations( + targets, + this.graph.editStyles.pendingDelete + ); + } const thatMark = flatten( await runOnTargetsForEachEditor(targets, async (editor, targets) => { diff --git a/src/actions/index.ts b/src/actions/index.ts index 37c7652da6..670711f639 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,7 +1,6 @@ import { ActionRecord, Graph } from "../Types"; import Clear from "./clear"; -import Copy from "./copy"; -import Cut from "./cut"; +import { Cut, Copy, Paste } from "./CutCopyPaste"; import Delete from "./delete"; import ExtractVariable from "./extractVariable"; import { Fold, Unfold } from "./fold"; @@ -15,7 +14,6 @@ import Wrap from "./wrap"; import { ScrollToTop, ScrollToCenter, ScrollToBottom } from "./Scroll"; import { IndentLines, OutdentLines } from "./Indent"; import { CommentLines } from "./Comment"; -import Paste from "./Paste"; import { Bring, Move, Swap } from "./BringMoveSwap"; import { InsertEmptyLineAbove, diff --git a/src/targetUtils.ts b/src/targetUtils.ts index 0148f95ea4..46c76f0c42 100644 --- a/src/targetUtils.ts +++ b/src/targetUtils.ts @@ -1,6 +1,7 @@ -import { TextEditor } from "vscode"; +import { TextEditor, Selection, Position } from "vscode"; import { groupBy } from "./itertools"; import { TypedSelection } from "./Types"; +import update from "immutability-helper"; export function ensureSingleEditor(targets: TypedSelection[]) { if (targets.length === 0) { @@ -42,3 +43,47 @@ export async function runOnTargetsForEachEditor( ): Promise { return runForEachEditor(targets, (target) => target.selection.editor, func); } + +/** Get the possible leading and trailing overflow ranges of the outside target compared to the inside target */ +export function getOutsideOverflow( + insideTarget: TypedSelection, + outsideTarget: TypedSelection +): TypedSelection[] { + const { start: insideStart, end: insideEnd } = + insideTarget.selection.selection; + const { start: outsideStart, end: outsideEnd } = + outsideTarget.selection.selection; + const result = []; + if (outsideStart.isBefore(insideStart)) { + result.push( + createTypeSelection( + insideTarget.selection.editor, + outsideStart, + insideStart + ) + ); + } + if (outsideEnd.isAfter(insideEnd)) { + result.push( + createTypeSelection(insideTarget.selection.editor, insideEnd, outsideEnd) + ); + } + return result; +} + +function createTypeSelection( + editor: TextEditor, + start: Position, + end: Position +): TypedSelection { + return { + selection: { + editor, + selection: new Selection(start, end), + }, + selectionType: "token", + selectionContext: {}, + insideOutsideType: "inside", + position: "contents", + }; +}