1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others |
---|
2 | // Distributed under an MIT license: http://codemirror.net/LICENSE |
---|
3 | |
---|
4 | (function(mod) { |
---|
5 | if (typeof exports == "object" && typeof module == "object") // CommonJS |
---|
6 | mod(require("../../lib/codemirror")); |
---|
7 | else if (typeof define == "function" && define.amd) // AMD |
---|
8 | define(["../../lib/codemirror"], mod); |
---|
9 | else // Plain browser env |
---|
10 | mod(CodeMirror); |
---|
11 | })(function(CodeMirror) { |
---|
12 | "use strict"; |
---|
13 | |
---|
14 | var HINT_ELEMENT_CLASS = "CodeMirror-hint"; |
---|
15 | var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; |
---|
16 | |
---|
17 | // This is the old interface, kept around for now to stay |
---|
18 | // backwards-compatible. |
---|
19 | CodeMirror.showHint = function(cm, getHints, options) { |
---|
20 | if (!getHints) return cm.showHint(options); |
---|
21 | if (options && options.async) getHints.async = true; |
---|
22 | var newOpts = {hint: getHints}; |
---|
23 | if (options) for (var prop in options) newOpts[prop] = options[prop]; |
---|
24 | return cm.showHint(newOpts); |
---|
25 | }; |
---|
26 | |
---|
27 | var asyncRunID = 0; |
---|
28 | function retrieveHints(getter, cm, options, then) { |
---|
29 | if (getter.async) { |
---|
30 | var id = ++asyncRunID; |
---|
31 | getter(cm, function(hints) { |
---|
32 | if (asyncRunID == id) then(hints); |
---|
33 | }, options); |
---|
34 | } else { |
---|
35 | then(getter(cm, options)); |
---|
36 | } |
---|
37 | } |
---|
38 | |
---|
39 | CodeMirror.defineExtension("showHint", function(options) { |
---|
40 | // We want a single cursor position. |
---|
41 | if (this.listSelections().length > 1 || this.somethingSelected()) return; |
---|
42 | |
---|
43 | if (this.state.completionActive) this.state.completionActive.close(); |
---|
44 | var completion = this.state.completionActive = new Completion(this, options); |
---|
45 | var getHints = completion.options.hint; |
---|
46 | if (!getHints) return; |
---|
47 | |
---|
48 | CodeMirror.signal(this, "startCompletion", this); |
---|
49 | return retrieveHints(getHints, this, completion.options, function(hints) { completion.showHints(hints); }); |
---|
50 | }); |
---|
51 | |
---|
52 | function Completion(cm, options) { |
---|
53 | this.cm = cm; |
---|
54 | this.options = this.buildOptions(options); |
---|
55 | this.widget = this.onClose = null; |
---|
56 | } |
---|
57 | |
---|
58 | Completion.prototype = { |
---|
59 | close: function() { |
---|
60 | if (!this.active()) return; |
---|
61 | this.cm.state.completionActive = null; |
---|
62 | |
---|
63 | if (this.widget) this.widget.close(); |
---|
64 | if (this.onClose) this.onClose(); |
---|
65 | CodeMirror.signal(this.cm, "endCompletion", this.cm); |
---|
66 | }, |
---|
67 | |
---|
68 | active: function() { |
---|
69 | return this.cm.state.completionActive == this; |
---|
70 | }, |
---|
71 | |
---|
72 | pick: function(data, i) { |
---|
73 | var completion = data.list[i]; |
---|
74 | if (completion.hint) completion.hint(this.cm, data, completion); |
---|
75 | else this.cm.replaceRange(getText(completion), completion.from || data.from, |
---|
76 | completion.to || data.to, "complete"); |
---|
77 | CodeMirror.signal(data, "pick", completion); |
---|
78 | this.close(); |
---|
79 | }, |
---|
80 | |
---|
81 | showHints: function(data) { |
---|
82 | if (!data || !data.list.length || !this.active()) return this.close(); |
---|
83 | |
---|
84 | if (this.options.completeSingle && data.list.length == 1) |
---|
85 | this.pick(data, 0); |
---|
86 | else |
---|
87 | this.showWidget(data); |
---|
88 | }, |
---|
89 | |
---|
90 | showWidget: function(data) { |
---|
91 | this.widget = new Widget(this, data); |
---|
92 | CodeMirror.signal(data, "shown"); |
---|
93 | |
---|
94 | var debounce = 0, completion = this, finished; |
---|
95 | var closeOn = this.options.closeCharacters; |
---|
96 | var startPos = this.cm.getCursor(), startLen = this.cm.getLine(startPos.line).length; |
---|
97 | |
---|
98 | var requestAnimationFrame = window.requestAnimationFrame || function(fn) { |
---|
99 | return setTimeout(fn, 1000/60); |
---|
100 | }; |
---|
101 | var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; |
---|
102 | |
---|
103 | function done() { |
---|
104 | if (finished) return; |
---|
105 | finished = true; |
---|
106 | completion.close(); |
---|
107 | completion.cm.off("cursorActivity", activity); |
---|
108 | if (data) CodeMirror.signal(data, "close"); |
---|
109 | } |
---|
110 | |
---|
111 | function update() { |
---|
112 | if (finished) return; |
---|
113 | CodeMirror.signal(data, "update"); |
---|
114 | retrieveHints(completion.options.hint, completion.cm, completion.options, finishUpdate); |
---|
115 | } |
---|
116 | function finishUpdate(data_) { |
---|
117 | data = data_; |
---|
118 | if (finished) return; |
---|
119 | if (!data || !data.list.length) return done(); |
---|
120 | if (completion.widget) completion.widget.close(); |
---|
121 | completion.widget = new Widget(completion, data); |
---|
122 | } |
---|
123 | |
---|
124 | function clearDebounce() { |
---|
125 | if (debounce) { |
---|
126 | cancelAnimationFrame(debounce); |
---|
127 | debounce = 0; |
---|
128 | } |
---|
129 | } |
---|
130 | |
---|
131 | function activity() { |
---|
132 | clearDebounce(); |
---|
133 | var pos = completion.cm.getCursor(), line = completion.cm.getLine(pos.line); |
---|
134 | if (pos.line != startPos.line || line.length - pos.ch != startLen - startPos.ch || |
---|
135 | pos.ch < startPos.ch || completion.cm.somethingSelected() || |
---|
136 | (pos.ch && closeOn.test(line.charAt(pos.ch - 1)))) { |
---|
137 | completion.close(); |
---|
138 | } else { |
---|
139 | debounce = requestAnimationFrame(update); |
---|
140 | if (completion.widget) completion.widget.close(); |
---|
141 | } |
---|
142 | } |
---|
143 | this.cm.on("cursorActivity", activity); |
---|
144 | this.onClose = done; |
---|
145 | }, |
---|
146 | |
---|
147 | buildOptions: function(options) { |
---|
148 | var editor = this.cm.options.hintOptions; |
---|
149 | var out = {}; |
---|
150 | for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; |
---|
151 | if (editor) for (var prop in editor) |
---|
152 | if (editor[prop] !== undefined) out[prop] = editor[prop]; |
---|
153 | if (options) for (var prop in options) |
---|
154 | if (options[prop] !== undefined) out[prop] = options[prop]; |
---|
155 | return out; |
---|
156 | } |
---|
157 | }; |
---|
158 | |
---|
159 | function getText(completion) { |
---|
160 | if (typeof completion == "string") return completion; |
---|
161 | else return completion.text; |
---|
162 | } |
---|
163 | |
---|
164 | function buildKeyMap(completion, handle) { |
---|
165 | var baseMap = { |
---|
166 | Up: function() {handle.moveFocus(-1);}, |
---|
167 | Down: function() {handle.moveFocus(1);}, |
---|
168 | PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, |
---|
169 | PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, |
---|
170 | Home: function() {handle.setFocus(0);}, |
---|
171 | End: function() {handle.setFocus(handle.length - 1);}, |
---|
172 | Enter: handle.pick, |
---|
173 | Tab: handle.pick, |
---|
174 | Esc: handle.close |
---|
175 | }; |
---|
176 | var custom = completion.options.customKeys; |
---|
177 | var ourMap = custom ? {} : baseMap; |
---|
178 | function addBinding(key, val) { |
---|
179 | var bound; |
---|
180 | if (typeof val != "string") |
---|
181 | bound = function(cm) { return val(cm, handle); }; |
---|
182 | // This mechanism is deprecated |
---|
183 | else if (baseMap.hasOwnProperty(val)) |
---|
184 | bound = baseMap[val]; |
---|
185 | else |
---|
186 | bound = val; |
---|
187 | ourMap[key] = bound; |
---|
188 | } |
---|
189 | if (custom) |
---|
190 | for (var key in custom) if (custom.hasOwnProperty(key)) |
---|
191 | addBinding(key, custom[key]); |
---|
192 | var extra = completion.options.extraKeys; |
---|
193 | if (extra) |
---|
194 | for (var key in extra) if (extra.hasOwnProperty(key)) |
---|
195 | addBinding(key, extra[key]); |
---|
196 | return ourMap; |
---|
197 | } |
---|
198 | |
---|
199 | function getHintElement(hintsElement, el) { |
---|
200 | while (el && el != hintsElement) { |
---|
201 | if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; |
---|
202 | el = el.parentNode; |
---|
203 | } |
---|
204 | } |
---|
205 | |
---|
206 | function Widget(completion, data) { |
---|
207 | this.completion = completion; |
---|
208 | this.data = data; |
---|
209 | var widget = this, cm = completion.cm; |
---|
210 | |
---|
211 | var hints = this.hints = document.createElement("ul"); |
---|
212 | hints.className = "CodeMirror-hints"; |
---|
213 | this.selectedHint = data.selectedHint || 0; |
---|
214 | |
---|
215 | var completions = data.list; |
---|
216 | for (var i = 0; i < completions.length; ++i) { |
---|
217 | var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; |
---|
218 | var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); |
---|
219 | if (cur.className != null) className = cur.className + " " + className; |
---|
220 | elt.className = className; |
---|
221 | if (cur.render) cur.render(elt, data, cur); |
---|
222 | else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); |
---|
223 | elt.hintId = i; |
---|
224 | } |
---|
225 | |
---|
226 | var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); |
---|
227 | var left = pos.left, top = pos.bottom, below = true; |
---|
228 | hints.style.left = left + "px"; |
---|
229 | hints.style.top = top + "px"; |
---|
230 | // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. |
---|
231 | var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); |
---|
232 | var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); |
---|
233 | (completion.options.container || document.body).appendChild(hints); |
---|
234 | var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; |
---|
235 | if (overlapY > 0) { |
---|
236 | var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); |
---|
237 | if (curTop - height > 0) { // Fits above cursor |
---|
238 | hints.style.top = (top = pos.top - height) + "px"; |
---|
239 | below = false; |
---|
240 | } else if (height > winH) { |
---|
241 | hints.style.height = (winH - 5) + "px"; |
---|
242 | hints.style.top = (top = pos.bottom - box.top) + "px"; |
---|
243 | var cursor = cm.getCursor(); |
---|
244 | if (data.from.ch != cursor.ch) { |
---|
245 | pos = cm.cursorCoords(cursor); |
---|
246 | hints.style.left = (left = pos.left) + "px"; |
---|
247 | box = hints.getBoundingClientRect(); |
---|
248 | } |
---|
249 | } |
---|
250 | } |
---|
251 | var overlapX = box.right - winW; |
---|
252 | if (overlapX > 0) { |
---|
253 | if (box.right - box.left > winW) { |
---|
254 | hints.style.width = (winW - 5) + "px"; |
---|
255 | overlapX -= (box.right - box.left) - winW; |
---|
256 | } |
---|
257 | hints.style.left = (left = pos.left - overlapX) + "px"; |
---|
258 | } |
---|
259 | |
---|
260 | cm.addKeyMap(this.keyMap = buildKeyMap(completion, { |
---|
261 | moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, |
---|
262 | setFocus: function(n) { widget.changeActive(n); }, |
---|
263 | menuSize: function() { return widget.screenAmount(); }, |
---|
264 | length: completions.length, |
---|
265 | close: function() { completion.close(); }, |
---|
266 | pick: function() { widget.pick(); }, |
---|
267 | data: data |
---|
268 | })); |
---|
269 | |
---|
270 | if (completion.options.closeOnUnfocus) { |
---|
271 | var closingOnBlur; |
---|
272 | cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); |
---|
273 | cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); |
---|
274 | } |
---|
275 | |
---|
276 | var startScroll = cm.getScrollInfo(); |
---|
277 | cm.on("scroll", this.onScroll = function() { |
---|
278 | var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); |
---|
279 | var newTop = top + startScroll.top - curScroll.top; |
---|
280 | var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); |
---|
281 | if (!below) point += hints.offsetHeight; |
---|
282 | if (point <= editor.top || point >= editor.bottom) return completion.close(); |
---|
283 | hints.style.top = newTop + "px"; |
---|
284 | hints.style.left = (left + startScroll.left - curScroll.left) + "px"; |
---|
285 | }); |
---|
286 | |
---|
287 | CodeMirror.on(hints, "dblclick", function(e) { |
---|
288 | var t = getHintElement(hints, e.target || e.srcElement); |
---|
289 | if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} |
---|
290 | }); |
---|
291 | |
---|
292 | CodeMirror.on(hints, "click", function(e) { |
---|
293 | var t = getHintElement(hints, e.target || e.srcElement); |
---|
294 | if (t && t.hintId != null) { |
---|
295 | widget.changeActive(t.hintId); |
---|
296 | if (completion.options.completeOnSingleClick) widget.pick(); |
---|
297 | } |
---|
298 | }); |
---|
299 | |
---|
300 | CodeMirror.on(hints, "mousedown", function() { |
---|
301 | setTimeout(function(){cm.focus();}, 20); |
---|
302 | }); |
---|
303 | |
---|
304 | CodeMirror.signal(data, "select", completions[0], hints.firstChild); |
---|
305 | return true; |
---|
306 | } |
---|
307 | |
---|
308 | Widget.prototype = { |
---|
309 | close: function() { |
---|
310 | if (this.completion.widget != this) return; |
---|
311 | this.completion.widget = null; |
---|
312 | this.hints.parentNode.removeChild(this.hints); |
---|
313 | this.completion.cm.removeKeyMap(this.keyMap); |
---|
314 | |
---|
315 | var cm = this.completion.cm; |
---|
316 | if (this.completion.options.closeOnUnfocus) { |
---|
317 | cm.off("blur", this.onBlur); |
---|
318 | cm.off("focus", this.onFocus); |
---|
319 | } |
---|
320 | cm.off("scroll", this.onScroll); |
---|
321 | }, |
---|
322 | |
---|
323 | pick: function() { |
---|
324 | this.completion.pick(this.data, this.selectedHint); |
---|
325 | }, |
---|
326 | |
---|
327 | changeActive: function(i, avoidWrap) { |
---|
328 | if (i >= this.data.list.length) |
---|
329 | i = avoidWrap ? this.data.list.length - 1 : 0; |
---|
330 | else if (i < 0) |
---|
331 | i = avoidWrap ? 0 : this.data.list.length - 1; |
---|
332 | if (this.selectedHint == i) return; |
---|
333 | var node = this.hints.childNodes[this.selectedHint]; |
---|
334 | node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); |
---|
335 | node = this.hints.childNodes[this.selectedHint = i]; |
---|
336 | node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; |
---|
337 | if (node.offsetTop < this.hints.scrollTop) |
---|
338 | this.hints.scrollTop = node.offsetTop - 3; |
---|
339 | else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) |
---|
340 | this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; |
---|
341 | CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); |
---|
342 | }, |
---|
343 | |
---|
344 | screenAmount: function() { |
---|
345 | return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; |
---|
346 | } |
---|
347 | }; |
---|
348 | |
---|
349 | CodeMirror.registerHelper("hint", "auto", function(cm, options) { |
---|
350 | var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; |
---|
351 | if (helpers.length) { |
---|
352 | for (var i = 0; i < helpers.length; i++) { |
---|
353 | var cur = helpers[i](cm, options); |
---|
354 | if (cur && cur.list.length) return cur; |
---|
355 | } |
---|
356 | } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { |
---|
357 | if (words) return CodeMirror.hint.fromList(cm, {words: words}); |
---|
358 | } else if (CodeMirror.hint.anyword) { |
---|
359 | return CodeMirror.hint.anyword(cm, options); |
---|
360 | } |
---|
361 | }); |
---|
362 | |
---|
363 | CodeMirror.registerHelper("hint", "fromList", function(cm, options) { |
---|
364 | var cur = cm.getCursor(), token = cm.getTokenAt(cur); |
---|
365 | var found = []; |
---|
366 | for (var i = 0; i < options.words.length; i++) { |
---|
367 | var word = options.words[i]; |
---|
368 | if (word.slice(0, token.string.length) == token.string) |
---|
369 | found.push(word); |
---|
370 | } |
---|
371 | |
---|
372 | if (found.length) return { |
---|
373 | list: found, |
---|
374 | from: CodeMirror.Pos(cur.line, token.start), |
---|
375 | to: CodeMirror.Pos(cur.line, token.end) |
---|
376 | }; |
---|
377 | }); |
---|
378 | |
---|
379 | CodeMirror.commands.autocomplete = CodeMirror.showHint; |
---|
380 | |
---|
381 | var defaultOptions = { |
---|
382 | hint: CodeMirror.hint.auto, |
---|
383 | completeSingle: true, |
---|
384 | alignWithWord: true, |
---|
385 | closeCharacters: /[\s()\[\]{};:>,]/, |
---|
386 | closeOnUnfocus: true, |
---|
387 | completeOnSingleClick: false, |
---|
388 | container: null, |
---|
389 | customKeys: null, |
---|
390 | extraKeys: null |
---|
391 | }; |
---|
392 | |
---|
393 | CodeMirror.defineOption("hintOptions", null); |
---|
394 | }); |
---|