{$} = 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 isNamedKey = key.length > 1 modifiers.push 'shift' if isNamedKey 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)