diff --git a/spec/atom/global-keymap-spec.coffee b/spec/atom/global-keymap-spec.coffee index 44dd26e36..98469446b 100644 --- a/spec/atom/global-keymap-spec.coffee +++ b/spec/atom/global-keymap-spec.coffee @@ -43,9 +43,13 @@ describe "GlobalKeymap", -> deleteCharHandler.reset() fragment.removeClass('command-mode').addClass('insert-mode') - keymap.handleKeyEvent(keypressEvent('x', target: fragment[0])) + event = keypressEvent('x', target: fragment[0]) + keymap.handleKeyEvent(event) expect(deleteCharHandler).not.toHaveBeenCalled() expect(insertCharHandler).toHaveBeenCalled() + commandEvent = insertCharHandler.argsForCall[0][0] + expect(commandEvent.keyEvent).toBe event + expect(event.char).toBe 'x' describe "when the event's target node *descends* from a selector with a matching binding", -> it "triggers the command event associated with that binding on the target node and returns false", -> diff --git a/spec/atom/vim-mode-spec.coffee b/spec/atom/vim-mode-spec.coffee index c6f12ce98..f4d6b8ebf 100644 --- a/spec/atom/vim-mode-spec.coffee +++ b/spec/atom/vim-mode-spec.coffee @@ -23,7 +23,7 @@ describe "VimMode", -> expect(editor).toHaveClass 'insert-mode' expect(editor).not.toHaveClass 'command-mode' - fdescribe "the x keybinding", -> + describe "the x keybinding", -> it "deletes a charachter", -> editor.buffer.setText("12345") editor.setCursor(column: 1, row: 0) @@ -33,6 +33,16 @@ describe "VimMode", -> expect(editor.buffer.getText()).toBe '1345' expect(editor.getCursor()).toEqual(column: 1, row: 0) + describe "numeric prefix binding", -> + it "repeats the following operation N times", -> + editor.buffer.setText("12345") + editor.setCursor(column: 1, row: 0) + + editor.trigger keydownEvent('3') + editor.trigger keydownEvent('x') + + expect(editor.buffer.getText()).toBe '15' + describe "insert-mode", -> beforeEach -> editor.trigger keydownEvent('i') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index cdaf7a4a8..e2dcdbefa 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -11,13 +11,17 @@ afterEach -> window.atom = new (require 'app') -window.keypressEvent = (pattern, properties={}) -> +eventPropertiesFromPattern = (pattern) -> bindingSet = new BindingSet("*", {}) - $.Event "keypress", _.extend(bindingSet.parseKeyPattern(pattern), properties) + parsedPattern = bindingSet.parseKeyPattern(pattern) + delete parsedPattern.key # key doesn't exist on browser-generated key events + parsedPattern + +window.keypressEvent = (pattern, properties={}) -> + $.Event "keypress", _.extend(eventPropertiesFromPattern(pattern), properties) window.keydownEvent = (pattern, properties={}) -> - bindingSet = new BindingSet("*", {}) - $.Event "keydown", _.extend(bindingSet.parseKeyPattern(pattern), properties) + $.Event "keydown", _.extend(eventPropertiesFromPattern(pattern), properties) window.waitsForPromise = (fn) -> window.waitsFor (moveOn) -> diff --git a/src/atom/editor.coffee b/src/atom/editor.coffee index d93a45e8b..52795861d 100644 --- a/src/atom/editor.coffee +++ b/src/atom/editor.coffee @@ -61,7 +61,7 @@ class Editor extends Template getCursor: -> @getAceSession().getSelection().getCursor() - delete: -> + deleteChar: -> @aceEditor.remove 'right' diff --git a/src/atom/global-keymap.coffee b/src/atom/global-keymap.coffee index 3f83dff04..0fb263e0c 100644 --- a/src/atom/global-keymap.coffee +++ b/src/atom/global-keymap.coffee @@ -19,10 +19,20 @@ class GlobalKeymap candidateBindingSets.sort (a, b) -> b.specificity - a.specificity for bindingSet in candidateBindingSets if command = bindingSet.commandForEvent(event) - $(event.target).trigger(command) + @triggerCommandEvent(event, command) return false currentNode = currentNode.parent() true reset: -> @BindingSets = [] + + triggerCommandEvent: (keyEvent, commandName) -> + commandEvent = $.Event(commandName) + keyEvent.char = @charForKeyEvent(keyEvent) + commandEvent.keyEvent = keyEvent + $(keyEvent.target).trigger(commandEvent) + + charForKeyEvent: (keyEvent) -> + String.fromCharCode(keyEvent.which).toLowerCase() + diff --git a/src/atom/vim-mode-operators.coffee b/src/atom/vim-mode-operators.coffee new file mode 100644 index 000000000..289819395 --- /dev/null +++ b/src/atom/vim-mode-operators.coffee @@ -0,0 +1,25 @@ +_ = require 'underscore' + +module.exports = + NumericPrefix: class + count: null + operatorToRepeat: null + complete: null + + constructor: (@count) -> + @complete = false + + compose: (@operatorToRepeat) -> + @complete = true + + isComplete: -> @complete + + execute: (editor) -> + _.times @count, => @operatorToRepeat.execute(editor) + + DeleteChar: class + execute: (editor) -> + editor.deleteChar() + + isComplete: -> true + diff --git a/src/atom/vim-mode.coffee b/src/atom/vim-mode.coffee index e01df66cd..e7db89ebe 100644 --- a/src/atom/vim-mode.coffee +++ b/src/atom/vim-mode.coffee @@ -1,20 +1,23 @@ +_ = require 'underscore' +$ = require 'jquery' +{ NumericPrefix, DeleteChar } = require 'vim-mode-operators' + module.exports = class VimMode editor: null + opStack: null constructor: (@editor) -> - atom.bindKeys '.command-mode' - 'i': 'insert-mode:activate' - 'x': 'command-mode:delete' - - atom.bindKeys '.insert-mode' - '': 'command-mode:activate' + @opStack = [] + atom.bindKeys '.command-mode', @commandModeBindings() + atom.bindKeys '.insert-mode', '': 'command-mode:activate' @editor.addClass('command-mode') @editor.on 'insert-mode:activate', => @activateInsertMode() @editor.on 'command-mode:activate', => @activateCommandMode() - @editor.on 'command-mode:delete', => @delete() + @editor.on 'command-mode:delete-char', => @deleteChar() + @editor.on 'command-mode:numeric-prefix', (e) => @numericPrefix(e) activateInsertMode: -> @editor.removeClass('command-mode') @@ -24,5 +27,33 @@ class VimMode @editor.removeClass('insert-mode') @editor.addClass('command-mode') - delete: -> - @editor.delete() + deleteChar: -> + @pushOperator(new DeleteChar) + + numericPrefix: (e) -> + @pushOperator(new NumericPrefix(e.keyEvent.char)) + + commandModeBindings: -> + bindings = + 'i': 'insert-mode:activate' + 'x': 'command-mode:delete-char' + for i in [0..9] + bindings[i] = 'command-mode:numeric-prefix' + bindings + + pushOperator: (op) -> + @opStack.push(op) + @processOpStack() + + processOpStack: -> + return unless @topOperator().isComplete() + poppedOperator = @opStack.pop() + if @opStack.length + @topOperator().compose(poppedOperator) + @processOpStack() + else + poppedOperator.execute(@editor) + + topOperator: -> + _.last @opStack +