Merge pull request #1119 from atom/cj-keymap-cleanup

Keymap cleanup
This commit is contained in:
Corey Johnson
2013-11-15 14:42:20 -08:00
13 changed files with 233 additions and 381 deletions

View File

@@ -13,7 +13,7 @@
"dependencies": {
"async": "0.2.6",
"bootstrap": "git://github.com/twbs/bootstrap.git#v3.0.0",
"clear-cut": "0.1.0",
"clear-cut": "0.2.0",
"coffee-script": "1.6.3",
"coffeestack": "0.6.0",
"emissary": "0.17.0",
@@ -78,7 +78,7 @@
"bookmarks": "0.10.0",
"bracket-matcher": "0.11.0",
"command-logger": "0.6.0",
"command-palette": "0.7.0",
"command-palette": "0.8.0",
"dev-live-reload": "0.15.0",
"editor-stats": "0.5.0",
"exception-reporting": "0.7.0",
@@ -90,12 +90,13 @@
"go-to-line": "0.8.0",
"grammar-selector": "0.8.0",
"image-view": "0.7.0",
"keybinding-resolver": "0.2.0",
"link": "0.7.0",
"markdown-preview": "0.15.0",
"metrics": "0.11.0",
"package-generator": "0.19.0",
"release-notes": "0.11.0",
"settings-view": "0.39.0",
"settings-view": "0.41.0",
"snippets": "0.13.0",
"spell-check": "0.13.0",
"status-bar": "0.16.0",

View File

@@ -137,28 +137,28 @@ describe "the `atom` global", ->
element2 = $$ -> @div class: 'test-2'
element3 = $$ -> @div class: 'test-3'
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement(element2)['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element2)).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element3)).toHaveLength 0
atom.activatePackage("package-with-keymaps")
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBe "test-1"
expect(atom.keymap.bindingsForElement(element2)['ctrl-z']).toBe "test-2"
expect(atom.keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)[0].command).toBe "test-1"
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element2)[0].command).toBe "test-2"
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element3)).toHaveLength 0
describe "when the metadata contains a 'keymaps' manifest", ->
it "loads only the keymaps specified by the manifest, in the specified order", ->
element1 = $$ -> @div class: 'test-1'
element3 = $$ -> @div class: 'test-3'
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)).toHaveLength 0
atom.activatePackage("package-with-keymaps-manifest")
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBe 'keymap-1'
expect(atom.keymap.bindingsForElement(element1)['ctrl-n']).toBe 'keymap-2'
expect(atom.keymap.bindingsForElement(element3)['ctrl-y']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)[0].command).toBe 'keymap-1'
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-n', element1)[0].command).toBe 'keymap-2'
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-y', element3)).toHaveLength 0
describe "menu loading", ->
beforeEach ->
@@ -317,8 +317,8 @@ describe "the `atom` global", ->
it "removes the package's keymaps", ->
atom.activatePackage('package-with-keymaps')
atom.deactivatePackage('package-with-keymaps')
expect(atom.keymap.bindingsForElement($$ -> @div class: 'test-1')['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement($$ -> @div class: 'test-2')['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-1')).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-2')).toHaveLength 0
it "removes the package's stylesheets", ->
atom.activatePackage('package-with-stylesheets')

View File

@@ -2719,7 +2719,7 @@ describe "Editor", ->
describe "when the escape key is pressed on the editor", ->
it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", ->
keymap.bindKeys '.editor', 'escape': 'test-event'
keymap.bindKeys 'name', '.editor', 'escape': 'test-event'
testEventHandler = jasmine.createSpy("testEventHandler")
editor.on 'test-event', testEventHandler

View File

