text: /*
   * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
   *  
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"), 
   * to deal in the Software without restriction, including without limitation 
   * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
   * and/or sell copies of the Software, and to permit persons to whom the 
   * Software is furnished to do so, subject to the following conditions:
   *  
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *  
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
   * DEALINGS IN THE SOFTWARE.
   * 
   */
  
  
  /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
  /*global define, $, brackets, window */
  /*unittests: KeyBindingManager */
  
  /**
   * Manages the mapping of keyboard inputs to commands.
   */
  define(function (require, exports, module) {
      "use strict";
  
      require("utils/Global");
  
      var AppInit             = require("utils/AppInit"),
          Commands            = require("command/Commands"),
          CommandManager      = require("command/CommandManager"),
          DefaultDialogs      = require("widgets/DefaultDialogs"),
          FileSystem          = require("filesystem/FileSystem"),
          FileSystemError     = require("filesystem/FileSystemError"),
          FileUtils           = require("file/FileUtils"),
          KeyEvent            = require("utils/KeyEvent"),
          Strings             = require("strings"),
          StringUtils         = require("utils/StringUtils"),
          UrlParams           = require("utils/UrlParams").UrlParams,
          _                   = require("thirdparty/lodash");
  
      var KeyboardPrefs       = JSON.parse(require("text!base-config/keyboard.json"));
      
      var KEYMAP_FILENAME     = "keymap.json",
          _userKeyMapFilePath = brackets.app.getApplicationSupportDirectory() + "/" + KEYMAP_FILENAME;
  
      /**
       * @private
       * Maps normalized shortcut descriptor to key binding info.
       * @type {!Object.<string, {commandID: string, key: string, displayKey: string}>}
       */
      var _keyMap            = {},    // For the actual key bindings including user specified ones
          // For the default factory key bindings, cloned from _keyMap after all extensions are loaded.
          _defaultKeyMap     = {};
  
      /**
       * @typedef {{shortcut: !string,
       *            commandID: ?string}} UserKeyBinding
       */
      
      /**
       * @private
       * Maps shortcut descriptor to a command id.
       * @type {UserKeyBinding}
       */
      var _customKeyMap      = {},
          _customKeyMapCache = {};
      
      /**
       * @private
       * Maps commandID to the list of shortcuts that are bound to it.
       * @type {!Object.<string, Array.<{key: string, displayKey: string}>>}
       */
      var _commandMap  = {},
          _allCommands = [];
      
      var _specialCommands = [Commands.EDIT_UNDO, Commands.EDIT_REDO, Commands.EDIT_SELECT_ALL,
                              Commands.EDIT_CUT, Commands.EDIT_COPY, Commands.EDIT_PASTE],
          _reservedShortcuts = ["Ctrl-Z", "Ctrl-Y", "Ctrl-A", "Ctrl-X", "Ctrl-C", "Ctrl-V"],
          _macReservedShortcuts = ["Cmd-,", "Cmd-H", "Cmd-Alt-H", "Cmd-M", "Cmd-Shift-Z", "Cmd-Q"],
          _keyNames = ["Up", "Down", "Left", "Right", "Backspace", "Enter", "Space", "Tab"];
  
      /**
       * @private
       * Flag to show key binding errors in the key map file. Default is true and 
       * it will be set to false when reloading without extensions. This flag is not 
       * used to suppress errors in loading or parsing the key map file. So if the key 
       * map file is corrupt, then the error dialog still shows up.
       *
       * @type {boolean}
       */
      var _showErrors = true;
  
      /**
       * @private
       * Allow clients to toggle key binding
       * @type {boolean}
       */
      var _enabled = true;
      
      /**
       * @private
       * Stack of registered global keydown hooks.
       * @type {Array.<function(Event): boolean>}
       */
      var _globalKeydownHooks = [];
  
      /**
       * @private
       */
      function _reset() {
          _keyMap = {};
          _defaultKeyMap = {};
          _customKeyMap = {};
          _customKeyMapCache = {};
          _commandMap = {};
          _globalKeydownHooks = [];
          _userKeyMapFilePath = brackets.app.getApplicationSupportDirectory() + "/" + KEYMAP_FILENAME;
      }
  
      /**
       * @private
       * Initialize an empty keymap as the current keymap. It overwrites the current keymap if there is one.
       * builds the keyDescriptor string from the given parts
       * @param {boolean} hasCtrl Is Ctrl key enabled
       * @param {boolean} hasAlt Is Alt key enabled
       * @param {boolean} hasShift Is Shift key enabled
       * @param {string} key The key that's pressed
       * @return {string} The normalized key descriptor
       */
      function _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key) {
          if (!key) {
              console.log("KeyBindingManager _buildKeyDescriptor() - No key provided!");
              return "";
          }
          
          var keyDescriptor = [];
         
          if (hasMacCtrl) {
              keyDescriptor.push("Ctrl");
          }
          if (hasAlt) {
              keyDescriptor.push("Alt");
          }
          if (hasShift) {
              keyDescriptor.push("Shift");
          }
  
          if (hasCtrl) {
              // Windows display Ctrl first, Mac displays Command symbol last
              if (brackets.platform === "mac") {
                  keyDescriptor.push("Cmd");
              } else {
                  keyDescriptor.unshift("Ctrl");
              }
          }
  
          keyDescriptor.push(key);
          
          return keyDescriptor.join("-");
      }
      
      
      /**
       * normalizes the incoming key descriptor so the modifier keys are always specified in the correct order
       * @param {string} The string for a key descriptor, can be in any order, the result will be Ctrl-Alt-Shift-<Key>
       * @return {string} The normalized key descriptor or null if the descriptor invalid
       */
      function normalizeKeyDescriptorString(origDescriptor) {
          var hasMacCtrl = false,
              hasCtrl = false,
              hasAlt = false,
              hasShift = false,
              key = "",
              error = false;
  
          function _compareModifierString(left, right) {
              if (!left || !right) {
                  return false;
              }
              left = left.trim().toLowerCase();
              right = right.trim().toLowerCase();
              
              return (left.length > 0 && left === right);
          }
          
          origDescriptor.split("-").forEach(function parseDescriptor(ele, i, arr) {
              if (_compareModifierString("ctrl", ele)) {
                  if (brackets.platform === "mac") {
                      hasMacCtrl = true;
                  } else {
                      hasCtrl = true;
                  }
              } else if (_compareModifierString("cmd", ele)) {
                  if (brackets.platform === "mac") {
                      hasCtrl = true;
                  } else {
                      error = true;
                  }
              } else if (_compareModifierString("alt", ele)) {
                  hasAlt = true;
              } else if (_compareModifierString("opt", ele)) {
                  if (brackets.platform === "mac") {
                      hasAlt = true;
                  } else {
                      error = true;
                  }
              } else if (_compareModifierString("shift", ele)) {
                  hasShift = true;
              } else if (key.length > 0) {
                  console.log("KeyBindingManager normalizeKeyDescriptorString() - Multiple keys defined. Using key: " + key + " from: " + origDescriptor);
                  error = true;
              } else {
                  key = ele;
              }
          });
          
          if (error) {
              return null;
          }
  
          // Check to see if the binding is for "-".
          if (key === "" && origDescriptor.search(/^.+--$/) !== -1) {
              key = "-";
          }
          
          // '+' char is valid if it's the only key. Keyboard shortcut strings should use
          // unicode characters (unescaped). Keyboard shortcut display strings may use
          // unicode escape sequences (e.g. \u20AC euro sign)
          if ((key.indexOf("+")) >= 0 && (key.length > 1)) {
              return null;
          }
          
          // Ensure that the first letter of the key name is in upper case.
          // i.e. 'a' => 'A' and 'up' => 'Up'
          if (/^[a-z]/.test(key)) {
              key = key.toLowerCase().replace(/(^[a-z])/, function (match, p1) {
                  return p1.toUpperCase();
              });
          }
          
          // No restriction on single character key yet, but other key names are restricted to either 
          // Function keys or those listed in _keyNames array.
          if (key.length > 1 && !/F\d+/.test(key) &&
                  _keyNames.indexOf(key) === -1) {
              return null;
          }
          
          return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
      }
      
      /**
       * @private
       * Looks for keycodes that have os-inconsistent keys and fixes them.
       * @param {number} The keycode from the keyboard event.
       * @param {string} The current best guess at what the key is.
       * @return {string} If the key is OS-inconsistent, the correct key; otherwise, the original key.
       **/
      function _mapKeycodeToKey(keycode, key) {
          // If keycode represents one of the digit keys (0-9), then return the corresponding digit
          // by subtracting KeyEvent.DOM_VK_0 from keycode. ie. [48-57] --> [0-9]
          if (keycode >= KeyEvent.DOM_VK_0 && keycode <= KeyEvent.DOM_VK_9) {
              return String(keycode - KeyEvent.DOM_VK_0);
          // Do the same with the numpad numbers
          // by subtracting KeyEvent.DOM_VK_NUMPAD0 from keycode. ie. [96-105] --> [0-9]
          } else if (keycode >= KeyEvent.DOM_VK_NUMPAD0 && keycode <= KeyEvent.DOM_VK_NUMPAD9) {
              return String(keycode - KeyEvent.DOM_VK_NUMPAD0);
          }
          
          
          switch (keycode) {
          case KeyEvent.DOM_VK_SEMICOLON:
              return ";";
          case KeyEvent.DOM_VK_EQUALS:
              return "=";
          case KeyEvent.DOM_VK_COMMA:
              return ",";
          case KeyEvent.DOM_VK_SUBTRACT:
          case KeyEvent.DOM_VK_DASH:
              return "-";
          case KeyEvent.DOM_VK_ADD:
              return "+";
          case KeyEvent.DOM_VK_DECIMAL:
          case KeyEvent.DOM_VK_PERIOD:
              return ".";
          case KeyEvent.DOM_VK_DIVIDE:
          case KeyEvent.DOM_VK_SLASH:
              return "/";
          case KeyEvent.DOM_VK_BACK_QUOTE:
              return "`";
          case KeyEvent.DOM_VK_OPEN_BRACKET:
              return "[";
          case KeyEvent.DOM_VK_BACK_SLASH:
              return "\\";
          case KeyEvent.DOM_VK_CLOSE_BRACKET:
              return "]";
          case KeyEvent.DOM_VK_QUOTE:
              return "'";
          default:
              return key;
          }
      }
      
      /**
       * Takes a keyboard event and translates it into a key in a key map
       */
      function _translateKeyboardEvent(event) {
          var hasMacCtrl = (brackets.platform === "mac") ? (event.ctrlKey) : false,
              hasCtrl = (brackets.platform !== "mac") ? (event.ctrlKey) : (event.metaKey),
              hasAlt = (event.altKey),
              hasShift = (event.shiftKey),
              key = String.fromCharCode(event.keyCode);
          
          //From the W3C, if we can get the KeyboardEvent.keyIdentifier then look here
          //As that will let us use keys like then function keys "F5" for commands. The
          //full set of values we can use is here
          //http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
          var ident = event.keyIdentifier;
          if (ident) {
              if (ident.charAt(0) === "U" && ident.charAt(1) === "+") {
                  //This is a unicode code point like "U+002A", get the 002A and use that
                  key = String.fromCharCode(parseInt(ident.substring(2), 16));
              } else {
                  //This is some non-character key, just use the raw identifier
                  key = ident;
              }
          }
          
          // Translate some keys to their common names
          if (key === "\t") {
              key = "Tab";
          } else if (key === " ") {
              key = "Space";
          } else if (key === "\b") {
              key = "Backspace";
          } else {
              key = _mapKeycodeToKey(event.keyCode, key);
          }
  
          return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
      }
      
      /**
       * Convert normalized key representation to display appropriate for platform.
       * @param {!string} descriptor Normalized key descriptor.
       * @return {!string} Display/Operating system appropriate string
       */
      function formatKeyDescriptor(descriptor) {
          var displayStr;
          
          if (brackets.platform === "mac") {
              displayStr = descriptor.replace(/-(?!$)/g, "");     // remove dashes
              displayStr = displayStr.replace("Ctrl", "\u2303");  // Ctrl > control symbol
              displayStr = displayStr.replace("Cmd", "\u2318");   // Cmd > command symbol
              displayStr = displayStr.replace("Shift", "\u21E7"); // Shift > shift symbol
              displayStr = displayStr.replace("Alt", "\u2325");   // Alt > option symbol
          } else {
              displayStr = descriptor.replace("Ctrl", Strings.KEYBOARD_CTRL);   // Ctrl
              displayStr = displayStr.replace("Shift", Strings.KEYBOARD_SHIFT); // Shift > shift symbol
              displayStr = displayStr.replace("Space", Strings.KEYBOARD_SPACE); // Alt > option symbol
              displayStr = displayStr.replace(/-(?!$)/g, "+");
          }
  
          return displayStr;
      }
  
      /**
       * @private
       * @param {string} A normalized key-description string.
       * @return {boolean} true if the key is already assigned, false otherwise.
       */
      function _isKeyAssigned(key) {
          return (_keyMap[key] !== undefined);
      }
  
      /**
       * Remove a key binding from _keymap
       *
       * @param {!string} key - a key-description string that may or may not be normalized.
       * @param {?string} platform - OS from which to remove the binding (all platforms if unspecified)
       */
      function removeBinding(key, platform) {
          if (!key || ((platform !== null) && (platform !== undefined) && (platform !== brackets.platform))) {
              return;
          }
  
          var normalizedKey = normalizeKeyDescriptorString(key);
          
          if (!normalizedKey) {
              console.log("Fail to nomalize " + key);
          } else if (_isKeyAssigned(normalizedKey)) {
              var binding = _keyMap[normalizedKey],
                  command = CommandManager.get(binding.commandID),
                  bindings = _commandMap[binding.commandID];
              
              // delete key binding record
              delete _keyMap[normalizedKey];
              
              if (bindings) {
                  // delete mapping from command to key binding
                  _commandMap[binding.commandID] = bindings.filter(function (b) {
                      return (b.key !== normalizedKey);
                  });
      
                  if (command) {
                      $(command).triggerHandler("keyBindingRemoved", [{key: normalizedKey, displayKey: binding.displayKey}]);
                  }
              }
          }
      }
  
      /**
       * @private
       *
       * @param {string} commandID
       * @param {string|{{key: string, displayKey: string}}} keyBinding - a single shortcut.
       * @param {?string} platform
       *     - "all" indicates all platforms, not overridable
       *     - undefined indicates all platforms, overridden by platform-specific binding
       * @return {?{key: string, displayKey:String}} Returns a record for valid key bindings.
       *     Returns null when key binding platform does not match, binding does not normalize,
       *     or is already assigned.
       */
      function _addBinding(commandID, keyBinding, platform) {
          var key,
              result = null,
              normalized,
              normalizedDisplay,
              explicitPlatform = keyBinding.platform || platform,
              targetPlatform,
              command,
              bindingsToDelete = [],
              existing;
  
          // For platform: "all", use explicit current plaform
          if (explicitPlatform && explicitPlatform !== "all") {
              targetPlatform = explicitPlatform;
          } else {
              targetPlatform = brackets.platform;
          }
          
          
          // Skip if the key binding is not for this platform.
          if (explicitPlatform === "mac" && brackets.platform !== "mac") {
              return null;
          }
  
          // if the request does not specify an explicit platform, and we're
          // currently on a mac, then replace Ctrl with Cmd.
          key = (keyBinding.key) || keyBinding;
          if (brackets.platform === "mac" && (explicitPlatform === undefined || explicitPlatform === "all")) {
              key = key.replace("Ctrl", "Cmd");
              if (keyBinding.displayKey !== undefined) {
                  keyBinding.displayKey = keyBinding.displayKey.replace("Ctrl", "Cmd");
              }
          }
          
          normalized = normalizeKeyDescriptorString(key);
          
          // skip if the key binding is invalid 
          if (!normalized) {
              console.error("Unable to parse key binding " + key + ". Permitted modifiers: Ctrl, Cmd, Alt, Opt, Shift; separated by '-' (not '+').");
              return null;
          }
          
          // check for duplicate key bindings
          existing = _keyMap[normalized];
          
          // for cross-platform compatibility
          if (exports.useWindowsCompatibleBindings) {
              // windows-only key bindings are used as the default binding
              // only if a default binding wasn't already defined
              if (explicitPlatform === "win") {
                  // search for a generic or platform-specific binding if it
                  // already exists
                  if (existing && (!existing.explicitPlatform ||
                                   existing.explicitPlatform === brackets.platform ||
                                   existing.explicitPlatform === "all")) {
                      // do not clobber existing binding with windows-only binding
                      return null;
                  }
                  
                  // target this windows binding for the current platform
                  targetPlatform = brackets.platform;
              }
          }
          
          // skip if this binding doesn't match the current platform
          if (targetPlatform !== brackets.platform) {
              return null;
          }
          
          // skip if the key is already assigned
          if (existing) {
              if (!existing.explicitPlatform && explicitPlatform) {
                  // remove the the generic binding to replace with this new platform-specific binding
                  removeBinding(normalized);
                  existing = false;
              }
          }
          
          // delete existing bindings when
          // (1) replacing a windows-compatible binding with a generic or
          //     platform-specific binding
          // (2) replacing a generic binding with a platform-specific binding
          var existingBindings = _commandMap[commandID] || [],
              isWindowsCompatible,
              isReplaceGeneric,
              ignoreGeneric;
          
          existingBindings.forEach(function (binding) {
              // remove windows-only bindings in _commandMap
              isWindowsCompatible = exports.useWindowsCompatibleBindings &&
                  binding.explicitPlatform === "win";
              
              // remove existing generic binding
              isReplaceGeneric = !binding.explicitPlatform &&
                  explicitPlatform;
              
              if (isWindowsCompatible || isReplaceGeneric) {
                  bindingsToDelete.push(binding);
              } else {
                  // existing binding is platform-specific and the requested binding is generic
                  ignoreGeneric = binding.explicitPlatform && !explicitPlatform;
              }
          });
  
          if (ignoreGeneric) {
              // explicit command binding overrides this one
              return null;
          }
          
          if (existing) {
              // do not re-assign a key binding
              console.error("Cannot assign " + normalized + " to " + commandID + ". It is already assigned to " + _keyMap[normalized].commandID);
              return null;
          }
          
          // remove generic or windows-compatible bindings
          bindingsToDelete.forEach(function (binding) {
              removeBinding(binding.key);
          });
          
          // optional display-friendly string (e.g. CMD-+ instead of CMD-=)
          normalizedDisplay = (keyBinding.displayKey) ? normalizeKeyDescriptorString(keyBinding.displayKey) : normalized;
          
          // 1-to-many commandID mapping to key binding
          if (!_commandMap[commandID]) {
              _commandMap[commandID] = [];
          }
          
          result = {
              key                 : normalized,
              displayKey          : normalizedDisplay,
              explicitPlatform    : explicitPlatform
          };
          
          _commandMap[commandID].push(result);
          
          // 1-to-1 key binding to commandID
          _keyMap[normalized] = {
              commandID           : commandID,
              key                 : normalized,
              displayKey          : normalizedDisplay,
              explicitPlatform    : explicitPlatform
          };
          
          // notify listeners
          command = CommandManager.get(commandID);
          
          if (command) {
              $(command).triggerHandler("keyBindingAdded", [result]);
          }
          
          return result;
      }
  
      /**
       * Returns a copy of the current key map. If the optional 'defaults' parameter is true,
       * then a copy of the default key map is returned.
       * @param {boolean=} defaults true if the caller wants a copy of the default key map.
       *                            Otherwise, the current active key map is returned.
       * @return {!Object.<string, {commandID: string, key: string, displayKey: string}>}
       */
      function getKeymap(defaults) {
          return $.extend({}, defaults ? _defaultKeyMap : _keyMap);
      }
  
      /**
       * Process the keybinding for the current key.
       *
       * @param {string} A key-description string.
       * @return {boolean} true if the key was processed, false otherwise
       */
      function _handleKey(key) {
          if (_enabled && _keyMap[key]) {
              // The execute() function returns a promise because some commands are async.
              // Generally, commands decide whether they can run or not synchronously,
              // and reject immediately, so we can test for that synchronously.
              var promise = CommandManager.execute(_keyMap[key].commandID);
              return (promise.state() !== "rejected");
          }
          return false;
      }
  
      // TODO (issue #414): Replace this temporary fix with a more robust solution to handle focus and modality
      /**
       * Enable or disable key bindings. Clients such as dialogs may wish to disable
       * global key bindings temporarily.
       *
       * @param {string} A key-description string.
       * @return {boolean} true if the key was processed, false otherwise
       */
      function setEnabled(value) {
          _enabled = value;
      }
  
      /**
       * @private
       *
       * Sort objects by platform property. Objects with a platform property come
       * before objects without a platform property.
       */
      function _sortByPlatform(a, b) {
          var a1 = (a.platform) ? 1 : 0,
              b1 = (b.platform) ? 1 : 0;
          return b1 - a1;
      }
  
      /**
       * Add one or more key bindings to a particular Command.
       *
       * @param {!string | Command} command - A command ID or command object
       * @param {?({key: string, displayKey: string}|Array.<{key: string, displayKey: string, platform: string}>)} keyBindings
       *     A single key binding or an array of keybindings. Example:
       *     "Shift-Cmd-F". Mac and Win key equivalents are automatically
       *     mapped to each other. Use displayKey property to display a different
       *     string (e.g. "CMD+" instead of "CMD=").
       * @param {?string} platform The target OS of the keyBindings either
       *     "mac", "win" or "linux". If undefined, all platforms not explicitly
       *     defined will use the key binding.
       * @return {{key: string, displayKey:String}|Array.<{key: string, displayKey:String}>}
       *     Returns record(s) for valid key binding(s)
       */
      function addBinding(command, keyBindings, platform) {
          var commandID = "",
              results;
          
          if (!command) {
              console.error("addBinding(): missing required parameter: command");
              return;
          }
          
          if (!keyBindings) { return; }
          
          if (typeof (command) === "string") {
              commandID = command;
          } else {
              commandID = command.getID();
          }
          
          if (Array.isArray(keyBindings)) {
              var keyBinding;
              results = [];
  
              // process platform-specific bindings first
              keyBindings.sort(_sortByPlatform);
              
              keyBindings.forEach(function addSingleBinding(keyBindingRequest) {
                  // attempt to add keybinding
                  keyBinding = _addBinding(commandID, keyBindingRequest, keyBindingRequest.platform);
                  
                  if (keyBinding) {
                      results.push(keyBinding);
                  }
              });
          } else {
              results = _addBinding(commandID, keyBindings, platform);
          }
          
          return results;
      }
  
      /**
       * Retrieve key bindings currently associated with a command
       *
       * @param {!string | Command} command - A command ID or command object
       * @return {!Array.<{{key: string, displayKey: string}}>} An array of associated key bindings.
       */
      function getKeyBindings(command) {
          var bindings    = [],
              commandID   = "";
          
          if (!command) {
              console.error("getKeyBindings(): missing required parameter: command");
              return [];
          }
          
          if (typeof (command) === "string") {
              commandID = command;
          } else {
              commandID = command.getID();
          }
          
          bindings = _commandMap[commandID];
          return bindings || [];
      }
      
      /**
       * Adds default key bindings when commands are registered to CommandManager
       * @param {$.Event} event jQuery event
       * @param {Command} command Newly registered command
       */
      function _handleCommandRegistered(event, command) {
          var commandId   = command.getID(),
              defaults    = KeyboardPrefs[commandId];
          
          if (defaults) {
              addBinding(commandId, defaults);
          }
      }
      
      /**
       * Adds a global keydown hook that gets first crack at keydown events 
       * before standard keybindings do. This is intended for use by modal or 
       * semi-modal UI elements like dialogs or the code hint list that should 
       * execute before normal command bindings are run. 
       * 
       * The hook is passed one parameter, the original keyboard event. If the 
       * hook handles the event (or wants to block other global hooks from 
       * handling the event), it should return true. Note that this will *only*
       * stop other global hooks and KeyBindingManager from handling the
       * event; to prevent further event propagation, you will need to call
       * stopPropagation(), stopImmediatePropagation(), and/or preventDefault()
       * as usual.
       *
       * Multiple keydown hooks can be registered, and are executed in order, 
       * most-recently-added first.
       * 
       * (We have to have a special API for this because (1) handlers are normally
       * called in least-recently-added order, and we want most-recently-added; 
       * (2) native DOM events don't have a way for us to find out if 
       * stopImmediatePropagation()/stopPropagation() has been called on the
       * event, so we have to have some other way for one of the hooks to 
       * indicate that it wants to block the other hooks from running.)
       *
       * @param {function(Event): boolean} hook The global hook to add.
       */
      function addGlobalKeydownHook(hook) {
          _globalKeydownHooks.push(hook);
      }
      
      /**
       * Removes a global keydown hook added by `addGlobalKeydownHook`.
       * Does not need to be the most recently added hook.
       *
       * @param {function(Event): boolean} hook The global hook to remove.
       */
      function removeGlobalKeydownHook(hook) {
          var index = _globalKeydownHooks.indexOf(hook);
          if (index !== -1) {
              _globalKeydownHooks.splice(index, 1);
          }
      }
      
      /**
       * Handles a given keydown event, checking global hooks first before
       * deciding to handle it ourselves.
       * @param {Event} The keydown event to handle.
       */
      function _handleKeyEvent(event) {
          var i, handled = false;
          for (i = _globalKeydownHooks.length - 1; i >= 0; i--) {
              if (_globalKeydownHooks[i](event)) {
                  handled = true;
                  break;
              }
          }
          if (!handled && _handleKey(_translateKeyboardEvent(event))) {
              event.stopPropagation();
              event.preventDefault();
          }
      }
  
      AppInit.htmlReady(function () {
          // Install keydown event listener.
          window.document.body.addEventListener(
              "keydown",
              _handleKeyEvent,
              true
          );
          
          exports.useWindowsCompatibleBindings = (brackets.platform !== "mac") &&
              (brackets.platform !== "win");
      });
      
      /**
       * @private
       * Displays an error dialog and also opens the user key map file for editing only if 
       * the error is not the loading file error.
       *
       * @param {?string} err Error type returned from JSON parser or open file operation
       * @param {string=} message Error message to be displayed in the dialog
       */
      function _showErrorsAndOpenKeyMap(err, message) {
          // Asynchronously loading Dialogs module to avoid the circular dependency
          require(["widgets/Dialogs"], function (Dialogs) {
              var errorMessage = Strings.ERROR_KEYMAP_CORRUPT;
              
              if (err === FileSystemError.UNSUPPORTED_ENCODING) {
                  errorMessage = Strings.ERROR_LOADING_KEYMAP;
              } else if (message) {
                  errorMessage = message;
              }
              
              Dialogs.showModalDialog(
                  DefaultDialogs.DIALOG_ID_ERROR,
                  Strings.ERROR_KEYMAP_TITLE,
                  errorMessage
              )
                  .done(function () {
                      if (err !== FileSystemError.UNSUPPORTED_ENCODING) {
                          CommandManager.execute(Commands.FILE_OPEN_KEYMAP);
                      }
                  });
          });
      }
      
      /**
       * @private
       *
       * Checks whether the given command ID is a special command that the user can't bind 
       * to another shortcut.
       * @param {!string} commandID A string referring to a specific command
       * @return {boolean} true if normalizedKey is a special command, false otherwise.
       */
      function _isSpecialCommand(commandID) {
          if (brackets.platform === "mac" && commandID === "file.quit") {
              return true;
          }
          
          return (_specialCommands.indexOf(commandID) > -1);
      }
      
      /**
       * @private
       *
       * Checks whether the given key combination is a shortcut of a special command
       * or a Mac system command that the user can't reassign to another command.
       * @param {!string} normalizedKey A key combination string used for a keyboard shortcut
       * @return {boolean} true if normalizedKey is a restricted shortcut, false otherwise.
       */
      function _isReservedShortcuts(normalizedKey) {
          if (!normalizedKey) {
              return false;
          }
          
          if (_reservedShortcuts.indexOf(normalizedKey) > -1 ||
                  _reservedShortcuts.indexOf(normalizedKey.replace("Cmd", "Ctrl")) > -1) {
              return true;
          }
          
          if (brackets.platform === "mac" && _macReservedShortcuts.indexOf(normalizedKey) > -1) {
              return true;
          }
          
          return false;
      }
      
      /**
       * @private
       *
       * Creates a bullet list item for any item in the given list.
       * @param {Array.<string>} list An array of strings to be converted into a 
       * message string with a bullet list.
       * @return {string} the html text version of the list
       */
      function _getBulletList(list) {
          var message = "<ul class='dialog-list'>";
          list.forEach(function (info) {
              message += "<li>" + info + "</li>";
          });
          message += "</ul>";
          return message;
      }
      
      /**
       * @private
       *
       * Gets the corresponding unicode symbol of an arrow key for display in the menu.
       * @param {string} normalizedKey 
       * @return {string} An empty string if normalizedKey does not have any arrow key. Otherwise, the name
       *                  of the arrow key is replaced with the corresponding unicode symbol in the return string.
       */
      function _getDisplayKey(normalizedKey) {
          var displayKey = "";
          if (/(Up|Down|Left|Right)$/i.test(normalizedKey)) {
              normalizedKey = normalizedKey.replace(/Up$/i, "\u2191");
              normalizedKey = normalizedKey.replace(/Down$/i, "\u2193");
              normalizedKey = normalizedKey.replace(/Left$/i, "\u2190");
              normalizedKey = normalizedKey.replace(/Right$/i, "\u2192");
              displayKey = normalizedKey;
          }
          return displayKey;
      }
      
      /**
       * @private
       *
       * Applies each user key binding to all the affected commands and updates _keyMap. 
       * Shows errors in a dialog and then opens the user key map file if any of the following 
       * is detected while applying the user key bindings.
       *     - A key binding is attempting to modify a special command.
       *     - A key binding is attempting to assign a shortcut of a special command to another one.
       *     - Multiple key bindings are specified for the same command ID.
       *     - The same key combination is listed for multiple key bindings.
       *     - A key binding has any invalid key syntax.
       *     - A key binding is referring to a non-existent command ID.
       */
      function _applyUserKeyBindings() {
          var remappedCommands   = [],
              remappedKeys       = [],
              restrictedCommands = [],
              restrictedKeys     = [],
              invalidKeys        = [],
              invalidCommands    = [],
              multipleKeys       = [],
              duplicateBindings  = [],
              errorMessage       = "";
          
          _.forEach(_customKeyMap, function (commandID, key) {
              var normalizedKey    = normalizeKeyDescriptorString(key),
                  existingBindings = _commandMap[commandID] || [];
  
              // Skip this since we don't allow user to update key binding of a special 
              // command like cut, copy, paste, undo, redo and select all.
              if (_isSpecialCommand(commandID)) {
                  restrictedCommands.push(commandID);
                  return;
              }
  
              // Skip this since we don't allow user to update a shortcut used in 
              // a special command or any Mac system command.
              if (_isReservedShortcuts(normalizedKey)) {
                  restrictedKeys.push(key);
                  return;
              }
  
              if (!normalizedKey) {
                  invalidKeys.push(key);
              } else if (_isKeyAssigned(normalizedKey)) {
                  if (remappedKeys.indexOf(normalizedKey) !== -1) {
                      // JSON parser already removed all the duplicates that have the exact
                      // same case or order in their keys. So we're only detecting duplicate 
                      // bindings that have different orders or different cases used in the key.
                      duplicateBindings.push(key);
                      return;
                  }
                  // The same key binding already exists, so skip this.
                  if (_keyMap[normalizedKey].commandID === commandID) {
                      // Still need to add it to the remappedCommands so that
                      // we can detect any duplicate later on.
                      remappedCommands.push(commandID);
                      return;
                  }
                  removeBinding(normalizedKey);
              }
  
              if (remappedKeys.indexOf(normalizedKey) === -1) {
                  remappedKeys.push(normalizedKey);
              }
  
              // Remove another key binding if the new key binding is for a command
              // that has a different key binding. e.g. "Ctrl-W": "edit.selectLine"
              // requires us to remove "Ctrl-W" from "file.close" command, but we
              // also need to remove "Ctrl-L" from "edit.selectLine".
              if (existingBindings.length) {
                  existingBindings.forEach(function (binding) {
                      removeBinding(binding.key);
                  });
              }
  
              if (commandID) {
                  if (_allCommands.indexOf(commandID) !== -1) {
                      if (remappedCommands.indexOf(commandID) === -1) {
                          var keybinding = { key: normalizedKey };
  
                          keybinding.displayKey = _getDisplayKey(normalizedKey);
                          addBinding(commandID, keybinding.displayKey ? keybinding : normalizedKey, brackets.platform);
                          remappedCommands.push(commandID);
                      } else {
                          multipleKeys.push(commandID);
                      }
                  } else {
                      invalidCommands.push(commandID);
                  }
              }
          });
  
          if (restrictedCommands.length) {
              errorMessage = StringUtils.format(Strings.ERROR_RESTRICTED_COMMANDS, _getBulletList(restrictedCommands));
          }
  
          if (restrictedKeys.length) {
              errorMessage += StringUtils.format(Strings.ERROR_RESTRICTED_SHORTCUTS, _getBulletList(restrictedKeys));
          }
  
          if (multipleKeys.length) {
              errorMessage += StringUtils.format(Strings.ERROR_MULTIPLE_SHORTCUTS, _getBulletList(multipleKeys));
          }
  
          if (duplicateBindings.length) {
              errorMessage += StringUtils.format(Strings.ERROR_DUPLICATE_SHORTCUTS, _getBulletList(duplicateBindings));
          }
  
          if (invalidKeys.length) {
              errorMessage += StringUtils.format(Strings.ERROR_INVALID_SHORTCUTS, _getBulletList(invalidKeys));
          }
  
          if (invalidCommands.length) {
              errorMessage += StringUtils.format(Strings.ERROR_NONEXISTENT_COMMANDS, _getBulletList(invalidCommands));
          }
  
          if (_showErrors && errorMessage) {
              _showErrorsAndOpenKeyMap("", errorMessage);
          }
      }
      
      /**
       * @private
       *
       * Restores the default key bindings for all the commands that are modified by each key binding
       * specified in _customKeyMapCache (old version) but no longer specified in _customKeyMap (new version).
       */
      function _undoPriorUserKeyBindings() {
          _.forEach(_customKeyMapCache, function (commandID, key) {
              var normalizedKey  = normalizeKeyDescriptorString(key),
                  defaults       = _.find(_.toArray(_defaultKeyMap), { "commandID": commandID }),
                  defaultCommand = _defaultKeyMap[normalizedKey];
  
              // We didn't modified this before, so skip it.
              if (_isSpecialCommand(commandID) ||
                      _isReservedShortcuts(normalizedKey)) {
                  return;
              }
  
              if (_isKeyAssigned(normalizedKey) &&
                      _customKeyMap[key] !== commandID && _customKeyMap[normalizedKey] !== commandID) {
                  // Unassign the key from any command. e.g. "Cmd-W": "file.open" in _customKeyMapCache
                  // will require us to remove Cmd-W shortcut from file.open command.
                  removeBinding(normalizedKey);
                  
                  // Reassign the default key binding. e.g. "Cmd-W": "file.open" in _customKeyMapCache
                  // will require us to reassign Cmd-O shortcut to file.open command.
                  if (defaults) {
                      addBinding(commandID, defaults, brackets.platform);
                  }
  
                  // Reassign the default key binding of the previously modified command. 
                  // e.g. "Cmd-W": "file.open" in _customKeyMapCache will require us to reassign Cmd-W
                  // shortcut to file.close command.
                  if (defaultCommand && defaultCommand.key) {
                      addBinding(defaultCommand.commandID, defaultCommand.key, brackets.platform);
                  }
              }
          });
      }
      
      /**
       * @private
       *
       * Gets the full file path to the user key map file. In testing environment
       * a different file path is returned so that running integration tests won't
       * pop up the error dialog showing the errors from the actual user key map file.
       *
       * @return {string} full file path to the user key map file.
       */
      function _getUserKeyMapFilePath() {
          if (window.isBracketsTestWindow) {
              return brackets.app.getApplicationSupportDirectory() + "/_test_/" + KEYMAP_FILENAME;
          }
          return _userKeyMapFilePath;
      }
  
      /**
       * @private
       *
       * Reads in the user key map file and parses its content into JSON.
       * Returns the user key bindings if JSON has "overrides".
       * Otherwise, returns an empty object or an error if the file 
       * cannot be parsed or loaded.
       *
       * @return {$.Promise} a jQuery promise that will be resolved with the JSON
       * object if the user key map file has "overrides" property or an empty JSON.
       * If the key map file cannot be read or cannot be parsed by the JSON parser,
       * then the promise is rejected with an error.
       */
      function _readUserKeyMap() {
          var file   = FileSystem.getFileForPath(_getUserKeyMapFilePath()),
              result = new $.Deferred();
         
          file.exists(function (err, doesExist) {
              if (doesExist) {
                  FileUtils.readAsText(file)
                      .done(function (text) {
                          var keyMap = {};
                          try {
                              if (text) {
                                  var json = JSON.parse(text);
                                  // If no overrides, return an empty key map.
                                  result.resolve((json && json.overrides) || keyMap);
                              } else {
                                  // The file is empty, so return an empty key map.
                                  result.resolve(keyMap);
                              }
                          } catch (err) {
                              // Cannot parse the text read from the key map file.
                              result.reject(err);
                          }
                      })
                      .fail(function (err) {
                          // Key map file cannot be loaded.
                          result.reject(err);
                      });
              } else {
                  // Just resolve if no user key map file
                  result.resolve();
              }
          });
          return result.promise();
      }
  
      /**
       * @private
       *
       * Reads in the user key bindings and updates the key map with each user key
       * binding by removing the existing one assigned to each key and adding 
       * new one for the specified command id. Shows errors and opens the user 
       * key map file if it cannot be parsed.
       */
      function _loadUserKeyMap() {
          _readUserKeyMap()
              .then(function (keyMap) {
                  _customKeyMapCache = _.cloneDeep(_customKeyMap);
                  _customKeyMap = keyMap;
                  _undoPriorUserKeyBindings();
                  _applyUserKeyBindings();
              }, function (err) {
                  _showErrorsAndOpenKeyMap(err);
              });
      }
          
      /**
       * @private
       *
       * Opens the existing key map file or creates a new one with default content 
       * if it does not exist.
       */
      function _openUserKeyMap() {
          var userKeyMapPath = _getUserKeyMapFilePath(),
              file = FileSystem.getFileForPath(userKeyMapPath);
          file.exists(function (err, doesExist) {
              if (doesExist) {
                  CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
              } else {
                  var defaultContent = "{\n    \"documentation\": \"https://github.com/adobe/brackets/wiki/User-Key-Bindings\"," +
                                       "\n    \"overrides\": {" +
                                       "\n        \n    }\n}\n";
                  
                  FileUtils.writeText(file, defaultContent, true)
                      .done(function () {
                          CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
                      });
              }
          });
      }
      
      $(CommandManager).on("commandRegistered", _handleCommandRegistered);
      CommandManager.register(Strings.CMD_OPEN_KEYMAP, Commands.FILE_OPEN_KEYMAP, _openUserKeyMap);
  
      // Asynchronously loading DocumentManager to avoid the circular dependency
      require(["document/DocumentManager"], function (DocumentManager) {
          $(DocumentManager).on("documentSaved", function checkKeyMapUpdates(e, doc) {
              if (doc && doc.file.fullPath === _userKeyMapFilePath) {
                  _loadUserKeyMap();
              }
          });
      });
      
      /**
       * @private
       *
       * Initializes _allCommands array and _defaultKeyMap so that we can use them for
       * detecting non-existent commands and restoring the original key binding.
       *
       * @param {string} fullPath file path to the user key map file.
       */
      function _initCommandAndKeyMaps() {
          _allCommands = CommandManager.getAll();
          // Keep a copy of the default key bindings before loading user key bindings.
          _defaultKeyMap = _.cloneDeep(_keyMap);
      }
      
      /**
       * @private
       *
       * Sets the full file path to the user key map file. Only used by unit tests 
       * to load a test file instead of the actual user key map file.
       *
       * @param {string} fullPath file path to the user key map file.
       */
      function _setUserKeyMapFilePath(fullPath) {
          _userKeyMapFilePath = fullPath;
      }
      
      AppInit.extensionsLoaded(function () {
          var params  = new UrlParams();
          params.parse();
          if (params.get("reloadWithoutUserExts") === "true") {
              _showErrors = false;
          }
          
          _initCommandAndKeyMaps();
          _loadUserKeyMap();
      });
  
      // unit test only
      exports._reset = _reset;
      exports._setUserKeyMapFilePath = _setUserKeyMapFilePath;
      exports._getDisplayKey = _getDisplayKey;
      exports._loadUserKeyMap = _loadUserKeyMap;
      exports._initCommandAndKeyMaps = _initCommandAndKeyMaps;
  
      // Define public API
      exports.getKeymap = getKeymap;
      exports.setEnabled = setEnabled;
      exports.addBinding = addBinding;
      exports.removeBinding = removeBinding;
      exports.formatKeyDescriptor = formatKeyDescriptor;
      exports.getKeyBindings = getKeyBindings;
      exports.addGlobalKeydownHook = addGlobalKeydownHook;
      exports.removeGlobalKeydownHook = removeGlobalKeydownHook;
      
      /**
       * Use windows-specific bindings if no other are found (e.g. Linux).
       * Core Brackets modules that use key bindings should always define at
       * least a generic keybinding that is applied for all platforms. This
       * setting effectively creates a compatibility mode for third party
       * extensions that define explicit key bindings for Windows and Mac, but
       * not Linux.
       */
      exports.useWindowsCompatibleBindings = false;
      
      // For unit testing only
      exports._handleKey = _handleKey;
      exports._handleKeyEvent = _handleKeyEvent;
  });