UNPKG

ace.improved

Version:
596 lines (512 loc) 20.3 kB
(function(ace) { // imports var Editor = ace.require("ace/editor").Editor, oop = ace.require("ace/lib/oop"), useragent = ace.require("ace/lib/useragent"), keyLib = ace.require("ace/lib/keys"), KeyBinding = ace.require("ace/keyboard/keybinding").KeyBinding, HashHandler = ace.require("ace/keyboard/hash_handler").HashHandler; // exports ace.ext = ace.ext || {}; var exports = ace.ext.keys = ace.ext.keys || {}; exports.lookupKeys = lookupKeys; exports.captureEditorCommand = captureEditorCommand; exports.allEditorCommands = allEditorCommands; exports.simulateKey = simulateKey; exports.simulateKeys = simulateKeys; exports.KeyHandlerForCustomizations = KeyHandlerForCustomizations; exports.addKeyCustomizationLayer = addKeyCustomizationLayer; exports.removeKeyCustomizationLayer = removeKeyCustomizationLayer; exports.removeAllKeyCustomizationLayers = removeAllKeyCustomizationLayers; exports.getKeyCustomizationLayer = getKeyCustomizationLayer; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function nameOfCommand(cmd) { if (!cmd) return "no command!"; if (Array.isArray(cmd)) return nameOfCommand(cmd[0]); if (typeof cmd === "string") return cmd; if (cmd.name) return cmd.name; if (cmd.command) return cmd.command; return "unknown command, no name"; } function allEditorCommands(ed) { return flattenedCommandKeyBindings(allCommandKeyBindings(ed)); } function flattenedCommandKeyBindings(allBindings) { var bindings = flatten(allBindings.map(keysAndCommandsOfCommandKeyBinding)); return bindings.reduce(function(map, cmdBnd) { var existing = map[cmdBnd.cmdName]; if (!existing) map[cmdBnd.cmdName] = []; var alreadyAdded = map[cmdBnd.cmdName].some(function(bound) { return bound.bindings === cmdBnd.bindings && bound.cmd === cmdBnd.cmd && bound.key === cmdBnd.key; }); if (!alreadyAdded) map[cmdBnd.cmdName].push(cmdBnd); return map; }, {}); } function keysAndCommandsOfCommandKeyBinding(b) { return Object.keys(b).map(function(k) { var cmd = b[k], keys = k.replace(/cmd-/i, "command-").toLowerCase(); return {key: keys, cmd: cmd, cmdName: nameOfCommand(cmd), bindings: b} }); } function allCommandKeyBindings(ed) { var custHandlers = ed.keyBinding["ace.ext.keys.customized"]; var handlers = getCustomizationAwareKeyHandlers(ed, custHandlers); return [ed.commands.commandKeyBinding].concat( handlers.map(function(h) { if (!h.isEmacs) return h.commandKeyBinding; return Object.keys(h.commandKeyBinding).reduce(function(bnd, k) { var sanitized = k.replace(/cmd-/ig, "command-").replace(/c-/ig, "ctrl-").replace(/m-/ig, "alt-").replace(/s-/ig, "shift-").toLowerCase(); bnd[sanitized] = h.commandKeyBinding[k]; return bnd; }, {}) })); } function convertKeysEmacsToGeneric(keys) { return (keys || "") .replace(/c-/gi, 'ctrl-') .replace(/m-/gi, 'alt-') .replace(/cmd-/gi, 'command-') .replace(/s-/gi, 'shift-'); } function convertKeysGenericToEmacs(keys) { return (keys || "") .replace(/ctrl-/gi, "c-") .replace(/alt-/gi, "m-") .replace(/shift-/gi, "s-") .replace(/command-/gi, "cmd-"); } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // customized key bindings // -=-=-=-=-=-=-=-=-=-=-=-=- function KeyHandlerForCustomizations(bindings) { HashHandler.call(this, bindings || {}); this.fixInputBindings(); } oop.inherits(KeyHandlerForCustomizations, HashHandler); (function() { this.takeOverEmacsBindings = function() { var emacs = ace.require("ace/keyboard/emacs"); var emacsH = emacs && emacs.handler; if (!emacsH) return Object.keys(emacsH.commandKeyBinding).forEach(function(k) { var name = emacsH.commandKeyBinding[k].name || emacsH.commandKeyBinding[k].command || emacsH.commandKeyBinding[k], keys = convertKeysEmacsToGeneric(k); if (!this.commandKeyBinding[keys]) this.bindKey(keys,name); }, this); this.addCommands(emacsH.commands); return this; }; this.fixInputBindings = function() { // huh? // FIXME! some characters like ` can't be used in key combos b/c ace // escapes them strangely var newBnds = this.commandKeyBinding; Object.keys(newBnds).forEach(function(ea) { if (ea.match(/input/)) newBnds[ea.replace(/input/g, '')] = newBnds[ea]; }); return this; }; this.handleKeyboard = function(data, hashId, keyString, keyCode) { var key = keyLib.KEY_MODS[hashId] + keyString; var cmd = this.commandKeyBinding[key]; if (data.$keyChain) { cmd = this.commandKeyBinding[data.$keyChain + " " + key]; } if (!cmd || cmd === 'null') return cmd; if (typeof cmd === 'object') { cmd = {command: cmd.name || cmd.command, args: cmd.args ? clone(cmd.args || cmd.args) : {}} } else cmd = {command: cmd, args: {}}; if (!cmd.command || cmd.command === 'null') return cmd; if (cmd.command == "chainKeys" || cmd.command[cmd.command.length - 1] == "chainKeys") { if (data.$keyChain) data.$keyChain += " " + key; data.$keyChain = data.$keyChain || key; return {command: "null"}; } // deactivate to not steal keyChain if (data.$keyChain && keyCode > 0 && cmd) data.$keyChain = ""; if (keyLib.KEY_MODS[hashId].indexOf("shift-") > -1) cmd.args.shifted = true; if (data.count) { cmd.args.count = data.count; data.count = null; } return cmd; } }).call(KeyHandlerForCustomizations.prototype); function getCustomizationAwareKeyHandlers(ed, customHandlers) { var handlers = ed.keyBinding.$handlers; if (!customizedKeysEnabled(ed)) return handlers; if (customHandlers) { var modeId = ed.session.getMode().$id; if (modeId) customHandlers = customHandlers.filter(function(ea) { return !ea.bindingsSpec.modes || ea.bindingsSpec.modes.indexOf(modeId) > -1; }); handlers = handlers.concat(customHandlers); } return handlers; } function customizedKeysEnabled(ed) { // dynamically checks if it is ok to apply key customizations. right now we // only check if isearch / occur / jump char behavior is active. This might be // extended from the outside var handlers = ed.keyBinding.$handlers; var last = handlers[handlers.length-1]; if (last.$iSearch || last.isOccurHandler || last.isJumpChar || last.isIyGoToChar) return false; if (ed.completer && ed.completer.activated) return false; return true; } function patchData(data) { if (data["patched-by-ace.ext.keys"]) return; data["patched-by-ace.ext.keys"] = true; // keyChain vs $keycHain: emacs handler uses one, the rest uses the latter data.__defineGetter__("keyChain", function() { return convertKeysGenericToEmacs(this.$keyChain); }); data.__defineSetter__("keyChain", function(v) { this.$keyChain = convertKeysEmacsToGeneric(v); return v; }); } function ensureKeyBindingCustomization() { if (!KeyBinding.prototype.$callKeyboardHandlersUnmodified) KeyBinding.prototype.$callKeyboardHandlersUnmodified = KeyBinding.prototype.$callKeyboardHandlers; KeyBinding.prototype.$callKeyboardHandlers = function(hashId, keyString, keyCode, e) { patchData(this.$data); exports.$lastKeyChain = this.$data.$keyChain; var custHandlers = this["ace.ext.keys.customized"]; this.$handlers = getCustomizationAwareKeyHandlers(this.$editor, custHandlers); try { var res = this.$callKeyboardHandlersUnmodified(hashId, keyString, keyCode, e); // if (res) exports.$lastKeyChain = ""; return res; } finally { if (custHandlers) { this.$handlers = this.$handlers.filter(function(h) { return custHandlers.indexOf(h) === -1; }); } } } } // KeyBinding.prototype["ace.ext.keys.customized"] // this.aceEditor.keyBinding.$handlers function getKeyCustomizationLayer(layerName) { var cust = KeyBinding.prototype["ace.ext.keys.customized"] || (KeyBinding.prototype["ace.ext.keys.customized"] = []); var found; for (var i = cust.length-1; i >= 0; i--) { if (cust[i].layerName === layerName) {found = cust[i]; break; }; } return found ? found.bindingsSpec : null; } function removeKeyCustomizationLayer(layerName) { var cust = KeyBinding.prototype["ace.ext.keys.customized"] || []; KeyBinding.prototype["ace.ext.keys.customized"] = cust.filter(function(h) { return h.layerName !== layerName; }); } function removeAllKeyCustomizationLayers() { var layers = KeyBinding.prototype["ace.ext.keys.customized"]; layers && layers.forEach(function(ea) { removeKeyCustomizationLayer(ea.layerName); }); } function addKeyCustomizationLayer(layerName, bindingsSpec) { // ed.session ensureKeyBindingCustomization(); var handler = bindingsSpec.keyHandler || new KeyHandlerForCustomizations(); if (bindingsSpec.commandKeyBinding) handler.bindKeys(bindingsSpec.commandKeyBinding); handler.fixInputBindings && handler.fixInputBindings(); handler.layerName = layerName; handler.bindingsSpec = bindingsSpec; var cust = KeyBinding.prototype["ace.ext.keys.customized"] || []; cust = cust.filter(function(h) { return h.layerName !== layerName; }) .concat([handler]) .sort(function(a,b) { return (a.bindingsSpec.priority || 0) - (b.bindingsSpec.priority || 0); }); KeyBinding.prototype["ace.ext.keys.customized"] = cust; } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // Commands invoke through key actions or otherwise will be captured after // running when while running doFunc and the result returned // *instead* of triggering normal actions. This basically overwrites the // typical ed.commands.exec handler function captureEditorCommand(ed, captureCommandFunc, captureCallFunc) { var origCallKeyboardHandlers = ed.keyBinding.$callKeyboardHandlers, commandExecHandler = ed.commands.addEventListener('exec', function(e) { uninstall(); e.stopPropagation(); e.preventDefault(); captureCommandFunc && captureCommandFunc(e.command, e); return true; }); var orig = ed.keyBinding.$callKeyboardHandlers; ed.keyBinding.$callKeyboardHandlers = function(hashId, keyString, keyCode, e) { captureCallFunc && captureCallFunc(hashId, keyString, keyCode, e); return orig.call(this, hashId, keyString, keyCode, e); }; return uninstall; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function uninstall() { commandExecHandler && ed.commands.removeEventListener('exec', commandExecHandler); ed.keyBinding.$callKeyboardHandlers = orig; } } function lookupKeys(ed, keys) { var result, reset = captureEditorCommand(ed, lookupKeys_captureCommandFunc); simulateKey(ed, keys); reset(); return result function lookupKeys_captureCommandFunc(cmd) { result = cmd; } } // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- var ts = 0; var getModifierHash = useragent.isMac && useragent.isOpera && !("KeyboardEvent" in window) ? function(e) { return 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0); } : function(e) { return 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0); }; exports.getModifierString = function(e) { return keyLib.KEY_MODS[getModifierHash(e)]; }; /** * Computes the "hashId" that is determined by the modifiers pressed. Will call * `callback` only the event qualifies as a "command key". * Will mutate `pressedKeys`! * * @param {Function} callback - Callback that takes parameters event, hashId, keyCode * @param {KeyEvent} keydown or keypress event * @param {Number} character code of the key pressed * @param {Object} pressedKeys - Map of key codes to truthy values for keeping track which keys were pressed * @return {Boolean} return value that'll be returned by the keydown/keypress event handler * **/ exports.normalizeCommandKeys = function normalizeCommandKeys(callback, e, keyCode, pressedKeys) { var hashId = getModifierHash(e); if (!useragent.isMac && pressedKeys) { if (pressedKeys[91] || pressedKeys[92]) hashId |= 8; if (pressedKeys.altGr) { if ((3 & hashId) != 3) pressedKeys.altGr = 0; else return; } if (keyCode === 18 || keyCode === 17) { var location = "location" in e ? e.location : e.keyLocation; if (keyCode === 17 && location === 1) { ts = e.timeStamp; } else if (keyCode === 18 && hashId === 3 && location === 2) { var dt = -ts; ts = e.timeStamp; dt += ts; if (dt < 3) pressedKeys.altGr = true; } } } if (keyCode in keyLib.MODIFIER_KEYS) { switch (keyLib.MODIFIER_KEYS[keyCode]) { case "Alt": hashId = 2; break; case "Shift": hashId = 4; break; case "Ctrl": hashId = 1; break; default: hashId = 8; break; } keyCode = -1; } if (hashId & 8 && (keyCode === 91 || keyCode === 93)) { keyCode = -1; } if (!hashId && keyCode === 13) { var location = "location" in e ? e.location : e.keyLocation; if (location === 3) { callback(e, hashId, -keyCode); if (e.defaultPrevented) return; } } if (useragent.isChromeOS && hashId & 8) { callback(e, hashId, keyCode); if (e.defaultPrevented) return; else hashId &= ~8; } // If there is no hashId and the keyCode is not a function key, then // we don't call the callback as we don't handle a command key here // (it's a normal key/character input). // 2014-03-11 rksm: command key == mac cmd? If just cmd is pressed hashId // will be -1 and callback will be called... please clarify. if (!hashId && !(keyCode in keyLib.FUNCTION_KEYS) && !(keyCode in keyLib.PRINTABLE_KEYS)) { return false; } return callback(e, hashId, keyCode); } var keyCodeCache = {}; function getKeyCodeForKey(key, type) { // reverse mapping, key -> code key = key.toLowerCase(); if (keyCodeCache[key]) return keyCodeCache[key]; var base = keyLib; // "MODIFIER_KEYS","FUNCTION_KEYS","PRINTABLE_KEYS" if (type) base = keyLib[type]; for (var code in base) if (key === base[code].toLowerCase()) return keyCodeCache[key] = typeof code === 'string' ? parseInt(code, 10) : code; } exports.isFunctionKey = function(string) { return !!getKeyCodeForKey(string, 'FUNCTION_KEYS'); }; exports.isModifierKey = function(string) { return !!getKeyCodeForKey(string, 'MODIFIER_KEYS'); }; exports.isPrintableKey = function(string) { return !!getKeyCodeForKey(string, 'PRINTABLE_KEYS'); }; function keySpec(key) { // 1. create a key event object. We first gather what properties need to be // passed to the event creator in terms of the keyboard state var spec = { keyString: '', keyCode: 0, ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, altGraphKey: false, isFunctionKey: false, isModified: false }; // 2. Are any modifier keys pressed? var keyMods = key.split(/[\-]/); var trailing = keyMods.pop(); var modsToEvent = { shift: "shiftKey", control: "ctrlKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey", command: "metaKey", cmd: "metaKey" } keyMods.forEach(function(mod) { var modEventFlag = modsToEvent[mod.toLowerCase()]; if (!modEventFlag) return; spec.isModified = true; spec[modEventFlag] = true; }); // 3. determine the key code and key string of the event. spec.isFunctionKey = exports.isFunctionKey(trailing); if (spec.isFunctionKey) { spec.keyCode = getKeyCodeForKey(trailing, 'FUNCTION_KEYS'); } else if (spec.isModified) { spec.keyCode = trailing.toUpperCase().charCodeAt(0); } else { spec.keyString = trailing; } return spec; } function simulateKey(editor, key) { var spec = keySpec(key); var e = exports.createKeyboardEvent(spec); if (spec.isFunctionKey || spec.isModified) { exports.normalizeCommandKeys(editor.onCommandKey.bind(editor), e, e.keyCode, {}); } if (!e.defaultPrevented && spec.keyString) { editor.onTextInput(spec.keyString); if (editor.session.$syncInformUndoManager) // FIXME for vim editor.session.$syncInformUndoManager(); } }; /* * usage: keys.simulateKeys(editor, "h i t h e e Left r") * (should print "hi there") */ function simulateKeys(editor, keysString) { // there can be multiple pressed keys separated by spaces. To simulate a // space press use a double space. split up the individual keys and // simulate each function ensureSpaces(s) { return s.length ? s : ' '; } var pressedKeys = keysString.length === 1 ? [keysString] : keysString.split(/ /g).map(ensureSpaces) pressedKeys.forEach(exports.simulateKey.bind(null,editor)); }; // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // eventing stuff // -=-=-=-=-=-=-=- function toUnicode(charCode) { var result = charCode.toString(16).toUpperCase(); while (result.length < 4) result = '0' + result; return '\\u' + result; } exports.createKeyboardEvent = function(spec) { var evt = document.createEvent('KeyboardEvent'), id = spec.keyString ? toUnicode(spec.keyString.charCodeAt(0)) : ""; function charCode() { return id.length ? String.fromCharCode(id.replace('\\u', '0x')).charCodeAt(0) : spec.keyCode; } function addProp(name, getter) { try { Object.defineProperty(evt, name, {get: getter}); } catch (e) {} } if (evt.initKeyboardEvent) { // webkit evt.initKeyboardEvent( "keydown", // type true, // canBubble true, // cancelable document.defaultView, // view id, // keyIdentifier 0, // keyLocation spec.ctrlKey, spec.altKey, spec.shiftKey, spec.metaKey, spec.altGraphKey); } else if (evt.initKeyEvent) { // gecko evt.initKeyEvent("keydown", true, true, document.defaultView, spec.ctrlKey, spec.altKey, spec.shiftKey, spec.metaKey, false, false, false, false, charCode(), id); } else { // jsdom evt._type = "keydown"; evt._bubbles = true; evt._cancelable = true; evt._target = document.defaultView; evt._currentTarget = null; evt._keyLocation = 0; evt.defaultPrevented = false; evt.preventDefault = function() { evt.defaultPrevented = true; }; evt.ctrlKey = spec.ctrlKey; evt.shiftKey = spec.shiftKey; evt.altKey = spec.altKey; evt.metaKey = spec.metaKey; evt.altGraphKey = spec.altGraphKey; } evt.keyCode || addProp("keyCode", charCode); evt.which || addProp("which", charCode); evt.keyIdentifier || addProp("keyIdentifier", function() { return id; }); return evt; }; // helpers function clone(obj) { // Shallow copy if (Array.isArray(obj)) return Array.prototype.slice.call(obj); var clone = {}; for (var name in obj) { clone[name] = obj[name]; } return clone; } function flatten(array, optDepth) { if (typeof optDepth === "number") { if (optDepth <= 0) return array; optDepth--; } return array.reduce(function (flattened, value) { return flattened.concat(Array.isArray(value) ? flatten(value, optDepth) : [value]); }, []); } })(window.ace);