@@ -21,25 +21,19 @@ describe "Keymap", ->
describe ".handleKeyEvent(event)", ->
deleteCharHandler = null
insertCharHandler = null
metaZHandler = null
beforeEach ->
keymap.bindKeys '.command-mode', 'x': 'deleteChar'
keymap.bindKeys '.insert-mode', 'x': 'insertChar'
keymap.bindKeys 'name', '.command-mode', 'x': 'deleteChar'
keymap.bindKeys 'name', '.insert-mode', 'x': 'insertChar'
keymap.bindKeys 'name', '.command-mode', 'meta-z': 'metaZPressed'
deleteCharHandler = jasmine.createSpy('deleteCharHandler')
insertCharHandler = jasmine.createSpy('insertCharHandler')
metaZHandler = jasmine.createSpy('metaZHandler')
fragment.on 'deleteChar', deleteCharHandler
fragment.on 'insertChar', insertCharHandler
it "adds a 'keystrokes' string to the event object", ->
event = keydownEvent('x', altKey: true, metaKey: true)
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'alt-meta-x'
event = keydownEvent(',', metaKey: true)
event.which = 188
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'meta-,'
fragment.on 'metaZPressed', metaZHandler
describe "when no binding matches the event's keystroke", ->
it "does not return false so the event continues to propagate", ->
@@ -47,10 +41,11 @@ describe "Keymap", ->
describe "when a non-English keyboard language is used", ->
it "uses the physical character pressed instead of the character it maps to in the current language", ->
event = keydownEvent('U+03B6', metaKey: true) # This is the 'z' key using the Greek keyboard layout
event.which = 122
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'meta-z'
event = keydownEvent('U+03B6', metaKey: true, which: 122, target: fragment[0]) # This is the 'z' key using the Greek keyboard layout
result = keymap.handleKeyEvent(event)
expect(result).toBe(false)
expect(metaZHandler).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", ->
@@ -67,9 +62,6 @@ describe "Keymap", ->
keymap.handleKeyEvent(event)
expect(deleteCharHandler).not.toHaveBeenCalled()
expect(insertCharHandler).toHaveBeenCalled()
commandEvent = insertCharHandler.argsForCall[0][0]
expect(commandEvent.keyEvent).toBe event
expect(event.keystrokes).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", ->
@@ -88,7 +80,7 @@ describe "Keymap", ->
describe "when the event's target node descends from multiple nodes that match selectors with a binding", ->
beforeEach ->
keymap.bindKeys '.child-node', 'x': 'foo'
keymap.bindKeys 'name', '.child-node', 'x': 'foo'
it "only triggers bindings on selectors associated with the closest ancestor node", ->
fooHandler = jasmine.createSpy 'fooHandler'
@@ -125,10 +117,10 @@ describe "Keymap", ->
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 !important', 'x': 'baz'
keymap.bindKeys '.command-mode .child-node', 'x': 'quux'
keymap.bindKeys '.child-node', 'x': 'bar'
keymap.bindKeys 'name', 'div .child-node', 'x': 'foo'
keymap.bindKeys 'name', '.command-mode .child-node !important', 'x': 'baz'
keymap.bindKeys 'name', '.command-mode .child-node', 'x': 'quux'
keymap.bindKeys 'name', '.child-node', 'x': 'bar'
fooHandler = jasmine.createSpy 'fooHandler'
barHandler = jasmine.createSpy 'barHandler'
@@ -146,8 +138,8 @@ describe "Keymap", ->
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.bindKeys 'name', '.child-node', 'x': 'foo', 'y': 'baz'
keymap.bindKeys 'name', '.child-node', 'x': 'bar'
fooHandler = jasmine.createSpy 'fooHandler'
barHandler = jasmine.createSpy 'barHandler'
@@ -168,7 +160,8 @@ describe "Keymap", ->
describe "when the event's target is the document body", ->
it "triggers the mapped event on the rootView", ->
window.rootView = new RootView
keymap.bindKeys 'body', 'x': 'foo'
rootView.attachToDom()
keymap.bindKeys 'name', 'body', 'x': 'foo'
fooHandler = jasmine.createSpy("fooHandler")
rootView.on 'foo', fooHandler
@@ -180,7 +173,7 @@ describe "Keymap", ->
describe "when the event matches a 'native!' binding", ->
it "returns true, allowing the browser's native key handling to process the event", ->
keymap.bindKeys '.grandchild-node', 'x': 'native!'
keymap.bindKeys 'name', '.grandchild-node', 'x': 'native!'
nativeHandler = jasmine.createSpy("nativeHandler")
fragment.on 'native!', nativeHandler
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment.find('.grandchild-node')[0]))).toBe true
@@ -190,7 +183,7 @@ describe "Keymap", ->
[quitHandler, closeOtherWindowsHandler] = []
beforeEach ->
keymap.bindKeys "*",
keymap.bindKeys 'name', "*",
'ctrl-x ctrl-c': 'quit'
'ctrl-x 1': 'close-other-windows'
@@ -220,7 +213,7 @@ describe "Keymap", ->
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", ->
it "clears the queued keystroke without triggering any events", ->
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment[0], ctrlKey: true))).toBe false
expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).toBe false
expect(quitHandler).not.toHaveBeenCalled()
@@ -230,7 +223,7 @@ describe "Keymap", ->
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, favoring the most specific selector", ->
keymap.bindKeys ".grandchild-node", 'ctrl-x ctrl-c': 'more-specific-quit'
keymap.bindKeys 'name', ".grandchild-node", 'ctrl-x ctrl-c': 'more-specific-quit'
grandchildNode = fragment.find('.grandchild-node')[0]
moreSpecificQuitHandler = jasmine.createSpy('moreSpecificQuitHandler')
fragment.on 'more-specific-quit', moreSpecificQuitHandler
@@ -254,39 +247,17 @@ describe "Keymap", ->
describe "when there is a complete binding with a more specific selector", ->
it "favors the more specific complete match", ->
describe "when a tab keystroke does not match any bindings", ->
it "returns false to prevent the browser from transferring focus", ->
expect(keymap.handleKeyEvent(keydownEvent('U+0009', target: fragment[0]))).toBe false
describe ".keystrokesByCommandForSelector(selector)", ->
it "returns a hash of all commands and their keybindings", ->
keymap.bindKeys 'body', 'a': 'letter'
keymap.bindKeys '.editor', 'b': 'letter'
keymap.bindKeys '.editor', '1': 'number'
keymap.bindKeys '.editor', 'meta-alt-1': 'number-with-modifiers'
expect(keymap.keystrokesByCommandForSelector()).toEqual
'letter': ['b', 'a']
'number': ['1']
'number-with-modifiers': ['alt-meta-1']
expect(keymap.keystrokesByCommandForSelector('.editor')).toEqual
'letter': ['b']
'number': ['1']
'number-with-modifiers': ['alt-meta-1']
describe ".bindKeys(selector, bindings)", ->
describe ".bindKeys(name, selector, bindings)", ->
it "normalizes the key patterns in the hash to put the modifiers in alphabetical order", ->
fooHandler = jasmine.createSpy('fooHandler')
fragment.on 'foo', fooHandler
keymap.bindKeys '*', 'ctrl-alt-delete': 'foo'
keymap.bindKeys 'name', '*', 'ctrl-alt-delete': 'foo'
result = keymap.handleKeyEvent(keydownEvent('delete', ctrlKey: true, altKey: true, target: fragment[0]))
expect(result).toBe(false)
expect(fooHandler).toHaveBeenCalled()
fooHandler.reset()
keymap.bindKeys '*', 'ctrl-alt--': 'foo'
keymap.bindKeys 'name', '*', 'ctrl-alt--': 'foo'
result = keymap.handleKeyEvent(keydownEvent('-', ctrlKey: true, altKey: true, target: fragment[0]))
expect(result).toBe(false)
expect(fooHandler).toHaveBeenCalled()
@@ -299,15 +270,17 @@ describe "Keymap", ->
'.brown':
'ctrl-h': 'harvest'
expect(keymap.bindingsForElement($$ -> @div class: 'green')).toEqual { 'ctrl-c': 'cultivate' }
expect(keymap.bindingsForElement($$ -> @div class: 'brown')).toEqual { 'ctrl-h': 'harvest' }
keymap.add 'medical',
'.green':
'ctrl-v': 'vomit'
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'green')).toHaveLength 2
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'brown')).toHaveLength 1
keymap.remove('nature')
expect(keymap.bindingsForElement($$ -> @div class: 'green')).toEqual {}
expect(keymap.bindingsForElement($$ -> @div class: 'brown')).toEqual {}
expect(keymap.bindingSetsByFirstKeystroke['ctrl-c']).toEqual []
expect(keymap.bindingSetsByFirstKeystroke['ctrl-h']).toEqual []
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'green')).toHaveLength 1
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'brown')).toEqual []
describe ".keystrokeStringForEvent(event)", ->
describe "when no modifiers are pressed", ->
@@ -332,54 +305,22 @@ describe "Keymap", ->
expect(keymap.keystrokeStringForEvent(keydownEvent('left', shiftKey: true))).toBe 'shift-left'
expect(keymap.keystrokeStringForEvent(keydownEvent('Left', shiftKey: true))).toBe 'shift-left'
describe ".bindingsForElement(element)", ->
describe ".keyBindingsMatchingElement(element)", ->
it "returns the matching bindings for the element", ->
keymap.bindKeys '.command-mode', 'c': 'c'
keymap.bindKeys '.grandchild-node', 'g': 'g'
keymap.bindKeys 'name', '.command-mode', 'c': 'c'
keymap.bindKeys 'name', '.grandchild-node', 'g': 'g'
bindings = keymap.bindingsForElement(fragment.find('.grandchild-node'))
expect(Object.keys(bindings).length).toBe 2
expect(bindings['c']).toEqual "c"
expect(bindings['g']).toEqual "g"
bindings = keymap.keyBindingsMatchingElement(fragment.find('.grandchild-node'))
expect(bindings).toHaveLength 2
expect(bindings[0].command).toEqual "g"
expect(bindings[1].command).toEqual "c"
describe "when multiple bindings match a keystroke", ->
it "only returns bindings that match the most specific selector", ->
keymap.bindKeys '.command-mode', 'g': 'command-mode'
keymap.bindKeys '.command-mode .grandchild-node', 'g': 'command-and-grandchild-node'
keymap.bindKeys '.grandchild-node', 'g': 'grandchild-node'
keymap.bindKeys 'name', '.command-mode', 'g': 'command-mode'
keymap.bindKeys 'name', '.command-mode .grandchild-node', 'g': 'command-and-grandchild-node'
keymap.bindKeys 'name', '.grandchild-node', 'g': 'grandchild-node'
bindings = keymap.bindingsForElement(fragment.find('.grandchild-node'))
expect(Object.keys(bindings).length).toBe 1
expect(bindings['g']).toEqual "command-and-grandchild-node"
describe ".getAllKeyMappings", ->
it "returns the all bindings", ->
keymap.bindKeys path.join('~', '.atom', 'packages', 'dummy', 'keymaps', 'a.cson'), '.command-mode', 'k': 'c'
mappings = keymap.getAllKeyMappings()
expect(mappings.length).toBe 1
expect(mappings[0].source).toEqual 'dummy'
expect(mappings[0].keystrokes).toEqual 'k'
expect(mappings[0].command).toEqual 'c'
expect(mappings[0].selector).toEqual '.command-mode'
describe ".determineSource", ->
describe "for a package", ->
it "returns '<package-name>'", ->
expect(keymap.determineSource(path.join('~', '.atom', 'packages', 'dummy', 'keymaps', 'a.cson'))).toEqual 'dummy'
describe "for a linked package", ->
it "returns '<package-name>'", ->
expect(keymap.determineSource(path.join('Users', 'john', 'github', 'dummy', 'keymaps', 'a.cson'))).toEqual 'dummy'
describe "for a user defined keymap", ->
it "returns 'User'", ->
expect(keymap.determineSource(path.join('~', '.atom', 'keymaps', 'a.cson'))).toEqual 'User'
describe "for a core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource(path.join('Applications', 'Atom.app', '..', 'node_modules', 'dummy', 'keymaps', 'a.cson'))).toEqual 'Core'
describe "for a linked core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource(path.join('Users', 'john', 'github', 'atom', 'keymaps', 'a.cson'))).toEqual 'Core'
bindings = keymap.keyBindingsMatchingElement(fragment.find('.grandchild-node'))
expect(bindings).toHaveLength 3
expect(bindings[0].command).toEqual "command-and-grandchild-node"

