diff --git a/keymaps/vim-mode.cson b/keymaps/vim-mode.cson index 1a58c494..bfc894ee 100644 --- a/keymaps/vim-mode.cson +++ b/keymaps/vim-mode.cson @@ -13,6 +13,12 @@ 'atom-text-editor.vim-mode.insert-mode': 'ctrl-w': 'editor:delete-to-beginning-of-word' 'ctrl-u': 'editor:delete-to-beginning-of-line' + 'left': 'vim-mode:move-left-insert' + 'right': 'vim-mode:move-right-insert' + +'atom-text-editor.vim-mode.insert-mode:not(.autocomplete-active)': + 'up': 'vim-mode:move-up-insert' + 'down': 'vim-mode:move-down-insert' 'atom-text-editor.vim-mode:not(.insert-mode)': 'h': 'vim-mode:move-left' diff --git a/lib/motions/general-motions.coffee b/lib/motions/general-motions.coffee index 47a3cc78..fe9d5215 100644 --- a/lib/motions/general-motions.coffee +++ b/lib/motions/general-motions.coffee @@ -89,7 +89,7 @@ class Motion selection.modifySelection => @moveCursor(selection.cursor, count, options) ensureCursorIsWithinLine: (cursor) -> - return if @vimState.mode is 'visual' or not cursor.selection.isEmpty() + return if @vimState.mode is 'visual' or @vimState.mode is 'insert' or not cursor.selection.isEmpty() {goalColumn} = cursor {row, column} = cursor.getBufferPosition() lastColumn = cursor.getCurrentLineBufferRange().end.column @@ -153,9 +153,10 @@ class MoveRight extends Motion _.times count, => wrapToNextLine = settings.wrapLeftRightMotion() - # when the motion is combined with an operator, we will only wrap to the next line - # if we are already at the end of the line (after the last character) - wrapToNextLine = false if @vimState.mode is 'operator-pending' and not cursor.isAtEndOfLine() + # when the motion is in insert mode or is combined with an operator, + # we will only wrap to the next line if we are already + # at the end of the line (after the last character) + wrapToNextLine = false if (@vimState.mode is 'insert' or @vimState.mode is 'operator-pending') and not cursor.isAtEndOfLine() cursor.moveRight() unless cursor.isAtEndOfLine() cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine() diff --git a/lib/operators/general-operators.coffee b/lib/operators/general-operators.coffee index dd992561..a8bb4865 100644 --- a/lib/operators/general-operators.coffee +++ b/lib/operators/general-operators.coffee @@ -1,6 +1,7 @@ _ = require 'underscore-plus' {Point, Range} = require 'atom' {ViewModel} = require '../view-models/view-model' +Prefixes = require '../prefixes' Utils = require '../utils' settings = require '../settings' @@ -228,11 +229,24 @@ class Repeat extends Operator isRecordable: -> false - execute: (count=1) -> + execute: (count) -> + return unless (cmd = @vimState.history[0])? + @editor.transact => - _.times count, => - cmd = @vimState.history[0] - cmd?.execute() + + if count? + # try to propagate prefix to inner operation + if cmd instanceof Prefixes.Repeat + cmd.count = count + else if cmd.motion instanceof Prefixes.Repeat + cmd.motion.count = count + else + repeat = new Prefixes.Repeat(count) + repeat.compose(cmd) + cmd = @vimState.history[0] = repeat + + cmd.execute() + # # It creates a mark at the current cursor position # diff --git a/lib/operators/input.coffee b/lib/operators/input.coffee index 3e5db4a3..2473ef05 100644 --- a/lib/operators/input.coffee +++ b/lib/operators/input.coffee @@ -8,17 +8,23 @@ settings = require '../settings' # tells the operation to repeat itself instead of enter insert mode. class Insert extends Operator standalone: true + count: 1 isComplete: -> @standalone or super - confirmChanges: (changes) -> + confirmChanges: (changes, insertionCheckpoint, options) -> + interrupted = options?.interrupted bundler = new TransactionBundler(changes) @typedText = bundler.buildInsertText() + if @count > 1 and not interrupted + @editor.insertText(@typedText) for i in [2..@count] - execute: -> + execute: (count) -> + @count = count if count? if @typingCompleted return unless @typedText? and @typedText.length > 0 - @editor.insertText(@typedText, normalizeLineEndings: true) + @editor.transact => + @editor.insertText(@typedText, normalizeLineEndings: true) for i in [1..@count] for cursor in @editor.getCursors() cursor.moveLeft() unless cursor.isAtBeginningOfLine() else @@ -28,6 +34,15 @@ class Insert extends Operator inputOperator: -> true +# an insert operation following cursor motion in insert mode can be cancelled +# and forgotten like it never happened +class InsertCancellable extends Insert + + confirmTransaction: (transaction) -> + super + if @typedText?.length is 0 + @vimState.history.shift() if @vimState.history[0] is this + class InsertAfter extends Insert execute: -> @editor.moveRight() unless @editor.getLastCursor().isAtEndOfLine() @@ -103,7 +118,7 @@ class Change extends Insert for selection in @editor.getSelections() selection.deleteSelectedText() - return super if @typingCompleted + return super(1) if @typingCompleted @vimState.activateInsertMode() @typingCompleted = true @@ -123,7 +138,7 @@ class Substitute extends Insert if @typingCompleted @typedText = @typedText.trimLeft() - return super + return super(1) @vimState.activateInsertMode() @typingCompleted = true @@ -147,7 +162,7 @@ class SubstituteLine extends Insert if @typingCompleted @typedText = @typedText.trimLeft() - return super + return super(1) @vimState.activateInsertMode() @typingCompleted = true @@ -189,6 +204,7 @@ module.exports = { InsertAtBeginningOfLine, InsertAboveWithNewline, InsertBelowWithNewline, + InsertCancellable, Change, Substitute, SubstituteLine diff --git a/lib/vim-state.coffee b/lib/vim-state.coffee index 22109a4b..47deb531 100644 --- a/lib/vim-state.coffee +++ b/lib/vim-state.coffee @@ -103,6 +103,10 @@ class VimState 'move-up': => new Motions.MoveUp(@editor, this) 'move-down': => new Motions.MoveDown(@editor, this) 'move-right': => new Motions.MoveRight(@editor, this) + 'move-left-insert': => @interruptInsertMode(); [new Motions.MoveLeft(@editor, this), new Operators.InsertCancellable(@editor, this)] + 'move-up-insert': => @interruptInsertMode(); [new Motions.MoveUp(@editor, this), new Operators.InsertCancellable(@editor, this)] + 'move-down-insert': => @interruptInsertMode(); [new Motions.MoveDown(@editor, this), new Operators.InsertCancellable(@editor, this)] + 'move-right-insert': => @interruptInsertMode(); [new Motions.MoveRight(@editor, this), new Operators.InsertCancellable(@editor, this)] 'move-to-next-word': => new Motions.MoveToNextWord(@editor, this) 'move-to-next-whole-word': => new Motions.MoveToNextWholeWord(@editor, this) 'move-to-end-of-word': => new Motions.MoveToEndOfWord(@editor, this) @@ -405,15 +409,26 @@ class VimState deactivateInsertMode: -> return unless @mode in [null, 'insert'] @editorElement.component.setInputEnabled(false) - @editor.groupChangesSinceCheckpoint(@insertionCheckpoint) changes = getChangesSinceCheckpoint(@editor.buffer, @insertionCheckpoint) item = @inputOperator(@history[0]) - @insertionCheckpoint = null if item? - item.confirmChanges(changes) + item.confirmChanges(changes, @insertionCheckpoint) + @editor.groupChangesSinceCheckpoint(@insertionCheckpoint) + @insertionCheckpoint = null for cursor in @editor.getCursors() cursor.moveLeft() unless cursor.isAtBeginningOfLine() + interruptInsertMode: -> + return unless @mode is 'insert' + changes = getChangesSinceCheckpoint(@editor.buffer, @insertionCheckpoint) + item = @inputOperator(@history[0]) + if item? + item.confirmChanges(changes, @insertionCheckpoint, interrupted: true) + @editor.groupChangesSinceCheckpoint(@insertionCheckpoint) + @insertionCheckpoint = null + @setInsertionCheckpoint() + + deactivateVisualMode: -> return unless @mode is 'visual' for selection in @editor.getSelections() diff --git a/spec/operators-spec.coffee b/spec/operators-spec.coffee index ea0319fb..b2a80599 100644 --- a/spec/operators-spec.coffee +++ b/spec/operators-spec.coffee @@ -235,6 +235,20 @@ describe "Operators", -> keydown('.') expect(editor.getText()).toBe 'abab' + it "is repeatable with a new count", -> + keydown('3') + keydown('s') + editor.insertText("ab") + keydown('escape') + expect(editor.getText()).toBe '0ab45' + keydown('1') + keydown('.') + expect(editor.getText()).toBe '0aab45' + editor.setCursorScreenPosition([0, 0]) + keydown('3') + keydown('.') + expect(editor.getText()).toBe 'abb45' + it "is undoable", -> editor.setCursorScreenPosition([0, 0]) keydown('3') @@ -1373,7 +1387,7 @@ describe "Operators", -> expect(editor.getText()).toBe "" - it "composes with motions", -> + it "composes with prefix", -> keydown 'd' keydown 'd' keydown '2' @@ -1381,6 +1395,26 @@ describe "Operators", -> expect(editor.getText()).toBe "78" + it "changes previous prefix", -> + keydown '2' + keydown 'd' + keydown 'd' + keydown '1' + keydown '.' + + expect(editor.getText()).toBe "78" + + it "adds prefix if none was there", -> + keydown 'd' + keydown 'd' + keydown 'u' + editor.setCursorScreenPosition([0, 0]) + keydown '2' + keydown '.' + keydown '.' + + expect(editor.getText()).toBe "" + describe "the r keybinding", -> beforeEach -> editor.setText("12\n34\n\n") @@ -1560,6 +1594,29 @@ describe "Operators", -> keydown 'u' expect(editor.getText()).toBe "123\n4567" + it "allows count prefix", -> + keydown '3' + keydown 'i' + editor.insertText("abc") + keydown 'escape' + expect(editor.getText()).toBe "abcabcabc123\nabcabcabc4567" + + keydown 'u' + expect(editor.getText()).toBe "123\n4567" + + keydown '.' + keydown '.' + expect(editor.getText()).toBe "abcabcababcabcabcc123\nabcabcababcabcabcc4567" + + keydown 'u' + expect(editor.getText()).toBe "abcabcabc123\nabcabcabc4567" + + editor.setText('123\n4567') + editor.setCursorBufferPosition([0, 0]) + keydown '2' + keydown '.' + expect(editor.getText()).toBe "abcabc123\n4567" + it "allows repeating typing", -> keydown 'i' editor.insertText("abcXX") @@ -1574,6 +1631,88 @@ describe "Operators", -> keydown '.' expect(editor.getText()).toBe "abababccc123\nabababccc4567" + it "gets prefix from .", -> + keydown 'i' + editor.insertText("abc") + keydown 'escape' + expect(editor.getText()).toBe "abc123\nabc4567" + + keydown '2' + keydown '.' + expect(editor.getText()).toBe "ababcabcc123\nababcabcc4567" + + keydown '.' + expect(editor.getText()).toBe "ababcababcabccc123\nababcababcabccc4567" + + it "stores for repeating only the last batch of characters", -> + keydown '2' + keydown 'i' + editor.insertText("abc") + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + editor.insertText("de") + keydown 'escape' + expect(editor.getText()).toBe "abdec123\nabdec4567" + + keydown '.' + expect(editor.getText()).toBe "abddeec123\nabddeec4567" + + describe "without wrapLeftRightMotion", -> + it "handles right motions correctly", -> + editor.setCursorBufferPosition [0, 0] + keydown 'i' + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 1] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 3] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 3] + editor.insertText "de" + expect(editor.getText()).toBe "123de\n4567" + + it "handles left motions correctly", -> + editor.setCursorBufferPosition [1, 1] + keydown 'i' + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + editor.insertText "de" + expect(editor.getText()).toBe "123\nde4567" + + describe "with wrapLeftRightMotion", -> + beforeEach -> + atom.config.set('vim-mode.wrapLeftRightMotion', true) + + it "handles right motions correctly", -> + editor.setCursorBufferPosition [0, 0] + keydown 'i' + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 1] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 3] + atom.commands.dispatch editorElement, 'vim-mode:move-right-insert' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + editor.insertText "de" + expect(editor.getText()).toBe "123\nde4567" + + it "handles left motions correctly", -> + editor.setCursorBufferPosition [1, 2] + keydown 'i' + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [1, 1] + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 3] + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + editor.insertText "de" + expect(editor.getText()).toBe "12de3\n4567" + describe 'the a keybinding', -> beforeEach -> editor.setText('') @@ -1597,6 +1736,33 @@ describe "Operators", -> expect(editor.getText()).toBe "abcabc" expect(editor.getCursorScreenPosition()).toEqual [0, 5] + it "combines with a prefix", -> + keydown '2' + keydown 'a' + editor.insertText("abc") + keydown 'escape' + expect(editor.getText()).toBe "abcabc" + expect(editor.getCursorScreenPosition()).toEqual [0, 5] + keydown '.' + expect(editor.getText()).toBe "abcabcabcabc" + expect(editor.getCursorScreenPosition()).toEqual [0, 11] + keydown '1' + keydown '.' + expect(editor.getText()).toBe "abcabcabcabcabc" + expect(editor.getCursorScreenPosition()).toEqual [0, 14] + + it "stores for repeating only the last batch of characters, repeats as insert", -> + keydown 'a' + editor.insertText("abc") + atom.commands.dispatch editorElement, 'vim-mode:move-left-insert' + editor.insertText("de") + keydown 'escape' + expect(editor.getText()).toBe "abdec" + expect(editor.getCursorScreenPosition()).toEqual [0, 3] + keydown '.' + expect(editor.getText()).toBe "abddeec" + expect(editor.getCursorScreenPosition()).toEqual [0, 4] + describe "the ctrl-a/ctrl-x keybindings", -> beforeEach -> atom.config.set 'vim-mode.numberRegex', settings.config.numberRegex.default