diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee
index b32669242..fdc49ad68 100644
--- a/build/tasks/docs-task.coffee
+++ b/build/tasks/docs-task.coffee
@@ -143,6 +143,8 @@ downloadFileFromRepo = ({repo, file}, callback) ->
downloadIncludes = (callback) ->
includes = [
+ {repo: 'atom-keymap', file: 'src/keymap.coffee'}
+ {repo: 'atom-keymap', file: 'src/key-binding.coffee'}
{repo: 'first-mate', file: 'src/grammar.coffee'}
{repo: 'first-mate', file: 'src/grammar-registry.coffee'}
{repo: 'node-pathwatcher', file: 'src/directory.coffee'}
diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson
index bce80b757..43ec6951d 100644
--- a/dot-atom/keymap.cson
+++ b/dot-atom/keymap.cson
@@ -12,7 +12,7 @@
# '.editor':
# 'enter': 'editor:newline'
#
-# 'body':
+# '.workspace':
# 'ctrl-P': 'core:move-up'
# 'ctrl-p': 'core:move-down'
#
diff --git a/package.json b/package.json
index 34537dd65..a47d2c481 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"atomShellVersion": "0.10.7",
"dependencies": {
"async": "0.2.6",
+ "atom-keymap": "^0.8.0",
"bootstrap": "git://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372",
"clear-cut": "0.4.0",
"coffee-script": "1.7.0",
@@ -30,13 +31,11 @@
"mkdirp": "0.3.5",
"keytar": "0.15.1",
"less-cache": "0.12.0",
- "loophole": "^0.3.0",
"mixto": "1.x",
"nslog": "0.5.0",
"oniguruma": ">=1.0.3 <2.0",
"optimist": "0.4.0",
- "pathwatcher": "0.19.0",
- "pegjs": "0.8.0",
+ "pathwatcher": "^1.0.0",
"property-accessors": "1.x",
"q": "^1.0.1",
"random-words": "0.0.1",
diff --git a/spec/keymap-spec.coffee b/spec/keymap-spec.coffee
deleted file mode 100644
index 4476693c1..000000000
--- a/spec/keymap-spec.coffee
+++ /dev/null
@@ -1,454 +0,0 @@
-fs = require 'fs-plus'
-path = require 'path'
-temp = require 'temp'
-Keymap = require '../src/keymap'
-{$, $$, WorkspaceView} = require 'atom'
-
-describe "Keymap", ->
- fragment = null
- keymap = null
- resourcePath = atom.getLoadSettings().resourcePath
- configDirPath = null
-
- beforeEach ->
- configDirPath = temp.mkdirSync('atom')
- keymap = new Keymap({configDirPath, resourcePath})
- fragment = $ """
-
- """
-
- afterEach ->
- keymap.destroy()
-
- describe ".handleKeyEvent(event)", ->
- deleteCharHandler = null
- insertCharHandler = null
- commandZHandler = null
-
- beforeEach ->
- keymap.bindKeys 'name', '.command-mode', 'x': 'deleteChar'
- keymap.bindKeys 'name', '.insert-mode', 'x': 'insertChar'
- keymap.bindKeys 'name', '.command-mode', 'cmd-z': 'commandZPressed'
-
- deleteCharHandler = jasmine.createSpy('deleteCharHandler')
- insertCharHandler = jasmine.createSpy('insertCharHandler')
- commandZHandler = jasmine.createSpy('commandZHandler')
- fragment.on 'deleteChar', deleteCharHandler
- fragment.on 'insertChar', insertCharHandler
- fragment.on 'commandZPressed', commandZHandler
-
- describe "when no binding matches the event's keystroke", ->
- it "does not return false so the event continues to propagate", ->
- expect(keymap.handleKeyEvent(keydownEvent('0', target: fragment[0]))).not.toBe false
-
- 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, which: 122, target: fragment[0]) # This is the 'z' key using the Greek keyboard layout
- result = keymap.handleKeyEvent(event)
-
- expect(result).toBe(false)
- expect(commandZHandler).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')
-
- event = keydownEvent('x', target: fragment[0])
- keymap.handleKeyEvent(event)
- expect(deleteCharHandler).not.toHaveBeenCalled()
- expect(insertCharHandler).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')
-
- 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", ->
- beforeEach ->
- keymap.bindKeys 'name', '.child-node', 'x': 'foo'
-
- it "only triggers bindings on selectors associated with the closest ancestor node", ->
- 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 'abortKeyBinding' is called on the triggered event", ->
- [fooHandler1, fooHandler2] = []
-
- beforeEach ->
- fooHandler1 = jasmine.createSpy('fooHandler1').andCallFake (e) ->
- expect(deleteCharHandler).not.toHaveBeenCalled()
- e.abortKeyBinding()
- fooHandler2 = jasmine.createSpy('fooHandler2')
-
- fragment.find('.child-node').on 'foo', fooHandler1
- fragment.on 'foo', fooHandler2
-
- it "aborts the current event and tries again with the next-most-specific key binding", ->
- target = fragment.find('.grandchild-node')[0]
- keymap.handleKeyEvent(keydownEvent('x', target: target))
- expect(fooHandler1).toHaveBeenCalled()
- expect(fooHandler2).not.toHaveBeenCalled()
- expect(deleteCharHandler).toHaveBeenCalled()
-
- it "does not throw an exception if the event was not triggered by the keymap", ->
- fragment.find('.grandchild-node').trigger 'foo'
-
- 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 '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'
- 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).not.toHaveBeenCalled()
- expect(barHandler).not.toHaveBeenCalled()
- expect(bazHandler).toHaveBeenCalled()
-
- describe "when the matching selectors have the same specificity", ->
- it "triggers the bindings for the most recently declared selector", ->
- keymap.bindKeys 'name', '.child-node', 'x': 'foo', 'y': 'baz'
- keymap.bindKeys 'name', '.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
-
- 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 the event's target is the document body", ->
- it "triggers the mapped event on the workspaceView", ->
- atom.workspaceView = new WorkspaceView
- atom.workspaceView.attachToDom()
- keymap.bindKeys 'name', 'body', 'x': 'foo'
- fooHandler = jasmine.createSpy("fooHandler")
- atom.workspaceView.on 'foo', fooHandler
-
- result = keymap.handleKeyEvent(keydownEvent('x', target: document.body))
- expect(result).toBe(false)
- expect(fooHandler).toHaveBeenCalled()
- expect(deleteCharHandler).not.toHaveBeenCalled()
- expect(insertCharHandler).not.toHaveBeenCalled()
-
- describe "when the event matches a 'native!' binding", ->
- it "returns true, allowing the browser's native key handling to process the event", ->
- 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
- expect(nativeHandler).not.toHaveBeenCalled()
-
- describe "when at least one binding partially matches the event's keystroke", ->
- [quitHandler, closeOtherWindowsHandler] = []
-
- beforeEach ->
- keymap.bindKeys 'name', "*",
- 'ctrl-x ctrl-c': 'quit'
- 'ctrl-x 1': 'close-other-windows'
-
- quitHandler = jasmine.createSpy('quitHandler')
- closeOtherWindowsHandler = jasmine.createSpy('closeOtherWindowsHandler')
- fragment.on 'quit', quitHandler
- fragment.on 'close-other-windows', closeOtherWindowsHandler
-
- it "only matches entire keystroke patterns", ->
- expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).not.toBe false
-
- 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", ->
- it "triggers the event associated with the matched multi-stroke binding", ->
- expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment[0], ctrlKey: true))).toBeFalsy()
- expect(keymap.handleKeyEvent(keydownEvent('ctrl', target: fragment[0]))).toBeFalsy() # This simulates actual key event behavior
- 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 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()
- expect(closeOtherWindowsHandler).not.toHaveBeenCalled()
-
- expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).not.toBe false
-
- 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 '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
-
- expect(keymap.handleKeyEvent(keydownEvent('x', target: grandchildNode, ctrlKey: true))).toBeFalsy()
- expect(keymap.handleKeyEvent(keydownEvent('1', target: grandchildNode))).toBeFalsy()
- expect(quitHandler).not.toHaveBeenCalled()
- expect(moreSpecificQuitHandler).not.toHaveBeenCalled()
- expect(closeOtherWindowsHandler).toHaveBeenCalled()
- closeOtherWindowsHandler.reset()
-
- expect(keymap.handleKeyEvent(keydownEvent('x', target: grandchildNode, ctrlKey: true))).toBeFalsy()
- expect(keymap.handleKeyEvent(keydownEvent('c', target: grandchildNode, ctrlKey: true))).toBeFalsy()
- expect(quitHandler).not.toHaveBeenCalled()
- expect(closeOtherWindowsHandler).not.toHaveBeenCalled()
- expect(moreSpecificQuitHandler).toHaveBeenCalled()
-
- 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(name, selector, bindings)", ->
- it "normalizes bindings that use shift with lower case alpha char", ->
- fooHandler = jasmine.createSpy('fooHandler')
- fragment.on 'foo', fooHandler
- keymap.bindKeys 'name', '*', 'ctrl-shift-l': 'foo'
- result = keymap.handleKeyEvent(keydownEvent('l', ctrlKey: true, altKey: false, shiftKey: true, target: fragment[0]))
- expect(result).toBe(false)
- expect(fooHandler).toHaveBeenCalled()
-
- it "normalizes bindings that use shift with upper case alpha char", ->
- fooHandler = jasmine.createSpy('fooHandler')
- fragment.on 'foo', fooHandler
- keymap.bindKeys 'name', '*', 'ctrl-shift-L': 'foo'
- result = keymap.handleKeyEvent(keydownEvent('l', ctrlKey: true, altKey: false, shiftKey: true, target: fragment[0]))
- expect(result).toBe(false)
- expect(fooHandler).toHaveBeenCalled()
-
- it "normalizes bindings that use an upper case alpha char without shift", ->
- fooHandler = jasmine.createSpy('fooHandler')
- fragment.on 'foo', fooHandler
- keymap.bindKeys 'name', '*', 'ctrl-L': 'foo'
- result = keymap.handleKeyEvent(keydownEvent('l', ctrlKey: true, altKey: false, shiftKey: true, target: fragment[0]))
- expect(result).toBe(false)
- expect(fooHandler).toHaveBeenCalled()
-
- 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 '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 'name', '*', 'ctrl-alt--': 'foo'
- result = keymap.handleKeyEvent(keydownEvent('-', ctrlKey: true, altKey: true, target: fragment[0]))
- expect(result).toBe(false)
- expect(fooHandler).toHaveBeenCalled()
-
- describe ".remove(name)", ->
- it "removes the binding set with the given selector and bindings", ->
- keymap.add 'nature',
- '.green':
- 'ctrl-c': 'cultivate'
- '.brown':
- '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.keyBindingsMatchingElement($$ -> @div class: 'green')).toHaveLength 1
- expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'brown')).toEqual []
-
- describe ".keystrokeStringForEvent(event)", ->
- describe "when no modifiers are pressed", ->
- it "returns a string that identifies the key pressed", ->
- expect(keymap.keystrokeStringForEvent(keydownEvent('a'))).toBe 'a'
- expect(keymap.keystrokeStringForEvent(keydownEvent('['))).toBe '['
- expect(keymap.keystrokeStringForEvent(keydownEvent('*'))).toBe '*'
- expect(keymap.keystrokeStringForEvent(keydownEvent('left'))).toBe 'left'
- expect(keymap.keystrokeStringForEvent(keydownEvent('\b'))).toBe 'backspace'
-
- describe "when ctrl, alt or command is pressed with a non-modifier key", ->
- it "returns a string that identifies the key pressed", ->
- expect(keymap.keystrokeStringForEvent(keydownEvent('a', altKey: true))).toBe 'alt-a'
- expect(keymap.keystrokeStringForEvent(keydownEvent('[', metaKey: true))).toBe 'cmd-['
- expect(keymap.keystrokeStringForEvent(keydownEvent('*', ctrlKey: true))).toBe 'ctrl-*'
- expect(keymap.keystrokeStringForEvent(keydownEvent('left', ctrlKey: true, metaKey: true, altKey: true))).toBe 'alt-cmd-ctrl-left'
-
- describe "when shift is pressed when a non-modifer key", ->
- it "returns a string that identifies the key pressed", ->
- expect(keymap.keystrokeStringForEvent(keydownEvent('A', shiftKey: true))).toBe 'shift-A'
- expect(keymap.keystrokeStringForEvent(keydownEvent('{', shiftKey: true))).toBe '{'
- expect(keymap.keystrokeStringForEvent(keydownEvent('left', shiftKey: true))).toBe 'shift-left'
- expect(keymap.keystrokeStringForEvent(keydownEvent('Left', shiftKey: true))).toBe 'shift-left'
-
- describe ".keyBindingsMatchingElement(element)", ->
- it "returns the matching bindings for the element", ->
- keymap.bindKeys 'name', '.command-mode', 'c': 'c'
- keymap.bindKeys 'name', '.grandchild-node', 'g': '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 'name', '.command-mode', 'g': 'cmd-mode'
- keymap.bindKeys 'name', '.command-mode .grandchild-node', 'g': 'cmd-and-grandchild-node'
- keymap.bindKeys 'name', '.grandchild-node', 'g': 'grandchild-node'
-
- bindings = keymap.keyBindingsMatchingElement(fragment.find('.grandchild-node'))
- expect(bindings).toHaveLength 3
- expect(bindings[0].command).toEqual "cmd-and-grandchild-node"
-
- describe ".keyBindingsForCommandMatchingElement(element)", ->
- beforeEach ->
- keymap.add 'nature',
- '.green':
- 'ctrl-c': 'cultivate'
- '.green-2':
- 'ctrl-o': 'cultivate'
- '.brown':
- 'ctrl-h': 'harvest'
- '.blue':
- 'ctrl-c': 'fly'
-
- it "finds a keymap for an element", ->
- el = $$ -> @div class: 'green'
- bindings = keymap.keyBindingsForCommandMatchingElement('cultivate', el)
- expect(bindings).toHaveLength 1
- expect(bindings[0].keystroke).toEqual "ctrl-c"
-
- it "no keymap an element without that map", ->
- el = $$ -> @div class: 'brown'
- bindings = keymap.keyBindingsForCommandMatchingElement('cultivate', el)
- expect(bindings).toHaveLength 0
-
- describe "loading platform specific keybindings", ->
- customKeymap = null
-
- beforeEach ->
- resourcePath = temp.mkdirSync('atom')
- customKeymap = new Keymap({configDirPath, resourcePath})
-
- afterEach ->
- customKeymap.destroy()
-
- it "doesn't load keybindings from other platforms", ->
- win32FilePath = path.join(resourcePath, "keymaps", "win32.cson")
- darwinFilePath = path.join(resourcePath, "keymaps", "darwin.cson")
- fs.writeFileSync(win32FilePath, '"body": "ctrl-l": "core:win32-move-left"')
- fs.writeFileSync(darwinFilePath, '"body": "ctrl-l": "core:darwin-move-left"')
-
- customKeymap.loadBundledKeymaps()
- keyBindings = customKeymap.keyBindingsForKeystroke('ctrl-l')
- expect(keyBindings).toHaveLength 1
- expect(keyBindings[0].command).toBe "core:#{process.platform}-move-left"
-
- describe "when the user keymap file is changed", ->
- it "is reloaded", ->
- keymapFilePath = path.join(configDirPath, "keymap.cson")
- fs.writeFileSync(keymapFilePath, '"body": "ctrl-l": "core:move-left"')
- keymap.loadUserKeymap()
-
- spyOn(keymap, 'loadUserKeymap').andCallThrough()
- fs.writeFileSync(keymapFilePath, "'body': 'ctrl-l': 'core:move-right'")
-
- waitsFor ->
- keymap.loadUserKeymap.callCount > 0
-
- runs ->
- keyBinding = keymap.keyBindingsForKeystroke('ctrl-l')[0]
- expect(keyBinding.command).toBe 'core:move-right'
- keymap.loadUserKeymap.reset()
- fs.removeSync(keymapFilePath)
-
- waitsFor ->
- keymap.loadUserKeymap.callCount > 0
-
- runs ->
- keyBinding = keymap.keyBindingsForKeystroke('ctrl-l')[0]
- expect(keyBinding).toBeUndefined()
-
- it "logs a warning when it can't be parsed", ->
- keymapFilePath = path.join(configDirPath, "keymap.json")
- fs.writeFileSync(keymapFilePath, '')
- keymap.loadUserKeymap()
-
- spyOn(keymap, 'loadUserKeymap').andCallThrough()
- fs.writeFileSync(keymapFilePath, '}{')
- spyOn(console, 'warn')
-
- waitsFor ->
- keymap.loadUserKeymap.callCount > 0
-
- runs ->
- expect(console.warn.callCount).toBe 1
- expect(console.warn.argsForCall[0][0].length).toBeGreaterThan 0
-
- describe "when adding a binding with an invalid selector", ->
- it "logs a warning and does not add it", ->
- spyOn(console, 'warn')
- keybinding =
- '##selector':
- 'cmd-a': 'invalid-command'
- keymap.add('test', keybinding)
-
- expect(console.warn.callCount).toBe 1
- expect(console.warn.argsForCall[0][0].length).toBeGreaterThan 0
- expect(-> keymap.keyBindingsMatchingElement(document.body)).not.toThrow()
- expect(keymap.keyBindingsForCommand('invalid:command')).toEqual []
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index 81b7dc630..3f68f9062 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -6,8 +6,8 @@ require '../vendor/jasmine-jquery'
path = require 'path'
_ = require 'underscore-plus'
fs = require 'fs-plus'
+Keymap = require '../src/keymap-extensions'
{$, WorkspaceView} = require 'atom'
-Keymap = require '../src/keymap'
Config = require '../src/config'
{Point} = require 'text-buffer'
Project = require '../src/project'
@@ -180,7 +180,15 @@ window.keyIdentifierForKey = (key) ->
"U+00" + charCode.toString(16)
window.keydownEvent = (key, properties={}) ->
- properties = $.extend({originalEvent: { keyIdentifier: keyIdentifierForKey(key) }}, properties)
+ originalEventProperties = {}
+ originalEventProperties.ctrl = properties.ctrlKey
+ originalEventProperties.alt = properties.altKey
+ originalEventProperties.shift = properties.shiftKey
+ originalEventProperties.cmd = properties.metaKey
+ originalEventProperties.target = properties.target?[0] ? properties.target
+ originalEventProperties.which = properties.which
+ originalEvent = Keymap.keydownEvent(key, originalEventProperties)
+ properties = $.extend({originalEvent}, properties)
$.Event("keydown", properties)
window.mouseEvent = (type, properties) ->
diff --git a/src/atom.coffee b/src/atom.coffee
index 5905902cd..5bf5b87d7 100644
--- a/src/atom.coffee
+++ b/src/atom.coffee
@@ -138,7 +138,7 @@ class Atom extends Model
@loadTime = null
Config = require './config'
- Keymap = require './keymap'
+ Keymap = require './keymap-extensions'
PackageManager = require './package-manager'
Clipboard = require './clipboard'
Syntax = require './syntax'
@@ -236,6 +236,7 @@ class Atom extends Model
WorkspaceView = require './workspace-view'
@workspace = Workspace.deserialize(@state.workspace) ? new Workspace
@workspaceView = new WorkspaceView(@workspace)
+ @keymap.defaultTarget = @workspaceView[0]
$(@workspaceViewParentSelector).append(@workspaceView)
deserializePackageStates: ->
diff --git a/src/key-binding.coffee b/src/key-binding.coffee
deleted file mode 100644
index 0dbc73f20..000000000
--- a/src/key-binding.coffee
+++ /dev/null
@@ -1,64 +0,0 @@
-_ = require 'underscore-plus'
-fs = require 'fs-plus'
-{specificity} = require 'clear-cut'
-
-module.exports =
-class KeyBinding
- @parser: null
- @currentIndex: 1
- @specificities: null
-
- @calculateSpecificity: (selector) ->
- @specificities ?= {}
- value = @specificities[selector]
- unless value?
- value = specificity(selector)
- @specificities[selector] = value
- value
-
- @normalizeKeystroke: (keystroke) ->
- normalizedKeystroke = keystroke.split(/\s+/).map (keystroke) =>
- keys = @parseKeystroke(keystroke)
- modifiers = keys[0...-1]
- modifiers.sort()
- key = _.last(keys)
-
- modifiers.push 'shift' if /^[A-Z]$/.test(key) and 'shift' not in modifiers
- key = key.toUpperCase() if /^[a-z]$/.test(key) and 'shift' in modifiers
-
- [modifiers..., key].join('-')
-
- normalizedKeystroke.join(' ')
-
- @parseKeystroke: (keystroke) ->
- unless @parser?
- try
- @parser = require './keystroke-pattern'
- catch
- {allowUnsafeEval} = require 'loophole'
- keystrokePattern = fs.readFileSync(require.resolve('./keystroke-pattern.pegjs'), 'utf8')
- PEG = require 'pegjs'
- allowUnsafeEval => @parser = PEG.buildParser(keystrokePattern)
-
- @parser.parse(keystroke)
-
- constructor: (source, command, keystroke, selector) ->
- @source = source
- @command = command
- @keystroke = KeyBinding.normalizeKeystroke(keystroke)
- @selector = selector.replace(/!important/g, '')
- @specificity = KeyBinding.calculateSpecificity(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
diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee
new file mode 100644
index 000000000..c86e82b11
--- /dev/null
+++ b/src/keymap-extensions.coffee
@@ -0,0 +1,27 @@
+fs = require 'fs-plus'
+path = require 'path'
+Keymap = require 'atom-keymap'
+CSON = require 'season'
+{jQuery} = require 'space-pen'
+
+Keymap::loadBundledKeymaps = ->
+ @loadKeyBindings(path.join(@resourcePath, 'keymaps'))
+ @emit('bundled-keymaps-loaded')
+
+Keymap::getUserKeymapPath = ->
+ if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
+ userKeymapPath
+ else
+ path.join(@configDirPath, 'keymap.cson')
+
+Keymap::loadUserKeymap = ->
+ userKeymapPath = @getUserKeymapPath()
+ if fs.isFileSync(userKeymapPath)
+ @loadKeyBindings(userKeymapPath, watch: true, suppressErrors: true)
+
+# This enables command handlers registered via jQuery to call
+# `.abortKeyBinding()` on the `jQuery.Event` object passed to the handler.
+jQuery.Event::abortKeyBinding = ->
+ @originalEvent?.abortKeyBinding?()
+
+module.exports = Keymap
diff --git a/src/keymap.coffee b/src/keymap.coffee
deleted file mode 100644
index 4a8d72701..000000000
--- a/src/keymap.coffee
+++ /dev/null
@@ -1,223 +0,0 @@
-{$} = require './space-pen-extensions'
-_ = require 'underscore-plus'
-fs = require 'fs-plus'
-path = require 'path'
-CSON = require 'season'
-KeyBinding = require './key-binding'
-{File} = require 'pathwatcher'
-{Emitter} = require 'emissary'
-
-Modifiers = ['alt', 'control', 'ctrl', 'shift', 'cmd']
-
-# Public: Associates keybindings with commands.
-#
-# An instance of this class is always available as the `atom.keymap` global.
-#
-# Keymaps are defined in a CSON/JSON format. A typical keymap looks something
-# like this:
-#
-# ```cson
-# 'body':
-# 'ctrl-l': 'package:do-something'
-# '.someClass':
-# 'enter': 'package:confirm'
-# ```
-#
-# As a key, you define the DOM element you want to work on, using CSS notation.
-# For that key, you define one or more key:value pairs, associating keystrokes
-# with a command to execute.
-module.exports =
-class Keymap
- Emitter.includeInto(this)
-
- constructor: ({@resourcePath, @configDirPath})->
- @keyBindings = []
-
- destroy: ->
- @unwatchUserKeymap()
-
- # Public: Returns an array of all {KeyBinding}s.
- getKeyBindings: ->
- _.clone(@keyBindings)
-
- # Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
- # that match a keystroke and element.
- #
- # 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 a array of {KeyBinding}s (sorted by selector specificity)
- # that match a command.
- #
- # command - The {String} representing the command (tree-view:toggle).
- # element - The DOM node that will match a {KeyBinding}'s selector.
- keyBindingsForCommandMatchingElement: (command, element) ->
- keyBindings = @keyBindingsForCommand(command)
- @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.filter (keyBinding) -> keyBinding.matches(keystroke)
-
- # Public: Returns an array of {KeyBinding}s that match a command
- #
- # keystroke - The {String} representing the keys pressed (e.g. ctrl-P)
- keyBindingsForCommand: (command) ->
- @keyBindings.filter (keyBinding) -> keyBinding.command == command
-
- # Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
- # whos selector matches the element.
- #
- # 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)
-
- # 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)
- charCode = event.which if !@isAscii(charCode) and @isAscii(event.which)
- key = @keyFromCharCode(charCode)
- else
- key = event.originalEvent.keyIdentifier.toLowerCase()
-
- modifiers = []
- if event.altKey and key not in Modifiers
- modifiers.push 'alt'
- if event.metaKey and key not in Modifiers
- modifiers.push 'cmd'
- if event.ctrlKey and key not in Modifiers
- modifiers.push 'ctrl'
- if event.shiftKey and key not in Modifiers
- # Don't push the shift modifier on single letter non-alpha keys (e.g. { or ')
- modifiers.push 'shift' unless /^[^a-z]$/i.test(key)
-
- if 'shift' in modifiers and /^[a-z]$/i.test(key)
- key = key.toUpperCase()
- else
- key = key.toLowerCase()
-
- keystroke = [modifiers..., key].join('-')
-
- if previousKeystroke
- if keystroke in Modifiers
- previousKeystroke
- else
- "#{previousKeystroke} #{keystroke}"
- else
- keystroke
-
- loadBundledKeymaps: ->
- @loadDirectory(path.join(@resourcePath, 'keymaps'))
- @emit('bundled-keymaps-loaded')
-
- getUserKeymapPath: ->
- if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
- userKeymapPath
- else
- path.join(@configDirPath, 'keymap.cson')
-
- unwatchUserKeymap: ->
- @userKeymapFile?.off()
- @remove(@userKeymapPath) if @userKeymapPath?
-
- loadUserKeymap: ->
- @unwatchUserKeymap()
- userKeymapPath = @getUserKeymapPath()
- if fs.isFileSync(userKeymapPath)
- @userKeymapPath = userKeymapPath
- @userKeymapFile = new File(userKeymapPath)
- @userKeymapFile.on 'contents-changed moved removed', => @loadUserKeymap()
- @add(@userKeymapPath, @readUserKeymap())
-
- readUserKeymap: ->
- try
- CSON.readFileSync(@userKeymapPath) ? {}
- catch error
- console.warn("Failed to load your keymap file: #{@userKeymapPath}", error.stack ? error)
- {}
-
- loadDirectory: (directoryPath) ->
- platforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32']
- otherPlatforms = platforms.filter (name) -> name != process.platform
-
- for filePath in fs.listSync(directoryPath, ['.cson', '.json'])
- continue if path.basename(filePath, path.extname(filePath)) in otherPlatforms
- @load(filePath)
-
- 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
- keyBinding = new KeyBinding(source, command, keystroke, selector)
- try
- $(keyBinding.selector) # Verify selector is valid before registering
- @keyBindings.push(keyBinding)
- catch
- console.warn("Keybinding '#{keystroke}': '#{command}' in #{source} has an invalid selector: '#{selector}'")
-
- handleKeyEvent: (event) ->
- element = event.target
- element = atom.workspaceView 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, event)
- shouldBubble = false
-
- break if shouldBubble?
-
- shouldBubble ? true
-
- triggerCommandEvent: (element, commandName, event) ->
- commandEvent = $.Event(commandName)
- commandEvent.originalEvent = event
- commandEvent.abortKeyBinding = -> commandEvent.stopImmediatePropagation()
- $(element).trigger(commandEvent)
- not commandEvent.isImmediatePropagationStopped()
-
- isAscii: (charCode) ->
- 0 <= charCode <= 127
-
- keyFromCharCode: (charCode) ->
- switch charCode
- when 8 then 'backspace'
- when 9 then 'tab'
- when 13 then 'enter'
- when 27 then 'escape'
- when 32 then 'space'
- when 127 then 'delete'
- else String.fromCharCode(charCode)
diff --git a/src/keystroke-pattern.pegjs b/src/keystroke-pattern.pegjs
deleted file mode 100644
index 5c0283916..000000000
--- a/src/keystroke-pattern.pegjs
+++ /dev/null
@@ -1,3 +0,0 @@
-keystrokePattern = key:key additionalKeys:additionalKey* { return [key].concat(additionalKeys); }
-additionalKey = '-' key:key { return key; }
-key = '-' / chars:[^-]+ { return chars.join('') }