View File

@@ -137,7 +137,7 @@ describe "RootView", ->
commandHandler = jasmine.createSpy('commandHandler')
rootView.on('foo-command', commandHandler)
atom.keymap.bindKeys('*', 'x': 'foo-command')
atom.keymap.bindKeys('name', '*', 'x': 'foo-command')
describe "when a keydown event is triggered in the RootView", ->
it "triggers matching keybindings for that event", ->

View File

@@ -23,7 +23,7 @@ atom.themes.requireStylesheet '../static/jasmine'
fixturePackagesPath = path.resolve(__dirname, './fixtures/packages')
atom.packages.packageDirPaths.unshift(fixturePackagesPath)
atom.keymap.loadBundledKeymaps()
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
keyBindingsToRestore = atom.keymap.getKeyBindings()
$(window).on 'core:close', -> window.close()
$(window).on 'unload', ->
@@ -50,6 +50,7 @@ beforeEach ->
projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures')
atom.project = atom.getWindowState().set('project', new Project(path: projectPath))
window.project = atom.project
atom.keymap.keyBindings = _.clone(keyBindingsToRestore)
window.resetTimeouts()
atom.packages.packageStates = {}
@@ -64,10 +65,6 @@ beforeEach ->
resolvePackagePath(packageName)
resolvePackagePath = _.bind(spy.originalValue, atom.packages)
# used to reset keymap after each spec
bindingSetsToRestore = _.clone(atom.keymap.bindingSets)
bindingSetsByFirstKeystrokeToRestore = _.clone(atom.keymap.bindingSetsByFirstKeystroke)
# prevent specs from modifying Atom's menus
spyOn(atom.menu, 'sendToBrowserProcess')
@@ -106,8 +103,6 @@ beforeEach ->
addCustomMatchers(this)
afterEach ->
atom.keymap.bindingSets = bindingSetsToRestore
atom.keymap.bindingSetsByFirstKeystroke = bindingSetsByFirstKeystrokeToRestore
atom.deactivatePackages()
atom.menu.template = []

