mirror of
https://github.com/atom/atom.git
synced 2026-01-24 06:18:03 -05:00
WIP: Begin introducing multi-keystroke bindings to Keymap
This commit is contained in:
@@ -33,96 +33,137 @@ describe "Keymap", ->
|
||||
keymap.handleKeyEvent(event)
|
||||
expect(event.keystroke).toBe 'alt-meta-x'
|
||||
|
||||
describe "when no binding matches the event", ->
|
||||
describe "when no binding matches the event's keystroke", ->
|
||||
it "returns true, so the event continues to propagate", ->
|
||||
expect(keymap.handleKeyEvent(keydownEvent('0', target: fragment[0]))).toBeTruthy()
|
||||
|
||||
describe "when the event's target node matches a selector with a matching binding", ->
|
||||
it "triggers the command event associated with that binding on the target node and returns false", ->
|
||||
result = keymap.handleKeyEvent(keydownEvent('x', target: fragment[0]))
|
||||
expect(result).toBe(false)
|
||||
expect(deleteCharHandler).toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
describe "when at least one binding fully matches the event's keystroke", ->
|
||||
describe "when the event's target node matches a selector with a matching binding", ->
|
||||
it "triggers the command event associated with that binding on the target node and returns false", ->
|
||||
result = keymap.handleKeyEvent(keydownEvent('x', target: fragment[0]))
|
||||
expect(result).toBe(false)
|
||||
expect(deleteCharHandler).toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
|
||||
deleteCharHandler.reset()
|
||||
fragment.removeClass('command-mode').addClass('insert-mode')
|
||||
deleteCharHandler.reset()
|
||||
fragment.removeClass('command-mode').addClass('insert-mode')
|
||||
|
||||
event = keydownEvent('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.keystroke).toBe 'x'
|
||||
event = keydownEvent('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.keystroke).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", ->
|
||||
target = fragment.find('.child-node')[0]
|
||||
result = keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(result).toBe(false)
|
||||
expect(deleteCharHandler).toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
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", ->
|
||||
target = fragment.find('.child-node')[0]
|
||||
result = keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(result).toBe(false)
|
||||
expect(deleteCharHandler).toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
|
||||
deleteCharHandler.reset()
|
||||
fragment.removeClass('command-mode').addClass('insert-mode')
|
||||
deleteCharHandler.reset()
|
||||
fragment.removeClass('command-mode').addClass('insert-mode')
|
||||
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(deleteCharHandler).not.toHaveBeenCalled()
|
||||
expect(insertCharHandler).toHaveBeenCalled()
|
||||
|
||||
describe "when the event's target node descends from multiple nodes that match selectors with a binding", ->
|
||||
it "only triggers bindings on selectors associated with the closest ancestor node", ->
|
||||
keymap.bindKeys '.child-node', 'x': 'foo'
|
||||
fooHandler = jasmine.createSpy 'fooHandler'
|
||||
fragment.on 'foo', fooHandler
|
||||
|
||||
target = fragment.find('.grandchild-node')[0]
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(fooHandler).toHaveBeenCalled()
|
||||
expect(deleteCharHandler).not.toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the event bubbles to a node that matches multiple selectors", ->
|
||||
describe "when the matching selectors differ in specificity", ->
|
||||
it "triggers the binding for the most specific selector", ->
|
||||
keymap.bindKeys 'div .child-node', 'x': 'foo'
|
||||
keymap.bindKeys '.command-mode .child-node', 'x': 'baz'
|
||||
keymap.bindKeys '.child-node', 'x': 'bar'
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(deleteCharHandler).not.toHaveBeenCalled()
|
||||
expect(insertCharHandler).toHaveBeenCalled()
|
||||
|
||||
describe "when the event's target node descends from multiple nodes that match selectors with a binding", ->
|
||||
it "only triggers bindings on selectors associated with the closest ancestor node", ->
|
||||
keymap.bindKeys '.child-node', 'x': 'foo'
|
||||
fooHandler = jasmine.createSpy 'fooHandler'
|
||||
barHandler = jasmine.createSpy 'barHandler'
|
||||
bazHandler = jasmine.createSpy 'bazHandler'
|
||||
fragment.on 'foo', fooHandler
|
||||
fragment.on 'bar', barHandler
|
||||
fragment.on 'baz', bazHandler
|
||||
|
||||
target = fragment.find('.grandchild-node')[0]
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(fooHandler).toHaveBeenCalled()
|
||||
expect(deleteCharHandler).not.toHaveBeenCalled()
|
||||
expect(insertCharHandler).not.toHaveBeenCalled()
|
||||
|
||||
expect(fooHandler).not.toHaveBeenCalled()
|
||||
expect(barHandler).not.toHaveBeenCalled()
|
||||
expect(bazHandler).toHaveBeenCalled()
|
||||
describe "when the event bubbles to a node that matches multiple selectors", ->
|
||||
describe "when the matching selectors differ in specificity", ->
|
||||
it "triggers the binding for the most specific selector", ->
|
||||
keymap.bindKeys 'div .child-node', 'x': 'foo'
|
||||
keymap.bindKeys '.command-mode .child-node', 'x': 'baz'
|
||||
keymap.bindKeys '.child-node', 'x': 'bar'
|
||||
|
||||
describe "when the matching selectors have the same specificity", ->
|
||||
it "triggers the bindings for the most recently declared selector", ->
|
||||
keymap.bindKeys '.child-node', 'x': 'foo', 'y': 'baz'
|
||||
keymap.bindKeys '.child-node', 'x': 'bar'
|
||||
fooHandler = jasmine.createSpy 'fooHandler'
|
||||
barHandler = jasmine.createSpy 'barHandler'
|
||||
bazHandler = jasmine.createSpy 'bazHandler'
|
||||
fragment.on 'foo', fooHandler
|
||||
fragment.on 'bar', barHandler
|
||||
fragment.on 'baz', bazHandler
|
||||
|
||||
fooHandler = jasmine.createSpy 'fooHandler'
|
||||
barHandler = jasmine.createSpy 'barHandler'
|
||||
bazHandler = jasmine.createSpy 'bazHandler'
|
||||
fragment.on 'foo', fooHandler
|
||||
fragment.on 'bar', barHandler
|
||||
fragment.on 'baz', bazHandler
|
||||
target = fragment.find('.grandchild-node')[0]
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
|
||||
target = fragment.find('.grandchild-node')[0]
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
expect(fooHandler).not.toHaveBeenCalled()
|
||||
expect(barHandler).not.toHaveBeenCalled()
|
||||
expect(bazHandler).toHaveBeenCalled()
|
||||
|
||||
expect(barHandler).toHaveBeenCalled()
|
||||
expect(fooHandler).not.toHaveBeenCalled()
|
||||
describe "when the matching selectors have the same specificity", ->
|
||||
it "triggers the bindings for the most recently declared selector", ->
|
||||
keymap.bindKeys '.child-node', 'x': 'foo', 'y': 'baz'
|
||||
keymap.bindKeys '.child-node', 'x': 'bar'
|
||||
|
||||
keymap.handleKeyEvent(keydownEvent('y', target: target))
|
||||
expect(bazHandler).toHaveBeenCalled()
|
||||
fooHandler = jasmine.createSpy 'fooHandler'
|
||||
barHandler = jasmine.createSpy 'barHandler'
|
||||
bazHandler = jasmine.createSpy 'bazHandler'
|
||||
fragment.on 'foo', fooHandler
|
||||
fragment.on 'bar', barHandler
|
||||
fragment.on 'baz', bazHandler
|
||||
|
||||
target = fragment.find('.grandchild-node')[0]
|
||||
keymap.handleKeyEvent(keydownEvent('x', target: target))
|
||||
|
||||
expect(barHandler).toHaveBeenCalled()
|
||||
expect(fooHandler).not.toHaveBeenCalled()
|
||||
|
||||
keymap.handleKeyEvent(keydownEvent('y', target: target))
|
||||
expect(bazHandler).toHaveBeenCalled()
|
||||
|
||||
describe "when at least one binding partially matches the event's keystroke", ->
|
||||
beforeEach ->
|
||||
keymap.bindKeys "*",
|
||||
'ctrl-x ctrl-c': 'quit'
|
||||
'ctrl-x 1': 'close-other-windows'
|
||||
|
||||
describe "when the event's target node matches a selector with a partially matching multi-stroke binding", ->
|
||||
describe "when a second keystroke added to the first to match a multi-stroke binding completely", ->
|
||||
fit "triggers the event associated with the matched multi-stroke binding", ->
|
||||
|
||||
quitHandler = jasmine.createSpy('quitHandler')
|
||||
fragment.on 'quit', quitHandler
|
||||
closeOtherWindowsHandler = jasmine.createSpy('closeOtherWindowsHandler')
|
||||
fragment.on 'close-other-windows', closeOtherWindowsHandler
|
||||
|
||||
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment[0], ctrlKey: true))).toBeFalsy()
|
||||
expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0], ctrlKey: true))).toBeFalsy()
|
||||
|
||||
expect(quitHandler).toHaveBeenCalled()
|
||||
expect(closeOtherWindowsHandler).not.toHaveBeenCalled()
|
||||
quitHandler.reset()
|
||||
|
||||
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment[0], ctrlKey: true))).toBeFalsy()
|
||||
expect(keymap.handleKeyEvent(keydownEvent('1', target: fragment[0]))).toBeFalsy()
|
||||
|
||||
expect(quitHandler).not.toHaveBeenCalled()
|
||||
expect(closeOtherWindowsHandler).toHaveBeenCalled()
|
||||
|
||||
describe "when a second keystroke added to the first doesn't match any bindings", ->
|
||||
it "clears the queued keystrokes without triggering any events", ->
|
||||
|
||||
describe "when the event's target node descends from multiple nodes that match selectors with a partial binding match", ->
|
||||
it "allows any of the bindings to be triggered upon a second keystroke"
|
||||
|
||||
describe "when there is a complete binding with a less specific selector", ->
|
||||
it "favors the more specific partial match", ->
|
||||
|
||||
describe "when there is a complete binding with a more specific selector", ->
|
||||
it "favors the more specific complete match", ->
|
||||
|
||||
describe ".bindKeys(selector, fnOrMap)", ->
|
||||
describe "when called with a selector and a hash", ->
|
||||
|
||||
@@ -8,30 +8,39 @@ PEG = require 'pegjs'
|
||||
module.exports =
|
||||
class BindingSet
|
||||
selector: null
|
||||
keystrokeMap: null
|
||||
commandsByKeystrokes: null
|
||||
commandForEvent: null
|
||||
parser: null
|
||||
|
||||
constructor: (@selector, mapOrFunction) ->
|
||||
@parser = PEG.buildParser(fs.read(require.resolve 'keystroke-pattern.pegjs'))
|
||||
@specificity = Specificity(@selector)
|
||||
@keystrokeMap = {}
|
||||
@commandsByKeystrokes = {}
|
||||
|
||||
if _.isFunction(mapOrFunction)
|
||||
@commandForEvent = mapOrFunction
|
||||
else
|
||||
@keystrokeMap = @normalizeKeystrokeMap(mapOrFunction)
|
||||
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(mapOrFunction)
|
||||
@commandForEvent = (event) =>
|
||||
for keystroke, command of @keystrokeMap
|
||||
return command if event.keystroke == keystroke
|
||||
for keystrokes, command of @commandsByKeystrokes
|
||||
return command if event.keystrokes == keystrokes
|
||||
null
|
||||
|
||||
normalizeKeystrokeMap: (keystrokeMap) ->
|
||||
normalizeKeystrokeMap = {}
|
||||
for keystroke, command of keystrokeMap
|
||||
normalizeKeystrokeMap[@normalizeKeystroke(keystroke)] = command
|
||||
matchesKeystrokePrefix: (event) ->
|
||||
for keystrokes, command of @commandsByKeystrokes
|
||||
return true if keystrokes.indexOf(event.keystrokes) == 0
|
||||
false
|
||||
|
||||
normalizeKeystrokeMap
|
||||
normalizeCommandsByKeystrokes: (commandsByKeystrokes) ->
|
||||
normalizedCommandsByKeystrokes = {}
|
||||
for keystrokes, command of commandsByKeystrokes
|
||||
normalizedCommandsByKeystrokes[@normalizeKeystrokes(keystrokes)] = command
|
||||
normalizedCommandsByKeystrokes
|
||||
|
||||
normalizeKeystrokes: (keystrokes) ->
|
||||
normalizedKeystrokes = keystrokes.split(/\s+/).map (keystroke) =>
|
||||
@normalizeKeystroke(keystroke)
|
||||
normalizedKeystrokes.join(' ')
|
||||
|
||||
normalizeKeystroke: (keystroke) ->
|
||||
keys = @parser.parse(keystroke)
|
||||
|
||||
@@ -8,6 +8,7 @@ Specificity = require 'specificity'
|
||||
module.exports =
|
||||
class Keymap
|
||||
bindingSets: null
|
||||
queuedKeystrokes: null
|
||||
|
||||
constructor: ->
|
||||
@bindingSets = []
|
||||
@@ -40,7 +41,8 @@ class Keymap
|
||||
keystrokeMap
|
||||
|
||||
handleKeyEvent: (event) ->
|
||||
event.keystroke = @keystrokeStringForEvent(event)
|
||||
event.keystrokes = @multiKeystrokeStringForEvent(event)
|
||||
@queuedKeystrokes = null
|
||||
currentNode = $(event.target)
|
||||
while currentNode.length
|
||||
candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
|
||||
@@ -52,6 +54,10 @@ class Keymap
|
||||
return false
|
||||
else if command == false
|
||||
return false
|
||||
|
||||
if bindingSet.matchesKeystrokePrefix(event)
|
||||
@queuedKeystrokes = event.keystrokes
|
||||
return false
|
||||
currentNode = currentNode.parent()
|
||||
true
|
||||
|
||||
@@ -60,6 +66,13 @@ class Keymap
|
||||
commandEvent.keyEvent = keyEvent
|
||||
$(keyEvent.target).trigger(commandEvent)
|
||||
|
||||
multiKeystrokeStringForEvent: (event) ->
|
||||
currentKeystroke = @keystrokeStringForEvent(event)
|
||||
if @queuedKeystrokes
|
||||
@queuedKeystrokes + ' ' + currentKeystroke
|
||||
else
|
||||
currentKeystroke
|
||||
|
||||
keystrokeStringForEvent: (event) ->
|
||||
if /^U\+/i.test event.originalEvent.keyIdentifier
|
||||
hexCharCode = event.originalEvent.keyIdentifier.replace(/^U\+/i, '')
|
||||
|
||||
Reference in New Issue
Block a user