diff --git a/spec/atom/command-map-spec.coffee b/spec/atom/command-map-spec.coffee index 7c803a58b..051906185 100644 --- a/spec/atom/command-map-spec.coffee +++ b/spec/atom/command-map-spec.coffee @@ -3,54 +3,105 @@ CommandMap = require 'command-map' describe "CommandMap", -> commandMap = null delegate = null + d = null + a = null + y = null beforeEach -> - delegate = { delete: jasmine.createSpy('delete') } + d = createKeyEvent 'd' + a = createKeyEvent 'a' + y = createKeyEvent 'y' + delegate = { + action1: jasmine.createSpy('action1') + action2: jasmine.createSpy('action2') + } commandMap = new CommandMap delegate describe "handleKeyEvent(event)", -> describe "when there is a single-character mapping to a command method", -> beforeEach -> - commandMap.mapKey 'd', 'delete' + commandMap.mapKey 'd', 'action1' it "calls the named method on the delegate with the given event when the event matches the pattern", -> commandMap.handleKeyEvent createKeyEvent('z') - expect(delegate.delete).not.toHaveBeenCalled() - commandMap.clearBufferedEvents() + expect(delegate.action1).not.toHaveBeenCalled() event = createKeyEvent 'd' commandMap.handleKeyEvent event - expect(delegate.delete).toHaveBeenCalled() + expect(delegate.action1).toHaveBeenCalled() - describe "when there is a multi character mapping to a command method", -> + describe "when there is a multi-character mapping to a command method", -> beforeEach -> - commandMap.mapKey 'dad', 'delete' + commandMap.mapKey 'dad', 'action1' it "calls the named method on the delegate with the given event when the event matches the pattern", -> - event1 = createKeyEvent 'd' - event2 = createKeyEvent 'a' - event3 = createKeyEvent 'd' + commandMap.handleKeyEvent d + expect(delegate.action1).not.toHaveBeenCalled() - commandMap.handleKeyEvent event1 - expect(delegate.delete).not.toHaveBeenCalled() + commandMap.handleKeyEvent a + expect(delegate.action1).not.toHaveBeenCalled() - commandMap.handleKeyEvent event2 - expect(delegate.delete).not.toHaveBeenCalled() + commandMap.handleKeyEvent d + expect(delegate.action1).toHaveBeenCalled() - commandMap.handleKeyEvent event3 - expect(delegate.delete).toHaveBeenCalled() + describe "when there is more than one pattern matching a prefix of key events", -> + inputTimeout = null - describe ".keyEventsMatchPattern(events, pattern)", -> - it "returns true only if the given events match the pattern", -> - event1 = createKeyEvent 'd' - event2 = createKeyEvent 'a' - event3 = createKeyEvent 'd' - events = [event1, event2, event3] + beforeEach -> + commandMap.mapKey 'da', 'action1' + commandMap.mapKey 'dad', 'action2' - expect(commandMap.keyEventsMatchPattern(events, "dad")).toBeTruthy() - expect(commandMap.keyEventsMatchPattern(events, "day")).toBeFalsy() - expect(commandMap.keyEventsMatchPattern(events, "da")).toBeFalsy() + spyOn(window, 'setTimeout').andCallFake (fn) -> + inputTimeout = fn + 'handle' - expect(commandMap.keyEventsMatchPattern([event1], "d")).toBeTruthy() + spyOn(window, 'clearTimeout') + + commandMap.handleKeyEvent d + expect(window.setTimeout).toHaveBeenCalled() + window.setTimeout.reset() + + commandMap.handleKeyEvent a + expect(window.clearTimeout).toHaveBeenCalledWith 'handle' + expect(window.setTimeout).toHaveBeenCalled() + expect(delegate.action1).not.toHaveBeenCalled() + expect(delegate.action2).not.toHaveBeenCalled() + + describe "when no additional key is pressed before the input timeout", -> + it "calls the method for the shorter pattern on the delegate and clears the event buffer", -> + inputTimeout() + expect(delegate.action1).toHaveBeenCalled() + + commandMap.handleKeyEvent d + expect(delegate.action2).not.toHaveBeenCalled() + + describe "when an additional matching key is pressed before the input timeout", -> + it "calls the method for the longer pattern on the delegate, cancels the timeout, and clears the event buffer", -> + commandMap.handleKeyEvent d + expect(window.clearTimeout).toHaveBeenCalledWith 'handle' + expect(delegate.action2).toHaveBeenCalled() + + # ensure the input buffer has been cleared, so we can match da + commandMap.handleKeyEvent d + commandMap.handleKeyEvent a + inputTimeout() + + expect(delegate.action1).toHaveBeenCalled() + + describe ".keyEventsMatchPattern(events, pattern)", -> + it "returns true only if the given events match the given pattern", -> + events = [d, a, d] + + expect(commandMap.keyEventsMatchPattern(events, "dad")).toBeTruthy() + expect(commandMap.keyEventsMatchPattern(events, "day")).toBeFalsy() + expect(commandMap.keyEventsMatchPattern(events, "da")).toBeFalsy() + + expect(commandMap.keyEventsMatchPattern([d], "d")).toBeTruthy() + + describe "keyEventsMatchPatternPrefix(events, pattern)", -> + it "returns true only if the given events match a prefix of the given pattern", -> + expect(commandMap.keyEventsMatchPatternPrefix([d, a], "dad")).toBeTruthy() + expect(commandMap.keyEventsMatchPatternPrefix([d, a], "da")).toBeTruthy() + expect(commandMap.keyEventsMatchPatternPrefix([d, a], "d")).toBeFalsy() + expect(commandMap.keyEventsMatchPatternPrefix([d, a, y], "da")).toBeFalsy() - describe "when there is more than one pattern matching a key event", -> diff --git a/src/atom/command-map.coffee b/src/atom/command-map.coffee index 9032a6838..914b230ee 100644 --- a/src/atom/command-map.coffee +++ b/src/atom/command-map.coffee @@ -20,6 +20,8 @@ class CommandMap ';': 186, '\'': 222, '[': 219, ']': 221, '\\': 220 + inputTimeout: 200 + constructor: (@delegate) -> @mappings = {} @bufferedEvents = [] @@ -28,11 +30,24 @@ class CommandMap @mappings[pattern] = action handleKeyEvent: (event) -> + window.clearTimeout(@inputTimeoutHandle) if @inputTimeoutHandle @bufferedEvents.push(event) + candidatePatterns = + (pattern for pattern of @mappings when @keyEventsMatchPatternPrefix(@bufferedEvents, pattern)) + + if candidatePatterns.length > 1 + @inputTimeoutHandle = _.delay (=> @triggerActionForBufferedKeyEvents()), @inputTimeout + else if candidatePatterns.length == 1 + @triggerActionForBufferedKeyEvents() + else + @clearBufferedEvents() + + triggerActionForBufferedKeyEvents: -> for pattern, action of @mappings if @keyEventsMatchPattern(@bufferedEvents, pattern) @delegate[action](event) + @clearBufferedEvents() keyEventsMatchPattern: (events, pattern) -> patternKeys = @parseKeyPattern(pattern) @@ -40,6 +55,12 @@ class CommandMap _.all(_.zip(events, patternKeys), ([event, pattern]) -> event.which == pattern.which) + keyEventsMatchPatternPrefix: (events, pattern) -> + patternKeys = @parseKeyPattern(pattern) + return false if events.length > patternKeys.length + _.all(_.zip(events, patternKeys[0...events.length]), ([event, pattern]) -> + event.which == pattern.which) + parseKeyPattern: (pattern) -> for char in pattern { which: char.toUpperCase().charCodeAt(0) }