View File

@@ -1,66 +0,0 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
PEG = require 'pegjs'
### Internal ###
module.exports =
class BindingSet
@parser: null
selector: null
commandsByKeystrokes: null
parser: null
name: null
constructor: (selector, commandsByKeystrokes, @index, @name) ->
keystrokePattern = fs.readFileSync(require.resolve('./keystroke-pattern.pegjs'), 'utf8')
BindingSet.parser ?= PEG.buildParser(keystrokePattern)
@specificity = specificity(selector)
@selector = selector.replace(/!important/g, '')
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes)
# Private:
getName: ->
@name
# Private:
getSelector: ->
@selector
# Private:
getCommandsByKeystrokes: ->
@commandsByKeystrokes
commandForEvent: (event) ->
for keystrokes, command of @commandsByKeystrokes
return command if event.keystrokes == keystrokes
null
matchesKeystrokePrefix: (event) ->
eventKeystrokes = event.keystrokes.split(' ')
for keystrokes, command of @commandsByKeystrokes
bindingKeystrokes = keystrokes.split(' ')
continue unless eventKeystrokes.length < bindingKeystrokes.length
return true if _.isEqual(eventKeystrokes, bindingKeystrokes[0...eventKeystrokes.length])
false
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 = BindingSet.parser.parse(keystroke)
modifiers = keys[0...-1]
modifiers.sort()
[modifiers..., _.last(keys)].join('-')

