ace.improved
Version:
For a better ace editor
596 lines (512 loc) • 20.3 kB
JavaScript
(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);