151 lines
5.7 KiB
JavaScript
151 lines
5.7 KiB
JavaScript
|
|
(function () {
|
||
|
|
// Minimal event-handling wrapper.
|
||
|
|
function stopEvent() {
|
||
|
|
if (this.preventDefault) {this.preventDefault(); this.stopPropagation();}
|
||
|
|
else {this.returnValue = false; this.cancelBubble = true;}
|
||
|
|
}
|
||
|
|
function addStop(event) {
|
||
|
|
if (!event.stop) event.stop = stopEvent;
|
||
|
|
return event;
|
||
|
|
}
|
||
|
|
function connect(node, type, handler) {
|
||
|
|
function wrapHandler(event) {handler(addStop(event || window.event));}
|
||
|
|
if (typeof node.addEventListener == "function")
|
||
|
|
node.addEventListener(type, wrapHandler, false);
|
||
|
|
else
|
||
|
|
node.attachEvent("on" + type, wrapHandler);
|
||
|
|
}
|
||
|
|
|
||
|
|
function forEach(arr, f) {
|
||
|
|
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
|
||
|
|
}
|
||
|
|
|
||
|
|
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
|
||
|
|
lineNumbers: true,
|
||
|
|
onKeyEvent: function(i, e) {
|
||
|
|
// Hook into ctrl-space
|
||
|
|
if (e.keyCode == 32 && (e.ctrlKey || e.metaKey) && !e.altKey) {
|
||
|
|
e.stop();
|
||
|
|
return startComplete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function startComplete() {
|
||
|
|
// We want a single cursor position.
|
||
|
|
if (editor.somethingSelected()) return;
|
||
|
|
// Find the token at the cursor
|
||
|
|
var cur = editor.getCursor(false), token = editor.getTokenAt(cur), tprop = token;
|
||
|
|
// If it's not a 'word-style' token, ignore the token.
|
||
|
|
if (!/^[\w$_]*$/.test(token.string)) {
|
||
|
|
token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state,
|
||
|
|
className: token.string == "." ? "js-property" : null};
|
||
|
|
}
|
||
|
|
// If it is a property, find out what it is a property of.
|
||
|
|
while (tprop.className == "js-property") {
|
||
|
|
tprop = editor.getTokenAt({line: cur.line, ch: tprop.start});
|
||
|
|
if (tprop.string != ".") return;
|
||
|
|
tprop = editor.getTokenAt({line: cur.line, ch: tprop.start});
|
||
|
|
if (!context) var context = [];
|
||
|
|
context.push(tprop);
|
||
|
|
}
|
||
|
|
var completions = getCompletions(token, context);
|
||
|
|
if (!completions.length) return;
|
||
|
|
function insert(str) {
|
||
|
|
editor.replaceRange(str, {line: cur.line, ch: token.start}, {line: cur.line, ch: token.end});
|
||
|
|
}
|
||
|
|
// When there is only one completion, use it directly.
|
||
|
|
if (completions.length == 1) {insert(completions[0]); return true;}
|
||
|
|
|
||
|
|
// Build the select widget
|
||
|
|
var complete = document.createElement("div");
|
||
|
|
complete.className = "completions";
|
||
|
|
var sel = complete.appendChild(document.createElement("select"));
|
||
|
|
sel.multiple = true;
|
||
|
|
for (var i = 0; i < completions.length; ++i) {
|
||
|
|
var opt = sel.appendChild(document.createElement("option"));
|
||
|
|
opt.appendChild(document.createTextNode(completions[i]));
|
||
|
|
}
|
||
|
|
sel.firstChild.selected = true;
|
||
|
|
sel.size = Math.min(10, completions.length);
|
||
|
|
var pos = editor.cursorCoords();
|
||
|
|
complete.style.left = pos.x + "px";
|
||
|
|
complete.style.top = pos.yBot + "px";
|
||
|
|
document.body.appendChild(complete);
|
||
|
|
// Hack to hide the scrollbar.
|
||
|
|
if (completions.length <= 10)
|
||
|
|
complete.style.width = (sel.clientWidth - 1) + "px";
|
||
|
|
|
||
|
|
var done = false;
|
||
|
|
function close() {
|
||
|
|
if (done) return;
|
||
|
|
done = true;
|
||
|
|
complete.parentNode.removeChild(complete);
|
||
|
|
}
|
||
|
|
function pick() {
|
||
|
|
insert(sel.options[sel.selectedIndex].value);
|
||
|
|
close();
|
||
|
|
setTimeout(function(){editor.focus();}, 50);
|
||
|
|
}
|
||
|
|
connect(sel, "blur", close);
|
||
|
|
connect(sel, "keydown", function(event) {
|
||
|
|
var code = event.keyCode;
|
||
|
|
// Enter and space
|
||
|
|
if (code == 13 || code == 32) {event.stop(); pick();}
|
||
|
|
// Escape
|
||
|
|
else if (code == 27) {event.stop(); close(); editor.focus();}
|
||
|
|
else if (code != 38 && code != 40) {close(); editor.focus(); setTimeout(startComplete, 50);}
|
||
|
|
});
|
||
|
|
connect(sel, "dblclick", pick);
|
||
|
|
|
||
|
|
sel.focus();
|
||
|
|
// Opera sometimes ignores focusing a freshly created node
|
||
|
|
if (window.opera) setTimeout(function(){if (!done) sel.focus();}, 100);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
|
||
|
|
"toUpperCase toLowerCase split concat match replace search").split(" ");
|
||
|
|
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
|
||
|
|
"lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
|
||
|
|
var funcProps = "prototype apply call bind".split(" ");
|
||
|
|
var keywords = ("break case catch continue debugger default delete do else false finally for function " +
|
||
|
|
"if in instanceof new null return switch throw true try typeof var void while with").split(" ");
|
||
|
|
|
||
|
|
function getCompletions(token, context) {
|
||
|
|
var found = [], start = token.string;
|
||
|
|
function maybeAdd(str) {
|
||
|
|
if (str.indexOf(start) == 0) found.push(str);
|
||
|
|
}
|
||
|
|
function gatherCompletions(obj) {
|
||
|
|
if (typeof obj == "string") forEach(stringProps, maybeAdd);
|
||
|
|
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
|
||
|
|
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
|
||
|
|
for (var name in obj) maybeAdd(name);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (context) {
|
||
|
|
// If this is a property, see if it belongs to some object we can
|
||
|
|
// find in the current environment.
|
||
|
|
var obj = context.pop(), base;
|
||
|
|
if (obj.className == "js-variable")
|
||
|
|
base = window[obj.string];
|
||
|
|
else if (obj.className == "js-string")
|
||
|
|
base = "";
|
||
|
|
else if (obj.className == "js-atom")
|
||
|
|
base = 1;
|
||
|
|
while (base != null && context.length)
|
||
|
|
base = base[context.pop().string];
|
||
|
|
if (base != null) gatherCompletions(base);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
// If not, just look in the window object and any local scope
|
||
|
|
// (reading into JS mode internals to get at the local variables)
|
||
|
|
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
|
||
|
|
gatherCompletions(window);
|
||
|
|
forEach(keywords, maybeAdd);
|
||
|
|
}
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
})();
|