View File

@@ -22,7 +22,7 @@ class ApplicationMenu
# The Object which describes the menu to display.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
update: (template, keystrokesByCommand) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@@ -97,14 +97,14 @@ class ApplicationMenu
]
]
# Private: Combines a menu template with the appropriate keystrokes.
# Private: Combines a menu template with the appropriate keystroke.
#
# * template:
# An Object conforming to atom-shell's menu api but lacking accelerator and
# click properties.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
#
# Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate: (template, keystrokesByCommand) ->
@@ -123,15 +123,15 @@ class ApplicationMenu
# The name of the command.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by atom shell to provide nice icons where available.
acceleratorForCommand: (command, keystrokesByCommand) ->
keystroke = keystrokesByCommand[command]?[0]
return null unless keystroke
firstKeystroke = keystrokesByCommand[command]?[0]
return null unless firstKeystroke
modifiers = keystroke.split('-')
modifiers = firstKeystroke.split('-')
key = modifiers.pop()
modifiers.push("Shift") if key != key.toLowerCase()

View File

@@ -1806,13 +1806,6 @@ class Editor extends View
else
'&nbsp;'
bindToKeyedEvent: (key, event, callback) ->
binding = {}
binding[key] = event
atom.keymap.bindKeys '.editor', binding
@on event, =>
callback(this, event)
replaceSelectedText: (replaceFn) ->
selection = @getSelection()
return false if selection.isEmpty()

48
src/key-binding.coffee Normal file
View File

