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('') }