diff --git a/src/actions/actions.ts b/src/actions/actions.ts index ac49e4414e6..b3ede3d58a7 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -3837,6 +3837,80 @@ class ActionReplaceCharacter extends BaseCommand { } } +@RegisterAction +class ActionReplaceCharacterVisual extends BaseCommand { + modes = [ModeName.Visual, ModeName.VisualLine]; + keys = ["r", ""]; + runsOnceForEveryCursor() { return false; } + canBeRepeatedWithDot = true; + + public async exec(position: Position, vimState: VimState): Promise { + const toInsert = this.keysPressed[1]; + + let visualSelectionOffset = 1; + let start = vimState.cursorStartPosition; + let end = vimState.cursorPosition; + + // If selection is reversed, reorganize it so that the text replace logic always works + if (end.isBeforeOrEqual(start)) { + [start, end] = [end, start]; + } + + // Limit to not replace EOL + end = new Position(end.line, Math.min(end.character, TextEditor.getLineAt(end).text.length - 1)); + + // Iterate over every line in the current selection + for (var lineNum = start.line; lineNum <= end.line; lineNum++) { + + // Get line of text + const lineText = TextEditor.getLineAt(new Position(lineNum, 0)).text; + + if (start.line === end.line) { + // This is a visual section all on one line, only replace the part within the selection + vimState.recordedState.transformations.push({ + type: "replaceText", + text: Array(end.character - start.character + 2).join(toInsert), + start: start, + end: new Position(end.line, end.character + 1), + manuallySetCursorPositions : true + }); + } else if (lineNum === start.line) { + // This is the first line of the selection so only replace after the cursor + vimState.recordedState.transformations.push({ + type: "replaceText", + text: Array(lineText.length - start.character + 1).join(toInsert), + start: start, + end: new Position(start.line, lineText.length), + manuallySetCursorPositions : true + }); + } else if (lineNum === end.line) { + // This is the last line of the selection so only replace before the cursor + vimState.recordedState.transformations.push({ + type: "replaceText", + text: Array(end.character + 1 + visualSelectionOffset).join(toInsert), + start: new Position(end.line, 0), + end: new Position(end.line, end.character + visualSelectionOffset), + manuallySetCursorPositions : true + }); + } else { + // Replace the entire line length since it is in the middle of the selection + vimState.recordedState.transformations.push({ + type: "replaceText", + text: Array(lineText.length + 1).join(toInsert), + start: new Position(lineNum, 0), + end: new Position(lineNum, lineText.length), + manuallySetCursorPositions : true + }); + } + } + + vimState.cursorPosition = start; + vimState.cursorStartPosition = start; + vimState.currentMode = ModeName.Normal; + return vimState; + } +} + @RegisterAction class ActionReplaceCharacterVisualBlock extends BaseCommand { modes = [ModeName.VisualBlock]; @@ -3846,18 +3920,28 @@ class ActionReplaceCharacterVisualBlock extends BaseCommand { public async exec(position: Position, vimState: VimState): Promise { const toInsert = this.keysPressed[1]; - + let textAtPos = ""; + let newText = ""; for (const { pos } of Position.IterateBlock(vimState.topLeft, vimState.bottomRight)) { + + textAtPos = TextEditor.getText(new vscode.Range(pos, pos.getRight())); + newText = toInsert; + + // If no character at this position, do not replace with anything + if (textAtPos === "") { + newText = ""; + } + vimState.recordedState.transformations.push({ type : "replaceText", - text : toInsert, + text : newText, start : pos, end : pos.getRight(), + manuallySetCursorPositions : true }); } const topLeft = VisualBlockMode.getTopLeftPosition(vimState.cursorPosition, vimState.cursorStartPosition); - vimState.allCursors = [ new Range(topLeft, topLeft) ]; vimState.currentMode = ModeName.Normal; diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index b058db874a3..65cda981674 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -1211,14 +1211,14 @@ export class ModeHandler implements vscode.Disposable { const selections = vscode.window.activeTextEditor.selections; const firstTransformation = transformations[0]; - const manuallySetCursorPositions = firstTransformation.type === "deleteRange" && - firstTransformation.manuallySetCursorPositions; + const manuallySetCursorPositions = ((firstTransformation.type === "deleteRange" || firstTransformation.type === "replaceText") + && firstTransformation.manuallySetCursorPositions); // We handle multiple cursors in a different way in visual block mode, unfortunately. // TODO - refactor that out! if (vimState.currentMode !== ModeName.VisualBlockInsertMode && - vimState.currentMode !== ModeName.VisualBlock && - !manuallySetCursorPositions) { + vimState.currentMode !== ModeName.VisualBlock && + !manuallySetCursorPositions) { vimState.allCursors = []; const resultingCursors: Range[] = []; @@ -1366,6 +1366,9 @@ export class ModeHandler implements vscode.Disposable { Position.EarlierOf(start, stop).getLineBegin(), Position.LaterOf(start, stop).getLineEnd() ) ]; + vimState.cursorStartPosition = selections[0].start as Position; + vimState.cursorPosition = selections[0].end as Position; + } else if (vimState.currentMode === ModeName.VisualBlock) { selections = []; diff --git a/src/motion/position.ts b/src/motion/position.ts index a166d570ee8..1650d96515e 100644 --- a/src/motion/position.ts +++ b/src/motion/position.ts @@ -191,6 +191,41 @@ export class Position extends vscode.Position { } } + /** + * Iterate over every position in the selection defined by the two positions passed in. + */ + public static *IterateSelection(topLeft: Position, bottomRight: Position): Iterable<{ line: string, char: string, pos: Position }> { + for (let lineIndex = topLeft.line; lineIndex <= bottomRight.line; lineIndex++) { + const line = TextEditor.getLineAt(new Position(lineIndex, 0)).text; + + if (lineIndex === topLeft.line) { + for (let charIndex = topLeft.character; charIndex < line.length + 1; charIndex++) { + yield { + line: line, + char: line[charIndex], + pos: new Position(lineIndex, charIndex) + }; + } + } else if (lineIndex === bottomRight.line) { + for (let charIndex = 0; charIndex < bottomRight.character + 1; charIndex++) { + yield { + line: line, + char: line[charIndex], + pos: new Position(lineIndex, charIndex) + }; + } + } else { + for (let charIndex = 0; charIndex < line.length + 1; charIndex++) { + yield { + line: line, + char: line[charIndex], + pos: new Position(lineIndex, charIndex) + }; + } + } + } + } + /** * Iterate over every line in the block defined by the two positions passed in. * diff --git a/src/transformations/transformations.ts b/src/transformations/transformations.ts index 4527fe1fa4c..8b118f9fe9d 100644 --- a/src/transformations/transformations.ts +++ b/src/transformations/transformations.ts @@ -75,6 +75,11 @@ export interface ReplaceTextTransformation { * If you don't know what this is, just ignore it. You probably don't need it. */ diff?: PositionDiff; + + /** + * Please don't use this! It's a hack. + */ + manuallySetCursorPositions?: boolean; } /** diff --git a/test/mode/modeVisual.test.ts b/test/mode/modeVisual.test.ts index d3ba8030076..28fce979aec 100644 --- a/test/mode/modeVisual.test.ts +++ b/test/mode/modeVisual.test.ts @@ -549,4 +549,23 @@ suite("Mode Visual", () => { endMode: ModeName.Normal }); }); + + suite("handles replace in visual mode", () => { + newTest({ + title: "Can do a single line replace", + start: ["one |two three four five"], + keysPressed: "vwwer1", + end: ["one |11111111111111 five"], + endMode: ModeName.Normal + }); + + newTest({ + title: "Can do a multi line replace", + start: ["one |two three four five", "one two three four five"], + keysPressed: "vjer1", + end: ["one |1111111111111111111", "1111111 three four five"], + endMode: ModeName.Normal + }); + }); + }); diff --git a/test/mode/modeVisualBlock.test.ts b/test/mode/modeVisualBlock.test.ts index 8140686cf75..81da665b39d 100644 --- a/test/mode/modeVisualBlock.test.ts +++ b/test/mode/modeVisualBlock.test.ts @@ -56,6 +56,14 @@ suite("Mode Visual Block", () => { end: ['t123est', 't123|est'], }); + newTest({ + title: "Can do a multi line replace", + start: ["one |two three four five", "one two three four five"], + keysPressed: "jeer1", + end: ["one |111111111 four five", "one 111111111 four five"], + endMode: ModeName.Normal + }); + newTest({ title: "Can handle 'D'", start: ['tes|t', 'test'], diff --git a/test/mode/modeVisualLine.test.ts b/test/mode/modeVisualLine.test.ts index 2c685e9370d..f6ce347abb3 100644 --- a/test/mode/modeVisualLine.test.ts +++ b/test/mode/modeVisualLine.test.ts @@ -291,4 +291,23 @@ suite("Mode Visual", () => { end: ['1', '|{', ' a = 1;', '}', '2'] }); }); + + suite("handles replace in visual line mode", () => { + newTest({ + title: "Can do a single line replace", + start: ["one |two three four five", "one two three four five"], + keysPressed: "Vr1", + end: ["|11111111111111111111111", "one two three four five"], + endMode: ModeName.Normal + }); + + newTest({ + title: "Can do a multi visual line replace", + start: ["one |two three four five", "one two three four five"], + keysPressed: "Vjr1", + end: ["|11111111111111111111111", "11111111111111111111111"], + endMode: ModeName.Normal + }); + }); + });