@@ -0,0 +1,48 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
PEG = require 'pegjs'
### Internal ###
module.exports =
class KeyBinding
@parser: null
@currentIndex: 1
@normalizeKeystroke: (keystroke) ->
normalizedKeystroke = keystroke.split(/\s+/).map (keystroke) =>
keys = @getParser().parse(keystroke)
modifiers = keys[0...-1]
modifiers.sort()
[modifiers..., _.last(keys)].join('-')
normalizedKeystroke.join(' ')
@getParser: ->
if not KeyBinding.parser
keystrokePattern = fs.readFileSync(require.resolve('./keystroke-pattern.pegjs'), 'utf8')
KeyBinding.parser = PEG.buildParser(keystrokePattern)
KeyBinding.parser
constructor: (source, command, keystroke, selector) ->
@source = source
@command = command
@keystroke = KeyBinding.normalizeKeystroke(keystroke)
@selector = selector.replace(/!important/g, '')
@specificity = specificity(selector)
@index = KeyBinding.currentIndex++
matches: (keystroke) ->
multiKeystroke = /\s/.test keystroke
if multiKeystroke
keystroke == @keystroke
else
keystroke.split(' ')[0] == @keystroke.split(' ')[0]
compare: (keyBinding) ->
if keyBinding.specificity == @specificity
keyBinding.index - @index
else
keyBinding.specificity - @specificity

View File

