From 2c2423f9857ee50b859e88f7d2f323f626ec3507 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Jan 2012 22:02:47 -0800 Subject: [PATCH] Single-digit numeric prefixes can repeat commands in vim-mode Vim mode has an operator stack. Every time an operator is pushed to the stack, we ask if it is complete. If it's complete, we compose it with the operator below it, then pop that operator if its complete. When no operators remain on the stack, we call execute the final composed operator. So far we only have DeleteChar (x) and NumericPrefix operators. --- spec/atom/global-keymap-spec.coffee | 6 +++- spec/atom/vim-mode-spec.coffee | 12 ++++++- spec/spec-helper.coffee | 12 ++++--- src/atom/editor.coffee | 2 +- src/atom/global-keymap.coffee | 12 ++++++- src/atom/vim-mode-operators.coffee | 25 +++++++++++++++ src/atom/vim-mode.coffee | 49 +++++++++++++++++++++++------ 7 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 src/atom/vim-mode-operators.coffee 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 +