diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index e28d037165d..d1acc424ec1 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -178,7 +178,25 @@ export class ModeHandler implements vscode.Disposable { // We shouldn't go to visual mode on any other mode, because the other visual modes are handled // very differently than vscode so only our extension will create them. And the other modes // like the plugin modes shouldn't be changed or else it might mess up the plugins actions. + this._logger.debug('Selections: Creating Multi Cursor Visual Selection!'); + if (e.kind === vscode.TextEditorSelectionChangeKind.Keyboard) { + this.vimState.cursors = e.textEditor.selections.map( + (sel) => + new Range( + Position.FromVSCodePosition(sel.anchor), + sel.anchor.isBefore(sel.active) + ? // This is another case where we are changing our cursor so that the selection looks right + Position.FromVSCodePosition(sel.active).getRight() + : Position.FromVSCodePosition(sel.active) + ) + ); + } await this.setCurrentMode(Mode.Visual); + // Here we need our updateView to draw the selection because we want vscode to include the initial + // character, and by doing it through out updateView we make sure that any resultant selection + // change event will be ignored. + await this.updateView(this.vimState); + return; } return this.updateView(this.vimState); } @@ -189,36 +207,55 @@ export class ModeHandler implements vscode.Disposable { */ if (e.kind !== vscode.TextEditorSelectionChangeKind.Mouse) { if (selection) { - if (e.kind === vscode.TextEditorSelectionChangeKind.Command) { - // This 'Command' kind is triggered when using a command like 'editor.action.smartSelect.grow' - // but it is also triggered when we set the 'editor.selections' on 'updateView'. - if ( - [Mode.Normal, Mode.Visual, Mode.Insert, Mode.Replace].includes( - this.vimState.currentMode - ) - ) { - // Since the selections weren't ignored then probably we got change of selection from - // a command, so we need to update our start and stop positions. This is where commands - // like 'editor.action.smartSelect.grow' are handled. - if (this.vimState.currentMode === Mode.Visual) { - this._logger.debug('Selections: Updating Visual Selection!'); - this.vimState.cursorStopPosition = Position.FromVSCodePosition(selection.active); - this.vimState.cursorStartPosition = Position.FromVSCodePosition(selection.anchor); - await this.updateView(this.vimState, { drawSelection: false, revealRange: false }); - return; - } else if (!selection.active.isEqual(selection.anchor)) { - this._logger.debug('Selections: Creating Visual Selection from command!'); - this.vimState.cursorStopPosition = Position.FromVSCodePosition(selection.active); - this.vimState.cursorStartPosition = Position.FromVSCodePosition(selection.anchor); - await this.setCurrentMode(Mode.Visual); - await this.updateView(this.vimState, { drawSelection: false, revealRange: false }); - return; + if ( + [Mode.Normal, Mode.Visual, Mode.Insert, Mode.Replace].includes(this.vimState.currentMode) + ) { + // Since the selections weren't ignored then probably we got change of selection from + // a command, so we need to update our start and stop positions. This is where commands + // like 'editor.action.smartSelect.grow' are handled. + if (this.vimState.currentMode === Mode.Visual) { + this._logger.debug('Selections: Updating Visual Selection!'); + const active = Position.FromVSCodePosition(selection.active); + const anchor = Position.FromVSCodePosition(selection.anchor); + this.vimState.cursorStopPosition = active; + this.vimState.cursorStartPosition = anchor; + if (e.kind === vscode.TextEditorSelectionChangeKind.Keyboard) { + if (selection.anchor.isAfter(selection.active)) { + this.vimState.cursorStartPosition = anchor.getLeft(); + } + } + // Here we need our updateView to draw the selection because even though we don't need to + // change the vscode selection, in some situations when you trigger an initial selection + // and then update that selection these events will be triggered only after vscode has done + // both selection changes and in that situation we will get all the events in a row and since + // when creating the visual selection the first time (the code block below this) we change + // the selection, then we need to change the selection on the updates as well or else we + // would revert the vscode selection back to the initial one. (This issue is noticeable when + // using the VSpaceCode extension that shows a QuickPick menu that allows you to call commands + // that change the selection in vscode, but we will only get this 'handleSelectionChange' + // function called with all those changes after the user closes the QuickPick menu) + await this.updateView(this.vimState); + return; + } else if (!selection.active.isEqual(selection.anchor)) { + this._logger.debug('Selections: Creating Visual Selection from command!'); + const active = Position.FromVSCodePosition(selection.active); + const anchor = Position.FromVSCodePosition(selection.anchor); + this.vimState.cursorStopPosition = active; + this.vimState.cursorStartPosition = anchor; + if (e.kind === vscode.TextEditorSelectionChangeKind.Keyboard) { + if (selection.anchor.isBefore(selection.active)) { + // This is another case where we are changing our cursor so that the selection looks right + this.vimState.cursorStopPosition = active.getRight(); + } } + await this.setCurrentMode(Mode.Visual); + // Here we need our updateView to draw the selection because we want vscode to include the + // initial character, and by doing it through out updateView we make sure that any resultant + // selection change event will be ignored. + await this.updateView(this.vimState); + return; } } - // Here we are on the selection changed of kind 'Keyboard' or 'undefined' which is triggered - // when pressing movement keys that are not caught on the 'type' override but also when using - // commands like 'cursorMove'. if (isVisualMode(this.vimState.currentMode)) { /** diff --git a/test/mode/modeVisual.test.ts b/test/mode/modeVisual.test.ts index 305f6373b71..349fd27201d 100644 --- a/test/mode/modeVisual.test.ts +++ b/test/mode/modeVisual.test.ts @@ -1667,4 +1667,426 @@ suite('Mode Visual', () => { endMode: Mode.Normal, }); }); + + suite('Visual mode with commands cursorRightSelect, cursorLeftSelect in Insert Mode', () => { + newTest({ + title: 'Command cursorRightSelect enters visual mode', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorRightSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'id', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|re": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorLeftSelect enters visual mode', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'id', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "b|ore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorUpSelect enters visual mode', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorUpSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'id', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {|ore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorDownSelect enters visual mode', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorDownSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'id', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|er": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorRightSelect enters visual mode with multi cursors', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorRightSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "before": ["|j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'vlgb_lllid', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|re": ["j"],`, + ` "afr": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorLeftSelect enters visual mode with multi cursors', + config: { + insertModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "before": ["|j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'vlgb_lllid', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "b|ore": ["j"],`, + ` "aer": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + }); + + suite('Visual mode with commands cursorRightSelect, cursorLeftSelect in Normal Mode', () => { + newTest({ + title: 'Command cursorRightSelect enters visual mode', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorRightSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|re": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorLeftSelect enters visual mode', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "b|ore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorUpSelect enters visual mode', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorUpSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {|ore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorDownSelect enters visual mode', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorDownSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|er": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorRightSelect enters visual mode with multi cursors', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorRightSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "before": ["|j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'vlgb_llld', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|re": ["j"],`, + ` "afr": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: 'Command cursorLeftSelect enters visual mode with multi cursors', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "before": ["|j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'vlgb_llld', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "b|ore": ["j"],`, + ` "aer": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: + 'Command cursorLeftSelect enters visual mode and next movement keeps anchor in right place', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "be|fore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'hd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "|ore": ["j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + + newTest({ + title: + 'Command cursorLeftSelect on multi cursors enters visual mode and next movement keeps anchor in right place', + config: { + normalModeKeyBindings: [ + { + before: [''], + commands: ['cursorLeftSelect'], + }, + ], + }, + start: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "before": ["|j"],`, + ` "after": ["g", "j"],`, + ` },`, + `]`, + ], + keysPressed: 'vlgb_lllhd', + end: [ + `"vim.normalModeKeyBindingsNonRecursive": [`, + ` {`, + ` "|ore": ["j"],`, + ` "er": ["g", "j"],`, + ` },`, + `]`, + ], + endMode: Mode.Normal, + }); + }); });