From 7a225e995039f8255f4e608b11e7e1e51e166040 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Sun, 14 Jan 2024 19:10:19 +0900 Subject: [PATCH 01/16] Fix the kill commands to kill and yank the rect-marked regions --- src/commands/helpers/eol.ts | 12 ++++ src/commands/kill.ts | 26 +++++---- src/commands/paredit.ts | 9 ++- src/commands/rectangle.ts | 12 +--- src/kill-yank/index.ts | 56 +++++++++++++++---- src/kill-yank/kill-ring-entity/editor-text.ts | 11 +++- src/rectangle.ts | 6 ++ 7 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 src/commands/helpers/eol.ts diff --git a/src/commands/helpers/eol.ts b/src/commands/helpers/eol.ts new file mode 100644 index 0000000000..81890c1201 --- /dev/null +++ b/src/commands/helpers/eol.ts @@ -0,0 +1,12 @@ +import * as vscode from "vscode"; + +export function getEolChar(eol: vscode.EndOfLine): string | undefined { + switch (eol) { + case vscode.EndOfLine.CRLF: + return "\r\n"; + case vscode.EndOfLine.LF: + return "\n"; + default: + return "\n"; + } +} diff --git a/src/commands/kill.ts b/src/commands/kill.ts index e08c7b601f..f7899c4e6b 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -59,7 +59,7 @@ export class KillWord extends KillYankCommand { const killRanges = textEditor.selections .map((selection) => findNextKillWordRange(textEditor.document, selection.active, repeat)) .filter((maybeRange: T | undefined): maybeRange is T => maybeRange != null); - await this.killYanker.kill(killRanges); + await this.killYanker.kill(killRanges, false); revealPrimaryActive(textEditor); } } @@ -96,7 +96,7 @@ export class BackwardKillWord extends KillYankCommand { const killRanges = textEditor.selections .map((selection) => findPreviousKillWordRange(textEditor.document, selection.active, repeat)) .filter((maybeRange: T | undefined): maybeRange is T => maybeRange != null); - await this.killYanker.kill(killRanges, AppendDirection.Backward); + await this.killYanker.kill(killRanges, false, AppendDirection.Backward); revealPrimaryActive(textEditor); } } @@ -132,7 +132,7 @@ export class KillLine extends KillYankCommand { return new Range(cursor, lineEnd); } }); - return this.killYanker.kill(ranges).then(() => revealPrimaryActive(textEditor)); + return this.killYanker.kill(ranges, false).then(() => revealPrimaryActive(textEditor)); } } @@ -148,7 +148,7 @@ export class KillWholeLine extends KillYankCommand { // From the beginning of the line to the beginning of the next line new Range(new Position(selection.active.line, 0), new Position(selection.active.line + 1, 0)), ); - return this.killYanker.kill(ranges).then(() => revealPrimaryActive(textEditor)); + return this.killYanker.kill(ranges, false).then(() => revealPrimaryActive(textEditor)); } } @@ -156,23 +156,25 @@ export class KillRegion extends KillYankCommand { public readonly id = "killRegion"; public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { - const selectionsAfterRectDisabled = - this.emacsController.inRectMarkMode && - this.emacsController.nativeSelections.map((selection) => { + if (this.emacsController.inRectMarkMode) { + const selectionsAfterRectDisabled = this.emacsController.nativeSelections.map((selection) => { const newLine = selection.active.line; const newChar = Math.min(selection.active.character, selection.anchor.character); return new vscode.Selection(newLine, newChar, newLine, newChar); }); - const ranges = getNonEmptySelections(textEditor); - await this.killYanker.kill(ranges); - if (selectionsAfterRectDisabled) { + const ranges = this.emacsController.nativeSelections.map((s) => new Range(s.start, s.end)); + await this.killYanker.kill(ranges, true); + textEditor.selections = selectionsAfterRectDisabled; + } else { + const ranges = getNonEmptySelections(textEditor); + await this.killYanker.kill(ranges, false); } + revealPrimaryActive(textEditor); this.emacsController.exitMarkMode(); - this.killYanker.cancelKillAppend(); } } @@ -182,7 +184,7 @@ export class CopyRegion extends KillYankCommand { public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { const ranges = getNonEmptySelections(textEditor); - await this.killYanker.copy(ranges); + await this.killYanker.copy(ranges, this.emacsController.inRectMarkMode); this.emacsController.exitMarkMode(); this.killYanker.cancelKillAppend(); makeSelectionsEmpty(textEditor); diff --git a/src/commands/paredit.ts b/src/commands/paredit.ts index 57e4b207a0..c50e302787 100644 --- a/src/commands/paredit.ts +++ b/src/commands/paredit.ts @@ -137,7 +137,7 @@ export class KillSexp extends KillYankCommand { return new Range(selection.anchor, newActivePosition); }); - await this.killYanker.kill(killRanges); + await this.killYanker.kill(killRanges, false); revealPrimaryActive(textEditor); } @@ -160,7 +160,7 @@ export class BackwardKillSexp extends KillYankCommand { return new Range(selection.anchor, newActivePosition); }); - await this.killYanker.kill(killRanges, AppendDirection.Backward); + await this.killYanker.kill(killRanges, false, AppendDirection.Backward); revealPrimaryActive(textEditor); } @@ -204,7 +204,10 @@ export class PareditKill extends KillYankCommand { return new Range(selection.anchor, newActivePosition); }); - await this.killYanker.kill(killRanges.filter((range) => !range.isEmpty)); + await this.killYanker.kill( + killRanges.filter((range) => !range.isEmpty), + false, + ); revealPrimaryActive(textEditor); } diff --git a/src/commands/rectangle.ts b/src/commands/rectangle.ts index 1b7d382e40..2107d67f8a 100644 --- a/src/commands/rectangle.ts +++ b/src/commands/rectangle.ts @@ -5,6 +5,7 @@ import { IEmacsController } from "../emulator"; import { getNonEmptySelections, makeSelectionsEmpty } from "./helpers/selection"; import { convertSelectionToRectSelections } from "../rectangle"; import { revealPrimaryActive } from "./helpers/reveal"; +import { getEolChar } from "./helpers/eol"; import { KillRing } from "../kill-yank/kill-ring"; import { Minibuffer } from "src/minibuffer"; @@ -126,17 +127,6 @@ export class KillRectangle extends EditRectangle { protected copy = true; } -const getEolChar = (eol: vscode.EndOfLine): string | undefined => { - switch (eol) { - case vscode.EndOfLine.CRLF: - return "\r\n"; - case vscode.EndOfLine.LF: - return "\n"; - default: - return "\n"; - } -}; - export class YankRectangle extends RectangleKillYankCommand { public readonly id = "yankRectangle"; diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index c50ce37ef3..dfe2055da0 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -7,6 +7,7 @@ import { KillRing, KillRingEntity } from "./kill-ring"; import { ClipboardTextKillRingEntity } from "./kill-ring-entity/clipboard-text"; import { AppendDirection, EditorTextKillRingEntity } from "./kill-ring-entity/editor-text"; import { logger } from "../logger"; +import { convertSelectionToRectSelections, getRectText } from "../rectangle"; export { AppendDirection }; @@ -74,14 +75,18 @@ export class KillYanker implements vscode.Disposable { } } - public async kill(ranges: Range[], appendDirection: AppendDirection = AppendDirection.Forward): Promise { + public async kill( + ranges: Range[], + rectMarkMode: boolean, + appendDirection: AppendDirection = AppendDirection.Forward, + ): Promise { if (!equalPositions(this.getCursorPositions(), this.prevKillPositions)) { this.isAppending = false; } - await this.copy(ranges, this.isAppending, appendDirection); + await this.copy(ranges, rectMarkMode, this.isAppending, appendDirection); - await this.delete(ranges); + await this.delete(ranges, rectMarkMode); this.isAppending = true; this.prevKillPositions = this.getCursorPositions(); @@ -89,21 +94,30 @@ export class KillYanker implements vscode.Disposable { public async copy( ranges: Range[], + rectMarkMode: boolean, shouldAppend = false, appendDirection: AppendDirection = AppendDirection.Forward, ): Promise { const newKillEntity = new EditorTextKillRingEntity( ranges.map((range) => ({ range, - text: this.textEditor.document.getText(range), + text: rectMarkMode ? getRectText(this.textEditor.document, range) : this.textEditor.document.getText(range), + rectMode: rectMarkMode, })), ); if (this.killRing !== null) { const currentKill = this.killRing.getTop(); if (shouldAppend && currentKill instanceof EditorTextKillRingEntity) { - currentKill.append(newKillEntity, appendDirection); - await vscode.env.clipboard.writeText(currentKill.asString()); + try { + currentKill.append(newKillEntity, appendDirection); + await vscode.env.clipboard.writeText(currentKill.asString()); + return; + } catch { + this.killRing.push(newKillEntity); + await vscode.env.clipboard.writeText(newKillEntity.asString()); + return; + } } else { this.killRing.push(newKillEntity); await vscode.env.clipboard.writeText(newKillEntity.asString()); @@ -137,17 +151,31 @@ export class KillYanker implements vscode.Disposable { if (killRingEntity.type === "editor") { const selections = this.textEditor.selections; const regionTexts = killRingEntity.getRegionTextsList(); + const fillIndent = killRingEntity.hasRectModeText(); const shouldPasteSeparately = regionTexts.length > 1 && flattenedText.split("\n").length !== regionTexts.length; - if (shouldPasteSeparately && regionTexts.length === selections.length) { + const canPasteSeparately = regionTexts.length === selections.length; + const pasteSeparately = shouldPasteSeparately && canPasteSeparately; + const customPaste = pasteSeparately || fillIndent; + if (customPaste) { + // The normal `paste` command is not suitable in this case, so we use `edit` command instead. + if (!pasteSeparately) { + // `pasteSeparately` is false, so there is only one paste target selection. + this.textEditor.selections = [this.textEditor.selection]; + } const success = await this.textEditor.edit((editBuilder) => { selections.forEach((selection, i) => { + const indent = selection.start.character; if (!selection.isEmpty) { editBuilder.delete(selection); } // `regionTexts.length === selections.length` has already been checked, // so noUncheckedIndexedAccess rule can be skipped here. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editBuilder.insert(selection.start, regionTexts[i]!.getAppendedText()); + let text = regionTexts[i]!.getAppendedText(); + if (fillIndent) { + text = text.replace(/^/gm, " ".repeat(indent)).slice(indent); + } + editBuilder.insert(selection.start, text); }); }); if (!success) { @@ -214,18 +242,26 @@ export class KillYanker implements vscode.Disposable { this.prevYankPositions = this.textEditor.selections.map((selection) => selection.active); } - private async delete(ranges: vscode.Range[], maxTrials = 3): Promise { + private async delete(ranges: vscode.Range[], rectMode: boolean, maxTrials = 3): Promise { + const deleteRanges = rectMode + ? ranges.flatMap((range) => + convertSelectionToRectSelections(this.textEditor.document, new vscode.Selection(range.start, range.end)), + ) + : ranges; + let success = false; let trial = 0; while (!success && trial < maxTrials) { success = await this.textEditor.edit((editBuilder) => { - ranges.forEach((range) => { + deleteRanges.forEach((range) => { editBuilder.delete(range); }); }); trial++; } + // TODO: Set the selections to the deleted ranges in the case of rectMode, which is now done in the kill command. + // It is needed to append the following kills. return success; } diff --git a/src/kill-yank/kill-ring-entity/editor-text.ts b/src/kill-yank/kill-ring-entity/editor-text.ts index 22d828cb70..e5ef8dc0ce 100644 --- a/src/kill-yank/kill-ring-entity/editor-text.ts +++ b/src/kill-yank/kill-ring-entity/editor-text.ts @@ -9,6 +9,7 @@ export enum AppendDirection { interface IRegionText { text: string; range: Range; + rectMode: boolean; } class AppendedRegionTexts { @@ -41,6 +42,10 @@ class AppendedRegionTexts { public getLastRange(): Range { return this.regionTexts[this.regionTexts.length - 1]!.range; // eslint-disable-line @typescript-eslint/no-non-null-assertion } + + public hasRectModeText(): boolean { + return this.regionTexts.some((regionText) => regionText.rectMode); + } } export class EditorTextKillRingEntity implements IKillRingEntity { @@ -78,7 +83,7 @@ export class EditorTextKillRingEntity implements IKillRingEntity { sortedAppendedTexts.forEach((item, i) => { const prevItem = sortedAppendedTexts[i - 1]; if (prevItem && prevItem.range.start.line !== item.range.start.line) { - allText += "\n" + item.text; + allText += "\n" + item.text; // TODO: Use the appropriate EOL char. } else { allText += item.text; } @@ -104,4 +109,8 @@ export class EditorTextKillRingEntity implements IKillRingEntity { (appendedRegionTexts, i) => appendedRegionTexts.append(additional[i]!, appendDirection), ); } + + public hasRectModeText(): boolean { + return this.regionTextsList.some((regionTexts) => regionTexts.hasRectModeText()); + } } diff --git a/src/rectangle.ts b/src/rectangle.ts index f8d2e9ad16..824d62f98d 100644 --- a/src/rectangle.ts +++ b/src/rectangle.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { getEolChar } from "./commands/helpers/eol"; export function convertSelectionToRectSelections( document: vscode.TextDocument, @@ -25,3 +26,8 @@ export function convertSelectionToRectSelections( return rectSelections; } + +export function getRectText(document: vscode.TextDocument, range: vscode.Range): string { + const rectRanges = convertSelectionToRectSelections(document, new vscode.Selection(range.start, range.end)); + return rectRanges.map((rectRange) => document.getText(rectRange)).join(getEolChar(document.eol)); +} From a3b0e9ca4b7deaad71929533d56588426afa35d5 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Mon, 15 Jan 2024 19:09:24 +0900 Subject: [PATCH 02/16] Fix kill-line not to be executed when the cursor is at the end of buffer, and fix KillYanker.copy() not to append empty texts --- src/commands/kill.ts | 11 ++++++++++- src/kill-yank/index.ts | 15 ++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/commands/kill.ts b/src/commands/kill.ts index f7899c4e6b..cd0f923726 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -8,6 +8,7 @@ import { WordCharacterClassifier, getMapForWordSeparators } from "vs/editor/comm import { findNextWordEnd, findPreviousWordStart } from "./helpers/wordOperations"; import { revealPrimaryActive } from "./helpers/reveal"; import { getNonEmptySelections, makeSelectionsEmpty } from "./helpers/selection"; +import { MessageManager } from "../message"; function getWordSeparators(): WordCharacterClassifier { // Ref: https://github.com/VSCodeVim/Vim/blob/91ca71f8607458c0558f9aff61e230c6917d4b51/src/configuration/configuration.ts#L155 @@ -132,7 +133,15 @@ export class KillLine extends KillYankCommand { return new Range(cursor, lineEnd); } }); - return this.killYanker.kill(ranges, false).then(() => revealPrimaryActive(textEditor)); + + const endOfDoc = textEditor.document.lineAt(textEditor.document.lineCount - 1).range.end; + const nonEndRanges = ranges.filter((range) => !(range.isEmpty && range.end.isEqual(endOfDoc))); + if (nonEndRanges.length === 0) { + MessageManager.showMessage("End of buffer"); + return Promise.resolve(); + } + + return this.killYanker.kill(nonEndRanges, false).then(() => revealPrimaryActive(textEditor)); } } diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index dfe2055da0..1716168e04 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -80,6 +80,9 @@ export class KillYanker implements vscode.Disposable { rectMarkMode: boolean, appendDirection: AppendDirection = AppendDirection.Forward, ): Promise { + if (ranges.length === 0 || ranges.some((range) => range.isEmpty)) { + this.isAppending = false; + } if (!equalPositions(this.getCursorPositions(), this.prevKillPositions)) { this.isAppending = false; } @@ -109,15 +112,9 @@ export class KillYanker implements vscode.Disposable { if (this.killRing !== null) { const currentKill = this.killRing.getTop(); if (shouldAppend && currentKill instanceof EditorTextKillRingEntity) { - try { - currentKill.append(newKillEntity, appendDirection); - await vscode.env.clipboard.writeText(currentKill.asString()); - return; - } catch { - this.killRing.push(newKillEntity); - await vscode.env.clipboard.writeText(newKillEntity.asString()); - return; - } + currentKill.append(newKillEntity, appendDirection); + await vscode.env.clipboard.writeText(currentKill.asString()); + return; } else { this.killRing.push(newKillEntity); await vscode.env.clipboard.writeText(newKillEntity.asString()); From 9894d64be874db15445b02d35c3d9d452d122241 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Mon, 15 Jan 2024 19:14:05 +0900 Subject: [PATCH 03/16] Fix copyRegion --- src/commands/kill.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/commands/kill.ts b/src/commands/kill.ts index cd0f923726..e8c8a88f5f 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -192,8 +192,13 @@ export class CopyRegion extends KillYankCommand { public readonly id = "copyRegion"; public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { - const ranges = getNonEmptySelections(textEditor); - await this.killYanker.copy(ranges, this.emacsController.inRectMarkMode); + if (this.emacsController.inRectMarkMode) { + const ranges = this.emacsController.nativeSelections.map((s) => new Range(s.start, s.end)); + await this.killYanker.copy(ranges, true); + } else { + const ranges = getNonEmptySelections(textEditor); + await this.killYanker.copy(ranges, false); + } this.emacsController.exitMarkMode(); this.killYanker.cancelKillAppend(); makeSelectionsEmpty(textEditor); From 09a32a76950f2fa1ab9f0ed4fc982d92747774b6 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Mon, 15 Jan 2024 19:55:44 +0900 Subject: [PATCH 04/16] [WIP] Add tests for the kill commands in rect mode --- src/test/suite/rectangle-mark-mode.test.ts | 144 ++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/src/test/suite/rectangle-mark-mode.test.ts b/src/test/suite/rectangle-mark-mode.test.ts index 15be8ef0ea..9d6a3ae988 100644 --- a/src/test/suite/rectangle-mark-mode.test.ts +++ b/src/test/suite/rectangle-mark-mode.test.ts @@ -1,7 +1,8 @@ import * as vscode from "vscode"; import assert from "assert"; import { EmacsEmulator } from "../../emulator"; -import { cleanUpWorkspace, setEmptyCursors, setupWorkspace, delay, assertTextEqual } from "./utils"; +import { cleanUpWorkspace, setEmptyCursors, setupWorkspace, delay, assertTextEqual, clearTextEditor } from "./utils"; +import { KillRing } from "../../kill-yank/kill-ring"; suite("RectangleMarkMode", () => { let activeTextEditor: vscode.TextEditor; @@ -84,7 +85,55 @@ KLMNOPQRST`; test("killing and yanking in rectangle-mark-mode", async () => { setEmptyCursors(activeTextEditor, [1, 2]); - const emulator = new EmacsEmulator(activeTextEditor); + const killRing = new KillRing(3); + const emulator = new EmacsEmulator(activeTextEditor, killRing); + + emulator.rectangleMarkMode(); + + await emulator.runCommand("forwardChar"); + await emulator.runCommand("forwardChar"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + + assert.deepStrictEqual(activeTextEditor.selections, [ + new vscode.Selection(1, 2, 1, 4), + new vscode.Selection(2, 2, 2, 4), + new vscode.Selection(3, 2, 3, 4), + ]); + + await emulator.runCommand("killRegion"); + + assertTextEqual( + activeTextEditor, + `0123456789 +abefghij +ABEFGHIJ +klopqrst +KLMNOPQRST`, + ); + + assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(3, 2, 3, 2)]); + + // Yanking the killed text in the rect-mark-mode. + // The text is yanked as a rectangle and automatically indented. + setEmptyCursors(activeTextEditor, [4, 2]); + await emulator.runCommand("yank"); + assertTextEqual( + activeTextEditor, + `0123456789 +abefghij +ABEFGHIJ +klopqrst +KLcdMNOPQRST + CD + mn`, + ); + }); + + test("killing in rectangle-mark-mode followed by another kill command that appends the killed text", async () => { + setEmptyCursors(activeTextEditor, [1, 2]); + const killRing = new KillRing(3); + const emulator = new EmacsEmulator(activeTextEditor, killRing); emulator.rectangleMarkMode(); @@ -111,6 +160,97 @@ KLMNOPQRST`, ); assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(3, 2, 3, 2)]); + + await emulator.runCommand("killLine"); + + assertTextEqual( + activeTextEditor, + `0123456789 +abefghij +ABEFGHIJ +kl +KLMNOPQRST`, + ); + + // Yanking the killed text in the rect-mark-mode. + // The text is yanked as a rectangle and automatically indented. + setEmptyCursors(activeTextEditor, [4, 2]); + await emulator.runCommand("yank"); + assertTextEqual( + activeTextEditor, + `0123456789 +abefghij +ABEFGHIJ +kl +KLcdMNOPQRST + CD + mnopqrst`, + ); + }); + + test("Killing a region including empty lines", async () => { + await clearTextEditor( + activeTextEditor, + `0123456789 + +abcdefghij + +ABCDEFGHIJ + +klmnopqrst + +KLMNOPQRST`, + ); + + setEmptyCursors(activeTextEditor, [0, 2]); + + const killRing = new KillRing(3); + const emulator = new EmacsEmulator(activeTextEditor, killRing); + + emulator.rectangleMarkMode(); + await emulator.runCommand("forwardChar"); + await emulator.runCommand("forwardChar"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + + assert.deepStrictEqual(activeTextEditor.selections, [ + new vscode.Selection(0, 2, 0, 4), + new vscode.Selection(1, 0, 1, 0), + new vscode.Selection(2, 2, 2, 4), + new vscode.Selection(3, 0, 3, 0), + new vscode.Selection(4, 2, 4, 4), + new vscode.Selection(5, 0, 5, 0), + ]); + + await emulator.runCommand("killRegion"); + + assertTextEqual( + activeTextEditor, + `01456789 + +abefghij + +ABEFGHIJ + +klmnopqrst + +KLMNOPQRST`, + ); + + await clearTextEditor(activeTextEditor, ""); + await emulator.runCommand("yank"); + assertTextEqual( + activeTextEditor, + `23 + +cd + +CD + `, + ); }); test("typing a character in rectangle-mark-mode", async () => { From cfc21f266283d402e728eae5f0ba9feca37626f0 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Mon, 15 Jan 2024 23:39:34 +0900 Subject: [PATCH 05/16] Implement yanking rect-killed texts --- src/commands/helpers/eol.ts | 2 +- src/commands/kill.ts | 8 -- src/emulator.ts | 14 ++- src/kill-yank/index.ts | 93 ++++++++++++++----- src/kill-yank/kill-ring-entity/editor-text.ts | 4 + 5 files changed, 82 insertions(+), 39 deletions(-) diff --git a/src/commands/helpers/eol.ts b/src/commands/helpers/eol.ts index 81890c1201..436a12c5e2 100644 --- a/src/commands/helpers/eol.ts +++ b/src/commands/helpers/eol.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -export function getEolChar(eol: vscode.EndOfLine): string | undefined { +export function getEolChar(eol: vscode.EndOfLine): string { switch (eol) { case vscode.EndOfLine.CRLF: return "\r\n"; diff --git a/src/commands/kill.ts b/src/commands/kill.ts index e8c8a88f5f..9c7d76e402 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -166,16 +166,8 @@ export class KillRegion extends KillYankCommand { public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { if (this.emacsController.inRectMarkMode) { - const selectionsAfterRectDisabled = this.emacsController.nativeSelections.map((selection) => { - const newLine = selection.active.line; - const newChar = Math.min(selection.active.character, selection.anchor.character); - return new vscode.Selection(newLine, newChar, newLine, newChar); - }); - const ranges = this.emacsController.nativeSelections.map((s) => new Range(s.start, s.end)); await this.killYanker.kill(ranges, true); - - textEditor.selections = selectionsAfterRectDisabled; } else { const ranges = getNonEmptySelections(textEditor); await this.killYanker.kill(ranges, false); diff --git a/src/emulator.ts b/src/emulator.ts index 7d114636a7..fdb9328c33 100644 --- a/src/emulator.ts +++ b/src/emulator.ts @@ -27,6 +27,8 @@ import { PromiseDelegate } from "./promise-delegate"; import { delay, type Unreliable } from "./utils"; export interface IEmacsController { + readonly textEditor: TextEditor; + runCommand(commandName: string): void | Thenable; enterMarkMode(pushMark?: boolean): void; @@ -39,7 +41,10 @@ export interface IEmacsController { } export class EmacsEmulator implements IEmacsController, vscode.Disposable { - private textEditor: TextEditor; + private _textEditor: TextEditor; + public get textEditor(): TextEditor { + return this._textEditor; + } private commandRegistry: EmacsCommandRegistry; @@ -123,7 +128,7 @@ export class EmacsEmulator implements IEmacsController, vscode.Disposable { minibuffer: Minibuffer = new InputBoxMinibuffer(), textRegister: Map = new Map(), ) { - this.textEditor = textEditor; + this._textEditor = textEditor; this.setNativeSelections(this.rectMode ? [] : textEditor.selections); // TODO: `[]` is workaround. this.markRing = new MarkRing(Configuration.instance.markRingMax); @@ -180,7 +185,7 @@ export class EmacsEmulator implements IEmacsController, vscode.Disposable { this.commandRegistry.register(new FindCommands.IsearchAbort(this, searchState)); this.commandRegistry.register(new FindCommands.IsearchExit(this, searchState)); - const killYanker = new KillYanker(textEditor, killRing, minibuffer); + const killYanker = new KillYanker(this, killRing, minibuffer); this.commandRegistry.register(new KillCommands.KillWord(this, killYanker)); this.commandRegistry.register(new KillCommands.BackwardKillWord(this, killYanker)); this.commandRegistry.register(new KillCommands.KillLine(this, killYanker)); @@ -230,8 +235,7 @@ export class EmacsEmulator implements IEmacsController, vscode.Disposable { } public setTextEditor(textEditor: TextEditor): void { - this.textEditor = textEditor; - this.killYanker.setTextEditor(textEditor); + this._textEditor = textEditor; } /** diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 1716168e04..15d6b77a75 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -3,19 +3,25 @@ import * as vscode from "vscode"; import { Position, Range, TextEditor } from "vscode"; import { MessageManager } from "../message"; import { equalPositions } from "../utils"; +import type { IEmacsController } from "../emulator"; import { KillRing, KillRingEntity } from "./kill-ring"; import { ClipboardTextKillRingEntity } from "./kill-ring-entity/clipboard-text"; import { AppendDirection, EditorTextKillRingEntity } from "./kill-ring-entity/editor-text"; import { logger } from "../logger"; import { convertSelectionToRectSelections, getRectText } from "../rectangle"; +import { getEolChar } from "../commands/helpers/eol"; export { AppendDirection }; export class KillYanker implements vscode.Disposable { - private textEditor: TextEditor; + private emacsController: IEmacsController; private killRing: KillRing | null; // If null, killRing is disabled and only clipboard is used. private minibuffer: Minibuffer; + private get textEditor(): TextEditor { + return this.emacsController.textEditor; + } + private isAppending = false; private prevKillPositions: Position[]; private docChangedAfterYank = false; @@ -26,8 +32,8 @@ export class KillYanker implements vscode.Disposable { private disposables: vscode.Disposable[]; - constructor(textEditor: TextEditor, killRing: KillRing | null, minibuffer: Minibuffer) { - this.textEditor = textEditor; + constructor(emacsController: IEmacsController, killRing: KillRing | null, minibuffer: Minibuffer) { + this.emacsController = emacsController; this.killRing = killRing; this.minibuffer = minibuffer; @@ -44,14 +50,6 @@ export class KillYanker implements vscode.Disposable { vscode.window.onDidChangeTextEditorSelection(this.onDidChangeTextEditorSelection, this, this.disposables); } - public setTextEditor(textEditor: TextEditor): void { - this.textEditor = textEditor; - } - - public getTextEditor(): TextEditor { - return this.textEditor; - } - public dispose(): void { for (const disposable of this.disposables) { disposable.dispose(); @@ -147,12 +145,12 @@ export class KillYanker implements vscode.Disposable { if (killRingEntity.type === "editor") { const selections = this.textEditor.selections; - const regionTexts = killRingEntity.getRegionTextsList(); - const fillIndent = killRingEntity.hasRectModeText(); - const shouldPasteSeparately = regionTexts.length > 1 && flattenedText.split("\n").length !== regionTexts.length; - const canPasteSeparately = regionTexts.length === selections.length; + const regionTextsList = killRingEntity.getRegionTextsList(); + const shouldPasteSeparately = + regionTextsList.length > 1 && flattenedText.split("\n").length !== regionTextsList.length; + const canPasteSeparately = regionTextsList.length === selections.length; const pasteSeparately = shouldPasteSeparately && canPasteSeparately; - const customPaste = pasteSeparately || fillIndent; + const customPaste = pasteSeparately || killRingEntity.hasRectModeText(); if (customPaste) { // The normal `paste` command is not suitable in this case, so we use `edit` command instead. if (!pasteSeparately) { @@ -161,18 +159,49 @@ export class KillYanker implements vscode.Disposable { } const success = await this.textEditor.edit((editBuilder) => { selections.forEach((selection, i) => { - const indent = selection.start.character; if (!selection.isEmpty) { editBuilder.delete(selection); } + // `regionTexts.length === selections.length` has already been checked, // so noUncheckedIndexedAccess rule can be skipped here. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - let text = regionTexts[i]!.getAppendedText(); - if (fillIndent) { - text = text.replace(/^/gm, " ".repeat(indent)).slice(indent); - } - editBuilder.insert(selection.start, text); + const regionTexts = regionTextsList[i]!; + + let pasteCursor = selection.start; + let textToAddAfterBuffer = ""; + regionTexts.forEach((regionText) => { + const indent = pasteCursor.character; + const regionHeight = regionText.range.end.line - regionText.range.start.line; + const regionWidth = regionText.range.end.character - regionText.range.start.character; + if (regionText.rectMode) { + regionText.text.split(/\r?\n/).forEach((lineToPaste, j) => { + const pastedLineLength = lineToPaste.length; + const targetLine = pasteCursor.line + j; + if (targetLine < this.textEditor.document.lineCount) { + const existingIndent = this.textEditor.document.lineAt(targetLine).range.end.character; + const whiteSpacesBefore = indent > existingIndent ? " ".repeat(indent - existingIndent) : ""; + const whiteSpacesAfter = " ".repeat(regionWidth - pastedLineLength); + const whiteSpacesFilledLine = whiteSpacesBefore + lineToPaste + whiteSpacesAfter; + editBuilder.insert(new Position(targetLine, pasteCursor.character), whiteSpacesFilledLine); + } else { + const whiteSpacesBefore = " ".repeat(indent); + const whiteSpacesAfter = " ".repeat(regionWidth - pastedLineLength); + const whiteSpacesFilledLine = whiteSpacesBefore + lineToPaste + whiteSpacesAfter; + textToAddAfterBuffer += getEolChar(this.textEditor.document.eol) + whiteSpacesFilledLine; + } + }); + } else { + if (pasteCursor.line < this.textEditor.document.lineCount) { + editBuilder.insert(pasteCursor, regionText.text); + } else { + textToAddAfterBuffer += regionText.text; + } + } + pasteCursor = new Position(pasteCursor.line + regionHeight, pasteCursor.character + regionWidth); + }); + const endOfDoc = this.textEditor.document.lineAt(this.textEditor.document.lineCount - 1).range.end; + editBuilder.insert(endOfDoc, textToAddAfterBuffer); }); }); if (!success) { @@ -240,6 +269,14 @@ export class KillYanker implements vscode.Disposable { } private async delete(ranges: vscode.Range[], rectMode: boolean, maxTrials = 3): Promise { + const selectionsAfterRectDeleted = + this.emacsController.inRectMarkMode && + this.emacsController.nativeSelections.map((selection) => { + const newLine = selection.active.line; + const newChar = Math.min(selection.active.character, selection.anchor.character); + return new vscode.Selection(newLine, newChar, newLine, newChar); + }); + const deleteRanges = rectMode ? ranges.flatMap((range) => convertSelectionToRectSelections(this.textEditor.document, new vscode.Selection(range.start, range.end)), @@ -257,8 +294,10 @@ export class KillYanker implements vscode.Disposable { trial++; } - // TODO: Set the selections to the deleted ranges in the case of rectMode, which is now done in the kill command. - // It is needed to append the following kills. + if (selectionsAfterRectDeleted) { + this.emacsController.exitMarkMode(); + this.textEditor.selections = selectionsAfterRectDeleted; + } return success; } @@ -272,6 +311,10 @@ export class KillYanker implements vscode.Disposable { } private getCursorPositions(): Position[] { - return this.textEditor.selections.map((selection) => selection.active); + if (this.emacsController.inRectMarkMode) { + return this.emacsController.nativeSelections.map((selection) => selection.active); + } else { + return this.textEditor.selections.map((selection) => selection.active); + } } } diff --git a/src/kill-yank/kill-ring-entity/editor-text.ts b/src/kill-yank/kill-ring-entity/editor-text.ts index e5ef8dc0ce..30d7e4aa5b 100644 --- a/src/kill-yank/kill-ring-entity/editor-text.ts +++ b/src/kill-yank/kill-ring-entity/editor-text.ts @@ -23,6 +23,10 @@ class AppendedRegionTexts { this.regionTexts = [regionText]; } + public forEach(callback: (regionText: IRegionText) => void) { + this.regionTexts.forEach(callback); + } + public append(another: AppendedRegionTexts, appendDirection: AppendDirection = AppendDirection.Forward) { if (appendDirection === AppendDirection.Forward) { this.regionTexts = this.regionTexts.concat(another.regionTexts); From 78b8ca8f2a81b62d6f1038b8a030f83e25d637f2 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Mon, 15 Jan 2024 23:45:04 +0900 Subject: [PATCH 06/16] pdate EditorTextKillRingEntity to use a specified EOL char --- src/kill-yank/index.ts | 1 + src/kill-yank/kill-ring-entity/editor-text.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 15d6b77a75..26b9e0e82c 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -105,6 +105,7 @@ export class KillYanker implements vscode.Disposable { text: rectMarkMode ? getRectText(this.textEditor.document, range) : this.textEditor.document.getText(range), rectMode: rectMarkMode, })), + this.textEditor.document.eol, ); if (this.killRing !== null) { diff --git a/src/kill-yank/kill-ring-entity/editor-text.ts b/src/kill-yank/kill-ring-entity/editor-text.ts index 30d7e4aa5b..331b734ff6 100644 --- a/src/kill-yank/kill-ring-entity/editor-text.ts +++ b/src/kill-yank/kill-ring-entity/editor-text.ts @@ -1,5 +1,6 @@ -import { Range } from "vscode"; +import { Range, EndOfLine } from "vscode"; import { IKillRingEntity } from "./kill-ring-entity"; +import { getEolChar } from "../../commands/helpers/eol"; export enum AppendDirection { Forward, @@ -55,9 +56,14 @@ class AppendedRegionTexts { export class EditorTextKillRingEntity implements IKillRingEntity { public readonly type = "editor"; private regionTextsList: AppendedRegionTexts[]; + private eolChar: string; - constructor(regionTexts: IRegionText[]) { + constructor( + regionTexts: IRegionText[], + private eol: EndOfLine, + ) { this.regionTextsList = regionTexts.map((regionText) => new AppendedRegionTexts(regionText)); + this.eolChar = getEolChar(eol); } public isSameClipboardText(clipboardText: string): boolean { @@ -87,7 +93,7 @@ export class EditorTextKillRingEntity implements IKillRingEntity { sortedAppendedTexts.forEach((item, i) => { const prevItem = sortedAppendedTexts[i - 1]; if (prevItem && prevItem.range.start.line !== item.range.start.line) { - allText += "\n" + item.text; // TODO: Use the appropriate EOL char. + allText += this.eolChar + item.text; } else { allText += item.text; } From ea401b6adeb7cc07c788b0d04ae6aab07cd43b8b Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 01:02:56 +0900 Subject: [PATCH 07/16] Fix KillYanker.delete() --- src/kill-yank/index.ts | 9 +++++---- src/test/suite/rectangle-mark-mode.test.ts | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 26b9e0e82c..04e68b3af2 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -291,14 +291,15 @@ export class KillYanker implements vscode.Disposable { deleteRanges.forEach((range) => { editBuilder.delete(range); }); + + if (selectionsAfterRectDeleted) { + this.emacsController.exitMarkMode(); + this.textEditor.selections = selectionsAfterRectDeleted; + } }); trial++; } - if (selectionsAfterRectDeleted) { - this.emacsController.exitMarkMode(); - this.textEditor.selections = selectionsAfterRectDeleted; - } return success; } diff --git a/src/test/suite/rectangle-mark-mode.test.ts b/src/test/suite/rectangle-mark-mode.test.ts index 9d6a3ae988..b0e3a09336 100644 --- a/src/test/suite/rectangle-mark-mode.test.ts +++ b/src/test/suite/rectangle-mark-mode.test.ts @@ -161,6 +161,8 @@ KLMNOPQRST`, assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(3, 2, 3, 2)]); + await delay(); // Wait for all related event listeners to have been called + await emulator.runCommand("killLine"); assertTextEqual( From 4039b4adb46edd8456ee0025e763301f913ca592 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 02:15:30 +0900 Subject: [PATCH 08/16] Fix the kill command in rect-mark-mode --- src/kill-yank/index.ts | 14 +++-- src/test/suite/rectangle-mark-mode.test.ts | 60 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 04e68b3af2..27d59e560e 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -173,9 +173,9 @@ export class KillYanker implements vscode.Disposable { let textToAddAfterBuffer = ""; regionTexts.forEach((regionText) => { const indent = pasteCursor.character; - const regionHeight = regionText.range.end.line - regionText.range.start.line; - const regionWidth = regionText.range.end.character - regionText.range.start.character; if (regionText.rectMode) { + const regionHeight = regionText.range.end.line - regionText.range.start.line; + const regionWidth = regionText.range.end.character - regionText.range.start.character; regionText.text.split(/\r?\n/).forEach((lineToPaste, j) => { const pastedLineLength = lineToPaste.length; const targetLine = pasteCursor.line + j; @@ -192,14 +192,22 @@ export class KillYanker implements vscode.Disposable { textToAddAfterBuffer += getEolChar(this.textEditor.document.eol) + whiteSpacesFilledLine; } }); + pasteCursor = new Position(pasteCursor.line + regionHeight, pasteCursor.character + regionWidth); } else { if (pasteCursor.line < this.textEditor.document.lineCount) { editBuilder.insert(pasteCursor, regionText.text); } else { textToAddAfterBuffer += regionText.text; } + const regionHeight = regionText.range.end.line - regionText.range.start.line; + if (regionHeight === 0) { + const regionLastLineLength = regionText.range.end.character - regionText.range.start.character; + pasteCursor = new Position(pasteCursor.line, pasteCursor.character + regionLastLineLength); + } else { + const regionLastLineLength = regionText.range.end.character; + pasteCursor = new Position(pasteCursor.line + regionHeight, regionLastLineLength); + } } - pasteCursor = new Position(pasteCursor.line + regionHeight, pasteCursor.character + regionWidth); }); const endOfDoc = this.textEditor.document.lineAt(this.textEditor.document.lineCount - 1).range.end; editBuilder.insert(endOfDoc, textToAddAfterBuffer); diff --git a/src/test/suite/rectangle-mark-mode.test.ts b/src/test/suite/rectangle-mark-mode.test.ts index b0e3a09336..8d937d603b 100644 --- a/src/test/suite/rectangle-mark-mode.test.ts +++ b/src/test/suite/rectangle-mark-mode.test.ts @@ -190,6 +190,66 @@ KLcdMNOPQRST ); }); + test("killing in rectangle-mark-mode followed by multiple kill command that append killed texts including multiple lines", async () => { + setEmptyCursors(activeTextEditor, [0, 8]); + const killRing = new KillRing(3); + const emulator = new EmacsEmulator(activeTextEditor, killRing); + + emulator.rectangleMarkMode(); + + await emulator.runCommand("forwardChar"); + await emulator.runCommand("forwardChar"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + + assert.deepStrictEqual(activeTextEditor.selections, [ + new vscode.Selection(0, 8, 0, 10), + new vscode.Selection(1, 8, 1, 10), + new vscode.Selection(2, 8, 2, 10), + ]); + + await emulator.runCommand("killRegion"); + + assertTextEqual( + activeTextEditor, + `01234567 +abcdefgh +ABCDEFGH +klmnopqrst +KLMNOPQRST`, + ); + + assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(2, 8, 2, 8)]); + + await delay(); // Wait for all related event listeners to have been called + + await emulator.runCommand("killLine"); + await emulator.runCommand("killLine"); + await emulator.runCommand("killLine"); + + assertTextEqual( + activeTextEditor, + `01234567 +abcdefgh +ABCDEFGHKLMNOPQRST`, + ); + + // Yanking the killed text in the rect-mark-mode. + // The text is yanked as a rectangle and automatically indented. + setEmptyCursors(activeTextEditor, [2, 1]); + await emulator.runCommand("yank"); + assertTextEqual( + activeTextEditor, + `01234567 +abcdefgh +A89BCDEFGHKLMNOPQRST + ij + IJ +klmnopqrst +`, + ); + }); + test("Killing a region including empty lines", async () => { await clearTextEditor( activeTextEditor, From b423c962e8b031c0034491fc115dcc7d900515de Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 16:27:22 +0900 Subject: [PATCH 09/16] Fix --- src/commands/kill.ts | 4 ++-- src/kill-yank/index.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/kill.ts b/src/commands/kill.ts index 9c7d76e402..c7c5e80e8b 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -166,7 +166,7 @@ export class KillRegion extends KillYankCommand { public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { if (this.emacsController.inRectMarkMode) { - const ranges = this.emacsController.nativeSelections.map((s) => new Range(s.start, s.end)); + const ranges = this.emacsController.nativeSelections; await this.killYanker.kill(ranges, true); } else { const ranges = getNonEmptySelections(textEditor); @@ -185,7 +185,7 @@ export class CopyRegion extends KillYankCommand { public async run(textEditor: TextEditor, isInMarkMode: boolean, prefixArgument: number | undefined): Promise { if (this.emacsController.inRectMarkMode) { - const ranges = this.emacsController.nativeSelections.map((s) => new Range(s.start, s.end)); + const ranges = this.emacsController.nativeSelections; await this.killYanker.copy(ranges, true); } else { const ranges = getNonEmptySelections(textEditor); diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 27d59e560e..604d5e181b 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -74,7 +74,7 @@ export class KillYanker implements vscode.Disposable { } public async kill( - ranges: Range[], + ranges: readonly Range[], rectMarkMode: boolean, appendDirection: AppendDirection = AppendDirection.Forward, ): Promise { @@ -94,7 +94,7 @@ export class KillYanker implements vscode.Disposable { } public async copy( - ranges: Range[], + ranges: readonly Range[], rectMarkMode: boolean, shouldAppend = false, appendDirection: AppendDirection = AppendDirection.Forward, @@ -277,7 +277,7 @@ export class KillYanker implements vscode.Disposable { this.prevYankPositions = this.textEditor.selections.map((selection) => selection.active); } - private async delete(ranges: vscode.Range[], rectMode: boolean, maxTrials = 3): Promise { + private async delete(ranges: readonly vscode.Range[], rectMode: boolean, maxTrials = 3): Promise { const selectionsAfterRectDeleted = this.emacsController.inRectMarkMode && this.emacsController.nativeSelections.map((selection) => { From bae0fe213bf6887af110de878e88f2d86f6849c6 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 16:33:49 +0900 Subject: [PATCH 10/16] Fix --- src/kill-yank/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 604d5e181b..15a1a54905 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -113,7 +113,6 @@ export class KillYanker implements vscode.Disposable { if (shouldAppend && currentKill instanceof EditorTextKillRingEntity) { currentKill.append(newKillEntity, appendDirection); await vscode.env.clipboard.writeText(currentKill.asString()); - return; } else { this.killRing.push(newKillEntity); await vscode.env.clipboard.writeText(newKillEntity.asString()); From 3c4b7e7262af377405afde0400c442d058faaaba Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 16:40:01 +0900 Subject: [PATCH 11/16] Fix --- src/kill-yank/kill-ring-entity/editor-text.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/kill-yank/kill-ring-entity/editor-text.ts b/src/kill-yank/kill-ring-entity/editor-text.ts index 331b734ff6..bbd27cf965 100644 --- a/src/kill-yank/kill-ring-entity/editor-text.ts +++ b/src/kill-yank/kill-ring-entity/editor-text.ts @@ -58,10 +58,7 @@ export class EditorTextKillRingEntity implements IKillRingEntity { private regionTextsList: AppendedRegionTexts[]; private eolChar: string; - constructor( - regionTexts: IRegionText[], - private eol: EndOfLine, - ) { + constructor(regionTexts: IRegionText[], eol: EndOfLine) { this.regionTextsList = regionTexts.map((regionText) => new AppendedRegionTexts(regionText)); this.eolChar = getEolChar(eol); } From 40f98a6dd80114b317f330dc74d39f24a6d0a63b Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 16:43:44 +0900 Subject: [PATCH 12/16] Fix --- src/test/suite/rectangle-mark-mode.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/suite/rectangle-mark-mode.test.ts b/src/test/suite/rectangle-mark-mode.test.ts index 8d937d603b..bb0353cd1e 100644 --- a/src/test/suite/rectangle-mark-mode.test.ts +++ b/src/test/suite/rectangle-mark-mode.test.ts @@ -114,7 +114,7 @@ KLMNOPQRST`, assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(3, 2, 3, 2)]); - // Yanking the killed text in the rect-mark-mode. + // Yank the killed text in the rect-mark-mode. // The text is yanked as a rectangle and automatically indented. setEmptyCursors(activeTextEditor, [4, 2]); await emulator.runCommand("yank"); @@ -174,7 +174,7 @@ kl KLMNOPQRST`, ); - // Yanking the killed text in the rect-mark-mode. + // Yank the killed text in the rect-mark-mode. // The text is yanked as a rectangle and automatically indented. setEmptyCursors(activeTextEditor, [4, 2]); await emulator.runCommand("yank"); @@ -234,7 +234,7 @@ abcdefgh ABCDEFGHKLMNOPQRST`, ); - // Yanking the killed text in the rect-mark-mode. + // Yank the killed text in the rect-mark-mode. // The text is yanked as a rectangle and automatically indented. setEmptyCursors(activeTextEditor, [2, 1]); await emulator.runCommand("yank"); From dd00bc2edc578808fd0b2fc157107627bf062bbc Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Tue, 16 Jan 2024 16:50:13 +0900 Subject: [PATCH 13/16] Fix --- src/kill-yank/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 15a1a54905..369949d770 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -153,8 +153,8 @@ export class KillYanker implements vscode.Disposable { const customPaste = pasteSeparately || killRingEntity.hasRectModeText(); if (customPaste) { // The normal `paste` command is not suitable in this case, so we use `edit` command instead. - if (!pasteSeparately) { - // `pasteSeparately` is false, so there is only one paste target selection. + if (!canPasteSeparately) { + // `canPasteSeparately` is false, so give up to paste separately and use the first selection. this.textEditor.selections = [this.textEditor.selection]; } const success = await this.textEditor.edit((editBuilder) => { @@ -163,7 +163,8 @@ export class KillYanker implements vscode.Disposable { editBuilder.delete(selection); } - // `regionTexts.length === selections.length` has already been checked, + // `canPasteSeparately = regionTexts.length === selections.length` has already been checked + // or this.selections.length === 1 is confirmed, so regionTextsList[i] is not null. // so noUncheckedIndexedAccess rule can be skipped here. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const regionTexts = regionTextsList[i]!; From c860353e4954c9b255000e8d3e8a327cd5a34543 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Wed, 17 Jan 2024 18:36:18 +0900 Subject: [PATCH 14/16] Fix killLine not to work when the cursor is at the end of doc --- src/commands/kill.ts | 22 +++++---- .../commands/kill-yank/kill-line.test.ts | 47 +++++++++++++++---- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/commands/kill.ts b/src/commands/kill.ts index c7c5e80e8b..47d1fcf828 100644 --- a/src/commands/kill.ts +++ b/src/commands/kill.ts @@ -111,8 +111,17 @@ export class KillLine extends KillYankCommand { this.emacsController.exitMarkMode(); makeSelectionsEmpty(textEditor); - const ranges = textEditor.selections.map((selection) => { - const cursor = selection.active; + const endOfDoc = textEditor.document.lineAt(textEditor.document.lineCount - 1).range.end; + + const actives = textEditor.selections.map((selection) => selection.active); + const nonEndActives = actives.filter((active) => !active.isEqual(endOfDoc)); + + if (nonEndActives.length === 0) { + MessageManager.showMessage("End of buffer"); + return Promise.resolve(); + } + + const ranges = nonEndActives.map((cursor) => { const lineAtCursor = textEditor.document.lineAt(cursor.line); if (prefixArgument !== undefined) { @@ -134,14 +143,7 @@ export class KillLine extends KillYankCommand { } }); - const endOfDoc = textEditor.document.lineAt(textEditor.document.lineCount - 1).range.end; - const nonEndRanges = ranges.filter((range) => !(range.isEmpty && range.end.isEqual(endOfDoc))); - if (nonEndRanges.length === 0) { - MessageManager.showMessage("End of buffer"); - return Promise.resolve(); - } - - return this.killYanker.kill(nonEndRanges, false).then(() => revealPrimaryActive(textEditor)); + return this.killYanker.kill(ranges, false).then(() => revealPrimaryActive(textEditor)); } } diff --git a/src/test/suite/commands/kill-yank/kill-line.test.ts b/src/test/suite/commands/kill-yank/kill-line.test.ts index 439036563c..a0378ede61 100644 --- a/src/test/suite/commands/kill-yank/kill-line.test.ts +++ b/src/test/suite/commands/kill-yank/kill-line.test.ts @@ -18,11 +18,13 @@ suite("killLine", () => { let activeTextEditor: vscode.TextEditor; let emulator: EmacsEmulator; - setup(async () => { - const initialText = `0123456789 + const initialText = `0123456789 abcdefghij ABCDEFGHIJ`; + + setup(async () => { activeTextEditor = await setupWorkspace(initialText); + vscode.env.clipboard.writeText(""); }); teardown(cleanUpWorkspace); @@ -158,6 +160,10 @@ abcdefghij await vscode.commands.executeCommand(interruptingCommand); // Interrupt + const endOfDoc = activeTextEditor.document.lineAt(activeTextEditor.document.lineCount - 1).range.end; + const secondKillAtEndOfDoc = activeTextEditor.selections.every((selection) => + selection.active.isEqual(endOfDoc), + ); await emulator.runCommand("killLine"); // 3nd line await emulator.runCommand("killLine"); // EOL of 3nd (no effect) @@ -166,9 +172,12 @@ abcdefghij setEmptyCursors(activeTextEditor, [0, 0]); await emulator.runCommand("yank"); - assert.ok( - !activeTextEditor.document.getText().includes("fghij\n"), // First 2 kills does not appear here - ); + if (!secondKillAtEndOfDoc) { + // If the second killLine was at the end of the doc, it didn't work. + assert.ok( + !activeTextEditor.document.getText().includes("fghij\n"), // First 2 kills does not appear here + ); + } await emulator.runCommand("yankPop"); assert.strictEqual(activeTextEditor.document.getText(), "fghij\n"); // First 2 kills appear here }); @@ -207,6 +216,10 @@ abcdefghij await op(); // Interrupt + const endOfDoc = activeTextEditor.document.lineAt(activeTextEditor.document.lineCount - 1).range.end; + const secondKillAtEndOfDoc = activeTextEditor.selections.every((selection) => + selection.active.isEqual(endOfDoc), + ); await emulator.runCommand("killLine"); // 3nd line await emulator.runCommand("killLine"); // EOL of 3nd (no effect) @@ -215,13 +228,31 @@ abcdefghij setEmptyCursors(activeTextEditor, [0, 0]); await emulator.runCommand("yank"); - assert.ok( - !activeTextEditor.document.getText().includes("fghij\n"), // First 2 kills does not appear here - ); + if (!secondKillAtEndOfDoc) { + // If the second killLine was at the end of the doc, it didn't work. + assert.ok( + !activeTextEditor.document.getText().includes("fghij\n"), // First 2 kills does not appear here + ); + } await emulator.runCommand("yankPop"); assert.strictEqual(activeTextEditor.document.getText(), "fghij\n"); // First 2 kills appear here }); }); + + test("nothing happens if the cursor is at the end of the document", async () => { + setEmptyCursors(activeTextEditor, [2, 10]); + + await emulator.runCommand("killLine"); + + assertTextEqual(activeTextEditor, initialText); + + await clearTextEditor(activeTextEditor); + + setEmptyCursors(activeTextEditor, [0, 0]); + await emulator.runCommand("yank"); + + assertTextEqual(activeTextEditor, ""); + }); }); suite("when prefix argument specified", () => { From bac42adc6110d73872f01fb9d77620c8146df48c Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 18 Jan 2024 14:29:19 +0900 Subject: [PATCH 15/16] Fix pasting --- src/kill-yank/index.ts | 18 ++---- src/test/suite/rectangle-mark-mode.test.ts | 69 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 369949d770..827623f42d 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -164,8 +164,8 @@ export class KillYanker implements vscode.Disposable { } // `canPasteSeparately = regionTexts.length === selections.length` has already been checked - // or this.selections.length === 1 is confirmed, so regionTextsList[i] is not null. - // so noUncheckedIndexedAccess rule can be skipped here. + // or `this.selections.length === 1` is confirmed, so regionTextsList[i] is not null + // and the `noUncheckedIndexedAccess` rule can be skipped here. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const regionTexts = regionTextsList[i]!; @@ -181,8 +181,8 @@ export class KillYanker implements vscode.Disposable { const targetLine = pasteCursor.line + j; if (targetLine < this.textEditor.document.lineCount) { const existingIndent = this.textEditor.document.lineAt(targetLine).range.end.character; - const whiteSpacesBefore = indent > existingIndent ? " ".repeat(indent - existingIndent) : ""; - const whiteSpacesAfter = " ".repeat(regionWidth - pastedLineLength); + const whiteSpacesBefore = " ".repeat(Math.max(indent - existingIndent, 0)); + const whiteSpacesAfter = " ".repeat(Math.max(regionWidth - pastedLineLength, 0)); const whiteSpacesFilledLine = whiteSpacesBefore + lineToPaste + whiteSpacesAfter; editBuilder.insert(new Position(targetLine, pasteCursor.character), whiteSpacesFilledLine); } else { @@ -196,16 +196,10 @@ export class KillYanker implements vscode.Disposable { } else { if (pasteCursor.line < this.textEditor.document.lineCount) { editBuilder.insert(pasteCursor, regionText.text); + // In this case, `pasteCursor` shouldn't be updated because `editBuilder.insert` handles it internally. } else { textToAddAfterBuffer += regionText.text; - } - const regionHeight = regionText.range.end.line - regionText.range.start.line; - if (regionHeight === 0) { - const regionLastLineLength = regionText.range.end.character - regionText.range.start.character; - pasteCursor = new Position(pasteCursor.line, pasteCursor.character + regionLastLineLength); - } else { - const regionLastLineLength = regionText.range.end.character; - pasteCursor = new Position(pasteCursor.line + regionHeight, regionLastLineLength); + // In this case, `pasteCursor` doesn't need to be updated because `editBuilder.insert` will no longer be called after this. } } }); diff --git a/src/test/suite/rectangle-mark-mode.test.ts b/src/test/suite/rectangle-mark-mode.test.ts index bb0353cd1e..134d30bade 100644 --- a/src/test/suite/rectangle-mark-mode.test.ts +++ b/src/test/suite/rectangle-mark-mode.test.ts @@ -246,6 +246,75 @@ A89BCDEFGHKLMNOPQRST ij IJ klmnopqrst +`, + ); + }); + + test("killing in rectangle-mark-mode followed by multiple kill command that append killed texts including multiple lines, then yanking the killed text to a buffer with an enough number of lines", async () => { + setEmptyCursors(activeTextEditor, [0, 8]); + const killRing = new KillRing(3); + const emulator = new EmacsEmulator(activeTextEditor, killRing); + + emulator.rectangleMarkMode(); + + await emulator.runCommand("forwardChar"); + await emulator.runCommand("forwardChar"); + await emulator.runCommand("nextLine"); + await emulator.runCommand("nextLine"); + + assert.deepStrictEqual(activeTextEditor.selections, [ + new vscode.Selection(0, 8, 0, 10), + new vscode.Selection(1, 8, 1, 10), + new vscode.Selection(2, 8, 2, 10), + ]); + + await emulator.runCommand("killRegion"); + + assertTextEqual( + activeTextEditor, + `01234567 +abcdefgh +ABCDEFGH +klmnopqrst +KLMNOPQRST`, + ); + + assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(2, 8, 2, 8)]); + + await delay(); // Wait for all related event listeners to have been called + + await emulator.runCommand("killLine"); + await emulator.runCommand("killLine"); + await emulator.runCommand("killLine"); + + assertTextEqual( + activeTextEditor, + `01234567 +abcdefgh +ABCDEFGHKLMNOPQRST`, + ); + + // Yank the killed text in the rect-mark-mode. + // The text is yanked as a rectangle and automatically indented. + clearTextEditor(activeTextEditor, "\n".repeat(10)); + await delay(100); + setEmptyCursors(activeTextEditor, [2, 0]); + assert.deepStrictEqual(activeTextEditor.selections, [new vscode.Selection(2, 0, 2, 0)]); + await emulator.runCommand("yank"); + assertTextEqual( + activeTextEditor, + ` + +89 +ij +IJ +klmnopqrst + + + + + + `, ); }); From ff8add440e5e6be5a844f63b0063aa8193037c9d Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 18 Jan 2024 20:47:08 +0900 Subject: [PATCH 16/16] Fix rect pasting --- src/kill-yank/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/kill-yank/index.ts b/src/kill-yank/index.ts index 827623f42d..0aee083f7d 100644 --- a/src/kill-yank/index.ts +++ b/src/kill-yank/index.ts @@ -192,7 +192,10 @@ export class KillYanker implements vscode.Disposable { textToAddAfterBuffer += getEolChar(this.textEditor.document.eol) + whiteSpacesFilledLine; } }); - pasteCursor = new Position(pasteCursor.line + regionHeight, pasteCursor.character + regionWidth); + pasteCursor = new Position( + pasteCursor.line + regionHeight, // This rect paste is different from the normal paste/edit.insert from the vertical direction perspective, so we need to update the vertical position of the cursor. + pasteCursor.character, // In contrast, the horizontal movement is automatically handled by the `editBuilder.insert` above internally, so we don't need to update the horizontal position of the cursor. + ); } else { if (pasteCursor.line < this.textEditor.document.lineCount) { editBuilder.insert(pasteCursor, regionText.text);