@@ -3,7 +3,7 @@ _ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
CSON = require 'season'
BindingSet = require './binding-set'
KeyBinding = require './key-binding'
{Emitter} = require 'emissary'
Modifiers = ['alt', 'control', 'ctrl', 'shift', 'meta']
@@ -25,166 +25,46 @@ module.exports =
class Keymap
Emitter.includeInto(this)
bindingSets: null
nextBindingSetIndex: 0
bindingSetsByFirstKeystroke: null
queuedKeystrokes: null
constructor: ({@resourcePath, @configDirPath})->
@bindingSets = []
@bindingSetsByFirstKeystroke = {}
@keyBindings = []
loadBundledKeymaps: ->
@loadDirectory(path.join(@resourcePath, 'keymaps'))
@emit('bundled-keymaps-loaded')
# Public: Returns an array of all {KeyBinding}s.
getKeyBindings: ->
_.clone(@keyBindings)
loadUserKeymap: ->
userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
@load(userKeymapPath) if userKeymapPath
loadDirectory: (directoryPath) ->
@load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])
load: (path) ->
@add(path, CSON.readFileSync(path))
add: (args...) ->
name = args.shift() if args.length > 1
keymap = args.shift()
for selector, bindings of keymap
@bindKeys(name, selector, bindings)
remove: (name) ->
for bindingSet in @bindingSets.filter((bindingSet) -> bindingSet.name is name)
_.remove(@bindingSets, bindingSet)
for keystrokes of bindingSet.commandsByKeystrokes
keystroke = keystrokes.split(' ')[0]
_.remove(@bindingSetsByFirstKeystroke[keystroke], bindingSet)
# Public: Returns an array of objects that represent every keystroke to
# command mapping. Each object contains the following keys `source`,
# `selector`, `command`, `keystrokes`.
getAllKeyMappings: ->
mappings = []
for bindingSet in @bindingSets
selector = bindingSet.getSelector()
source = @determineSource(bindingSet.getName())
for keystrokes, command of bindingSet.getCommandsByKeystrokes()
mappings.push {keystrokes, command, selector, source}
mappings
# Private: Returns a user friendly description of where a keybinding was
# loaded from.
# Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
# that match a keystroke and element.
#
# * filePath:
# The absolute path from which the keymap was loaded
# * keystroke:
# The string representing the keys pressed (e.g. ctrl-P).
# * element:
# The DOM node that will match a {KeyBinding}'s selector.
keyBindingsForKeystrokeMatchingElement: (keystroke, element) ->
keyBindings = @keyBindingsForKeystroke(keystroke)
@keyBindingsMatchingElement(element, keyBindings)
# Public: Returns an array of {KeyBinding}s that match a keystroke
# * keystroke:
# The string representing the keys pressed (e.g. ctrl-P)
keyBindingsForKeystroke: (keystroke) ->
keystroke = KeyBinding.normalizeKeystroke(keystroke)
keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.matches(keystroke)
# Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
# whos selector matches the element.
#
# Returns one of:
# * `Core` indicates it comes from a bundled package.
# * `User` indicates that it was defined by a user.
# * `<package-name>` the package which defined it.
# * `Unknown` if an invalid path was passed in.
determineSource: (filePath) ->
return 'Unknown' unless filePath
# * element:
# The DOM node that will match a {KeyBinding}'s selector.
keyBindingsMatchingElement: (element, keyBindings=@keyBindings) ->
keyBindings = keyBindings.filter ({selector}) -> $(element).closest(selector).length > 0
keyBindings.sort (a, b) -> a.compare(b)
pathParts = filePath.split(path.sep)
if _.contains(pathParts, 'node_modules') or _.contains(pathParts, 'atom') or _.contains(pathParts, 'src')
'Core'
else if _.contains(pathParts, '.atom') and _.contains(pathParts, 'keymaps') and !_.contains(pathParts, 'packages')
'User'
else
packageNameIndex = pathParts.length - 3
pathParts[packageNameIndex]
bindKeys: (args...) ->
name = args.shift() if args.length > 2
[selector, bindings] = args
bindingSet = new BindingSet(selector, bindings, @nextBindingSetIndex++, name)
@bindingSets.unshift(bindingSet)
for keystrokes of bindingSet.commandsByKeystrokes
keystroke = keystrokes.split(' ')[0] # only index by first keystroke
@bindingSetsByFirstKeystroke[keystroke] ?= []
@bindingSetsByFirstKeystroke[keystroke].push(bindingSet)
unbindKeys: (selector, bindings) ->
bindingSet = _.detect @bindingSets, (bindingSet) ->
bindingSet.selector is selector and bindingSet.bindings is bindings
if bindingSet
_.remove(@bindingSets, bindingSet)
bindingsForElement: (element) ->
keystrokeMap = {}
currentNode = $(element)
while currentNode.length
bindingSets = @bindingSetsForNode(currentNode)
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
currentNode = currentNode.parent()
keystrokeMap
handleKeyEvent: (event) =>
event.keystrokes = @multiKeystrokeStringForEvent(event)
isMultiKeystroke = @queuedKeystrokes?
@queuedKeystrokes = null
firstKeystroke = event.keystrokes.split(' ')[0]
bindingSetsForFirstKeystroke = @bindingSetsByFirstKeystroke[firstKeystroke]
if bindingSetsForFirstKeystroke?
currentNode = $(event.target)
currentNode = rootView if currentNode is $('body')[0]
while currentNode.length
candidateBindingSets = @bindingSetsForNode(currentNode, bindingSetsForFirstKeystroke)
for bindingSet in candidateBindingSets
command = bindingSet.commandForEvent(event)
if command is 'native!'
return true
else if command
continue if @triggerCommandEvent(event, command)
return false
else if command == false
return false
if bindingSet.matchesKeystrokePrefix(event)
@queuedKeystrokes = event.keystrokes
return false
currentNode = currentNode.parent()
return false if isMultiKeystroke
return false if firstKeystroke is 'tab'
bindingSetsForNode: (node, candidateBindingSets = @bindingSets) ->
bindingSets = candidateBindingSets.filter (set) -> node.is(set.selector)
bindingSets.sort (a, b) ->
if b.specificity == a.specificity
b.index - a.index
else
b.specificity - a.specificity
triggerCommandEvent: (keyEvent, commandName) ->
keyEvent.target = rootView[0] if keyEvent.target == document.body and window.rootView
commandEvent = $.Event(commandName)
commandEvent.keyEvent = keyEvent
aborted = false
commandEvent.abortKeyBinding = ->
@stopImmediatePropagation()
aborted = true
$(keyEvent.target).trigger(commandEvent)
aborted
multiKeystrokeStringForEvent: (event) ->
currentKeystroke = @keystrokeStringForEvent(event)
if @queuedKeystrokes
if currentKeystroke in Modifiers
@queuedKeystrokes
else
@queuedKeystrokes + ' ' + currentKeystroke
else
currentKeystroke
keystrokeStringForEvent: (event) ->
# Public: Returns a keystroke string derived from an event.
# * event:
# A DOM or jQuery event
# * previousKeystroke:
# An optional string used for multiKeystrokes
keystrokeStringForEvent: (event, previousKeystroke) ->
if event.originalEvent.keyIdentifier.indexOf('U+') == 0
hexCharCode = event.originalEvent.keyIdentifier[2..]
charCode = parseInt(hexCharCode, 16)
@@ -207,16 +87,73 @@ class Keymap
else
key = key.toLowerCase()
[modifiers..., key].join('-')
keystroke = [modifiers..., key].join('-')
keystrokesByCommandForSelector: (selector)->
keystrokesByCommand = {}
for bindingSet in @bindingSets
for keystroke, command of bindingSet.commandsByKeystrokes
continue if selector? and selector != bindingSet.selector
keystrokesByCommand[command] ?= []
keystrokesByCommand[command].push keystroke
keystrokesByCommand
if previousKeystroke
if keystroke in Modifiers
previousKeystroke
else
"#{previousKeystroke} #{keystroke}"
else
keystroke
loadBundledKeymaps: ->
@loadDirectory(path.join(@resourcePath, 'keymaps'))
@emit('bundled-keymaps-loaded')
loadUserKeymap: ->
userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
@load(userKeymapPath) if userKeymapPath
loadDirectory: (directoryPath) ->
@load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])
load: (path) ->
@add(path, CSON.readFileSync(path))
add: (source, keyMappingsBySelector) ->
for selector, keyMappings of keyMappingsBySelector
@bindKeys(source, selector, keyMappings)
remove: (source) ->
@keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.source isnt source
bindKeys: (source, selector, keyMappings) ->
for keystroke, command of keyMappings
@keyBindings.push new KeyBinding(source, command, keystroke, selector)
handleKeyEvent: (event) ->
element = event.target
element = rootView if element == document.body
keystroke = @keystrokeStringForEvent(event, @queuedKeystroke)
keyBindings = @keyBindingsForKeystrokeMatchingElement(keystroke, element)
if keyBindings.length == 0 and @queuedKeystroke
@queuedKeystroke = null
return false
else
@queuedKeystroke = null
for keyBinding in keyBindings
partialMatch = keyBinding.keystroke isnt keystroke
if partialMatch
@queuedKeystroke = keystroke
shouldBubble = false
else
if keyBinding.command is 'native!'
shouldBubble = true
else if @triggerCommandEvent(element, keyBinding.command)
shouldBubble = false
break if shouldBubble?
shouldBubble ? true
triggerCommandEvent: (element, commandName) ->
commandEvent = $.Event(commandName)
commandEvent.abortKeyBinding = -> commandEvent.stopImmediatePropagation()
$(element).trigger(commandEvent)
not commandEvent.isImmediatePropagationStopped()
isAscii: (charCode) ->
0 <= charCode <= 127

