diff --git a/package.json b/package.json index b3942fee1..6f11896e2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index e00bb024a..1e8c5dda8 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -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') diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index ac8bb3614..e2c8f5817 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -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 diff --git a/spec/keymap-spec.coffee b/spec/keymap-spec.coffee index aaa386b56..41b3dda09 100644 --- a/spec/keymap-spec.coffee +++ b/spec/keymap-spec.coffee @@ -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 ''", -> - expect(keymap.determineSource(path.join('~', '.atom', 'packages', 'dummy', 'keymaps', 'a.cson'))).toEqual 'dummy' - - describe "for a linked package", -> - it "returns ''", -> - 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" diff --git a/spec/root-view-spec.coffee b/spec/root-view-spec.coffee index 1cdec3870..29396f67a 100644 --- a/spec/root-view-spec.coffee +++ b/spec/root-view-spec.coffee @@ -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", -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 0c381bf6a..0c07dd1af 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -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 = [] diff --git a/src/binding-set.coffee b/src/binding-set.coffee deleted file mode 100644 index 691206dc2..000000000 --- a/src/binding-set.coffee +++ /dev/null @@ -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('-') diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 57a0d7e81..7b8844bad 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -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() diff --git a/src/editor.coffee b/src/editor.coffee index 2f8720e67..9a38fb173 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1806,13 +1806,6 @@ class Editor extends View else ' ' - 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() diff --git a/src/key-binding.coffee b/src/key-binding.coffee new file mode 100644 index 000000000..11b4505d0 --- /dev/null +++ b/src/key-binding.coffee @@ -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 diff --git a/src/keymap.coffee b/src/keymap.coffee index d4d1b5902..7f7869813 100644 --- a/src/keymap.coffee +++ b/src/keymap.coffee @@ -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. - # * `` 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 diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index 5a3ce12cf..15e47e24f 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -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 diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 22f7fc5f3..6549073b3 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -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()