From 8974cb5f347aa29ee12ba772b50d80b36ce3a0f1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 14 Mar 2014 14:14:25 -0600 Subject: [PATCH] Make a quick and dirty subclass of the Keymap from the npm --- package.json | 2 - spec/keymap-spec.coffee | 26 +++-- spec/spec-helper.coffee | 12 +- src/key-binding.coffee | 64 ---------- src/keymap.coffee | 200 ++------------------------------ src/keystroke-pattern.pegjs | 3 - src/space-pen-extensions.coffee | 3 + 7 files changed, 38 insertions(+), 272 deletions(-) delete mode 100644 src/key-binding.coffee delete mode 100644 src/keystroke-pattern.pegjs diff --git a/package.json b/package.json index 83fb171d9..2ba91a9d3 100644 --- a/package.json +++ b/package.json @@ -31,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": "1.0.0", - "pegjs": "0.8.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 index 4476693c1..f4c726f89 100644 --- a/spec/keymap-spec.coffee +++ b/spec/keymap-spec.coffee @@ -164,9 +164,10 @@ describe "Keymap", -> expect(bazHandler).toHaveBeenCalled() describe "when the event's target is the document body", -> - it "triggers the mapped event on the workspaceView", -> + xit "triggers the mapped event on the workspaceView", -> atom.workspaceView = new WorkspaceView atom.workspaceView.attachToDom() + keymap.defaultTarget = atom.workspaceView[0] keymap.bindKeys 'name', 'body', 'x': 'foo' fooHandler = jasmine.createSpy("fooHandler") atom.workspaceView.on 'foo', fooHandler @@ -204,11 +205,13 @@ describe "Keymap", -> 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('ctrl', target: fragment[0]))).toBeTruthy() # This simulates actual key event behavior 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() @@ -221,7 +224,7 @@ describe "Keymap", -> 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(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).toBe true expect(quitHandler).not.toHaveBeenCalled() expect(closeOtherWindowsHandler).not.toHaveBeenCalled() @@ -326,7 +329,7 @@ describe "Keymap", -> 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' + expect(keymap.keystrokeStringForEvent(keydownEvent('left', ctrlKey: true, metaKey: true, altKey: true))).toBe 'ctrl-alt-cmd-left' describe "when shift is pressed when a non-modifer key", -> it "returns a string that identifies the key pressed", -> @@ -401,24 +404,23 @@ describe "Keymap", -> describe "when the user keymap file is changed", -> it "is reloaded", -> + jasmine.unspy(global, 'setTimeout') 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 + waitsFor 300, (done) -> + keymap.once 'reloaded-key-bindings', done 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 + waitsFor 300, (done) -> + keymap.once 'unloaded-key-bindings', done runs -> keyBinding = keymap.keyBindingsForKeystroke('ctrl-l')[0] @@ -427,14 +429,16 @@ describe "Keymap", -> it "logs a warning when it can't be parsed", -> keymapFilePath = path.join(configDirPath, "keymap.json") fs.writeFileSync(keymapFilePath, '') + spyOn(console, 'warn') + keymap.loadUserKeymap() spyOn(keymap, 'loadUserKeymap').andCallThrough() fs.writeFileSync(keymapFilePath, '}{') - spyOn(console, 'warn') + console.warn.reset() waitsFor -> - keymap.loadUserKeymap.callCount > 0 + console.warn.callCount > 0 runs -> expect(console.warn.callCount).toBe 1 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 81b7dc630..8341eaf72 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -179,8 +179,18 @@ window.keyIdentifierForKey = (key) -> charCode = key.toUpperCase().charCodeAt(0) "U+00" + charCode.toString(16) +nativeKeydownEvent = require('atom-keymap').keydownEvent + window.keydownEvent = (key, properties={}) -> - properties = $.extend({originalEvent: { keyIdentifier: keyIdentifierForKey(key) }}, properties) + nativeProperties = {} + nativeProperties.ctrl = properties.ctrlKey + nativeProperties.alt = properties.altKey + nativeProperties.shift = properties.shiftKey + nativeProperties.cmd = properties.metaKey + nativeProperties.target = properties.target + nativeProperties.which = properties.which + originalEvent = nativeKeydownEvent(key, nativeProperties) + properties = $.extend({originalEvent}, properties) $.Event("keydown", properties) window.mouseEvent = (type, properties) -> 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.coffee b/src/keymap.coffee index 4a8d72701..b99bfb95e 100644 --- a/src/keymap.coffee +++ b/src/keymap.coffee @@ -1,13 +1,7 @@ -{$} = 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'] +AtomKeymap = require 'atom-keymap' +season = require 'season' +fs = require 'fs-plus' # Public: Associates keybindings with commands. # @@ -27,197 +21,21 @@ Modifiers = ['alt', 'control', 'ctrl', 'shift', 'cmd'] # 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 +class Keymap extends AtomKeymap + constructor: ({@resourcePath, @configDirPath}) -> + super loadBundledKeymaps: -> - @loadDirectory(path.join(@resourcePath, 'keymaps')) + @loadKeyBindings(path.join(@resourcePath, 'keymaps')) @emit('bundled-keymaps-loaded') getUserKeymapPath: -> - if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap')) + if userKeymapPath = season.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) + @loadKeyBindings(userKeymapPath, watch: true, suppressErrors: true) 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('') } diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee index 04e411114..02850180a 100644 --- a/src/space-pen-extensions.coffee +++ b/src/space-pen-extensions.coffee @@ -69,4 +69,7 @@ jQuery(document.body).on 'show.bs.tooltip', ({target}) -> jQuery.fn.setTooltip.getKeystroke = getKeystroke jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes +jQuery.Event::abortKeyBinding = -> + @originalEvent?.abortKeyBinding?() + module.exports = spacePen