View File

@@ -29,9 +29,11 @@ class MenuManager
# Public: Refreshes the currently visible menu.
update: ->
keystrokesByCommand = atom.keymap.keystrokesByCommandForSelector('body')
_.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor'))
_.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor:not(.mini)'))
keystrokesByCommand = {}
selectors = ['body', '.editor', '.editor:not(.mini)']
for binding in atom.keymap.getKeyBindings() when binding.selector in selectors
keystrokesByCommand[binding.command] ?= []
keystrokesByCommand[binding.command].push binding.keystroke
@sendToBrowserProcess(@template, keystrokesByCommand)
# Private
@@ -55,7 +57,7 @@ class MenuManager
# Private: OSX can't handle displaying accelerators for multiple keystrokes.
# If they are sent across, it will stop processing accelerators for the rest
# of the menu items.
filterMultipleKeystrokes: (keystrokesByCommand) ->
filterMultipleKeystroke: (keystrokesByCommand) ->
filtered = {}
for key, bindings of keystrokesByCommand
for binding in bindings
@@ -67,5 +69,5 @@ class MenuManager
# Private
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystrokes(keystrokesByCommand)
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.sendChannel 'update-application-menu', template, keystrokesByCommand

View File

@@ -54,7 +54,8 @@ class WindowEventHandler
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
@subscribe $(document), 'keydown', atom.keymap.handleKeyEvent
@subscribe $(document), 'keydown', (event) ->
atom.keymap.handleKeyEvent(event)
@subscribe $(document), 'drop', (e) ->
e.preventDefault()