complete.js
5.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
(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,
theme: "night",
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;
}
})();