xm
2024-06-14 722af26bc6fec32bb289b1df51a9016a4935610f
提交 | 用户 | 时间
722af2 1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
X 2 // Distributed under an MIT license: https://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   CodeMirror.defineExtension("showHint", function(options) {
28     options = parseOptions(this, this.getCursor("start"), options);
29     var selections = this.listSelections()
30     if (selections.length > 1) return;
31     // By default, don't allow completion when something is selected.
32     // A hint function can have a `supportsSelection` property to
33     // indicate that it can handle selections.
34     if (this.somethingSelected()) {
35       if (!options.hint.supportsSelection) return;
36       // Don't try with cross-line selections
37       for (var i = 0; i < selections.length; i++)
38         if (selections[i].head.line != selections[i].anchor.line) return;
39     }
40
41     if (this.state.completionActive) this.state.completionActive.close();
42     var completion = this.state.completionActive = new Completion(this, options);
43     if (!completion.options.hint) return;
44
45     CodeMirror.signal(this, "startCompletion", this);
46     completion.update(true);
47   });
48
49   function Completion(cm, options) {
50     this.cm = cm;
51     this.options = options;
52     this.widget = null;
53     this.debounce = 0;
54     this.tick = 0;
55     this.startPos = this.cm.getCursor("start");
56     this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
57
58     var self = this;
59     cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
60   }
61
62   var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
63     return setTimeout(fn, 1000/60);
64   };
65   var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
66
67   Completion.prototype = {
68     close: function() {
69       if (!this.active()) return;
70       this.cm.state.completionActive = null;
71       this.tick = null;
72       this.cm.off("cursorActivity", this.activityFunc);
73
74       if (this.widget && this.data) CodeMirror.signal(this.data, "close");
75       if (this.widget) this.widget.close();
76       CodeMirror.signal(this.cm, "endCompletion", this.cm);
77     },
78
79     active: function() {
80       return this.cm.state.completionActive == this;
81     },
82
83     pick: function(data, i) {
84       var completion = data.list[i];
85       if (completion.hint) completion.hint(this.cm, data, completion);
86       else this.cm.replaceRange(getText(completion), completion.from || data.from,
87                                 completion.to || data.to, "complete");
88       CodeMirror.signal(data, "pick", completion);
89       this.close();
90     },
91
92     cursorActivity: function() {
93       if (this.debounce) {
94         cancelAnimationFrame(this.debounce);
95         this.debounce = 0;
96       }
97
98       var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
99       if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
100           pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
101           (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
102         this.close();
103       } else {
104         var self = this;
105         this.debounce = requestAnimationFrame(function() {self.update();});
106         if (this.widget) this.widget.disable();
107       }
108     },
109
110     update: function(first) {
111       if (this.tick == null) return
112       var self = this, myTick = ++this.tick
113       fetchHints(this.options.hint, this.cm, this.options, function(data) {
114         if (self.tick == myTick) self.finishUpdate(data, first)
115       })
116     },
117
118     finishUpdate: function(data, first) {
119       if (this.data) CodeMirror.signal(this.data, "update");
120
121       var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
122       if (this.widget) this.widget.close();
123
124       this.data = data;
125
126       if (data && data.list.length) {
127         if (picked && data.list.length == 1) {
128           this.pick(data, 0);
129         } else {
130           this.widget = new Widget(this, data);
131           CodeMirror.signal(data, "shown");
132         }
133       }
134     }
135   };
136
137   function parseOptions(cm, pos, options) {
138     var editor = cm.options.hintOptions;
139     var out = {};
140     for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
141     if (editor) for (var prop in editor)
142       if (editor[prop] !== undefined) out[prop] = editor[prop];
143     if (options) for (var prop in options)
144       if (options[prop] !== undefined) out[prop] = options[prop];
145     if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
146     return out;
147   }
148
149   function getText(completion) {
150     if (typeof completion == "string") return completion;
151     else return completion.text;
152   }
153
154   function buildKeyMap(completion, handle) {
155     var baseMap = {
156       Up: function() {handle.moveFocus(-1);},
157       Down: function() {handle.moveFocus(1);},
158       PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
159       PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
160       Home: function() {handle.setFocus(0);},
161       End: function() {handle.setFocus(handle.length - 1);},
162       Enter: handle.pick,
163       Tab: handle.pick,
164       Esc: handle.close
165     };
166     var custom = completion.options.customKeys;
167     var ourMap = custom ? {} : baseMap;
168     function addBinding(key, val) {
169       var bound;
170       if (typeof val != "string")
171         bound = function(cm) { return val(cm, handle); };
172       // This mechanism is deprecated
173       else if (baseMap.hasOwnProperty(val))
174         bound = baseMap[val];
175       else
176         bound = val;
177       ourMap[key] = bound;
178     }
179     if (custom)
180       for (var key in custom) if (custom.hasOwnProperty(key))
181         addBinding(key, custom[key]);
182     var extra = completion.options.extraKeys;
183     if (extra)
184       for (var key in extra) if (extra.hasOwnProperty(key))
185         addBinding(key, extra[key]);
186     return ourMap;
187   }
188
189   function getHintElement(hintsElement, el) {
190     while (el && el != hintsElement) {
191       if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
192       el = el.parentNode;
193     }
194   }
195
196   function Widget(completion, data) {
197     this.completion = completion;
198     this.data = data;
199     this.picked = false;
200     var widget = this, cm = completion.cm;
201
202     var hints = this.hints = document.createElement("ul");
203     var theme = completion.cm.options.theme;
204     hints.className = "CodeMirror-hints " + theme;
205     this.selectedHint = data.selectedHint || 0;
206
207     var completions = data.list;
208     for (var i = 0; i < completions.length; ++i) {
209       var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
210       var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
211       if (cur.className != null) className = cur.className + " " + className;
212       elt.className = className;
213       if (cur.render) cur.render(elt, data, cur);
214       else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
215       elt.hintId = i;
216     }
217
218     var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
219     var left = pos.left, top = pos.bottom, below = true;
220     hints.style.left = left + "px";
221     hints.style.top = top + "px";
222     // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
223     var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
224     var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
225     (completion.options.container || document.body).appendChild(hints);
226     var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
227     var scrolls = hints.scrollHeight > hints.clientHeight + 1
228     var startScroll = cm.getScrollInfo();
229
230     if (overlapY > 0) {
231       var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
232       if (curTop - height > 0) { // Fits above cursor
233         hints.style.top = (top = pos.top - height) + "px";
234         below = false;
235       } else if (height > winH) {
236         hints.style.height = (winH - 5) + "px";
237         hints.style.top = (top = pos.bottom - box.top) + "px";
238         var cursor = cm.getCursor();
239         if (data.from.ch != cursor.ch) {
240           pos = cm.cursorCoords(cursor);
241           hints.style.left = (left = pos.left) + "px";
242           box = hints.getBoundingClientRect();
243         }
244       }
245     }
246     var overlapX = box.right - winW;
247     if (overlapX > 0) {
248       if (box.right - box.left > winW) {
249         hints.style.width = (winW - 5) + "px";
250         overlapX -= (box.right - box.left) - winW;
251       }
252       hints.style.left = (left = pos.left - overlapX) + "px";
253     }
254     if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
255       node.style.paddingRight = cm.display.nativeBarWidth + "px"
256
257     cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
258       moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
259       setFocus: function(n) { widget.changeActive(n); },
260       menuSize: function() { return widget.screenAmount(); },
261       length: completions.length,
262       close: function() { completion.close(); },
263       pick: function() { widget.pick(); },
264       data: data
265     }));
266
267     if (completion.options.closeOnUnfocus) {
268       var closingOnBlur;
269       cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
270       cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
271     }
272
273     cm.on("scroll", this.onScroll = function() {
274       var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
275       var newTop = top + startScroll.top - curScroll.top;
276       var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
277       if (!below) point += hints.offsetHeight;
278       if (point <= editor.top || point >= editor.bottom) return completion.close();
279       hints.style.top = newTop + "px";
280       hints.style.left = (left + startScroll.left - curScroll.left) + "px";
281     });
282
283     CodeMirror.on(hints, "dblclick", function(e) {
284       var t = getHintElement(hints, e.target || e.srcElement);
285       if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
286     });
287
288     CodeMirror.on(hints, "click", function(e) {
289       var t = getHintElement(hints, e.target || e.srcElement);
290       if (t && t.hintId != null) {
291         widget.changeActive(t.hintId);
292         if (completion.options.completeOnSingleClick) widget.pick();
293       }
294     });
295
296     CodeMirror.on(hints, "mousedown", function() {
297       setTimeout(function(){cm.focus();}, 20);
298     });
299
300     CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
301     return true;
302   }
303
304   Widget.prototype = {
305     close: function() {
306       if (this.completion.widget != this) return;
307       this.completion.widget = null;
308       this.hints.parentNode.removeChild(this.hints);
309       this.completion.cm.removeKeyMap(this.keyMap);
310
311       var cm = this.completion.cm;
312       if (this.completion.options.closeOnUnfocus) {
313         cm.off("blur", this.onBlur);
314         cm.off("focus", this.onFocus);
315       }
316       cm.off("scroll", this.onScroll);
317     },
318
319     disable: function() {
320       this.completion.cm.removeKeyMap(this.keyMap);
321       var widget = this;
322       this.keyMap = {Enter: function() { widget.picked = true; }};
323       this.completion.cm.addKeyMap(this.keyMap);
324     },
325
326     pick: function() {
327       this.completion.pick(this.data, this.selectedHint);
328     },
329
330     changeActive: function(i, avoidWrap) {
331       if (i >= this.data.list.length)
332         i = avoidWrap ? this.data.list.length - 1 : 0;
333       else if (i < 0)
334         i = avoidWrap ? 0  : this.data.list.length - 1;
335       if (this.selectedHint == i) return;
336       var node = this.hints.childNodes[this.selectedHint];
337       if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
338       node = this.hints.childNodes[this.selectedHint = i];
339       node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
340       if (node.offsetTop < this.hints.scrollTop)
341         this.hints.scrollTop = node.offsetTop - 3;
342       else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
343         this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
344       CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
345     },
346
347     screenAmount: function() {
348       return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
349     }
350   };
351
352   function applicableHelpers(cm, helpers) {
353     if (!cm.somethingSelected()) return helpers
354     var result = []
355     for (var i = 0; i < helpers.length; i++)
356       if (helpers[i].supportsSelection) result.push(helpers[i])
357     return result
358   }
359
360   function fetchHints(hint, cm, options, callback) {
361     if (hint.async) {
362       hint(cm, callback, options)
363     } else {
364       var result = hint(cm, options)
365       if (result && result.then) result.then(callback)
366       else callback(result)
367     }
368   }
369
370   function resolveAutoHints(cm, pos) {
371     var helpers = cm.getHelpers(pos, "hint"), words
372     if (helpers.length) {
373       var resolved = function(cm, callback, options) {
374         var app = applicableHelpers(cm, helpers);
375         function run(i) {
376           if (i == app.length) return callback(null)
377           fetchHints(app[i], cm, options, function(result) {
378             if (result && result.list.length > 0) callback(result)
379             else run(i + 1)
380           })
381         }
382         run(0)
383       }
384       resolved.async = true
385       resolved.supportsSelection = true
386       return resolved
387     } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
388       return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
389     } else if (CodeMirror.hint.anyword) {
390       return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
391     } else {
392       return function() {}
393     }
394   }
395
396   CodeMirror.registerHelper("hint", "auto", {
397     resolve: resolveAutoHints
398   });
399
400   CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
401     var cur = cm.getCursor(), token = cm.getTokenAt(cur)
402     var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
403     if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
404       term = token.string.substr(0, cur.ch - token.start)
405     } else {
406       term = ""
407       from = cur
408     }
409     var found = [];
410     for (var i = 0; i < options.words.length; i++) {
411       var word = options.words[i];
412       if (word.slice(0, term.length) == term)
413         found.push(word);
414     }
415
416     if (found.length) return {list: found, from: from, to: to};
417   });
418
419   CodeMirror.commands.autocomplete = CodeMirror.showHint;
420
421   var defaultOptions = {
422     hint: CodeMirror.hint.auto,
423     completeSingle: true,
424     alignWithWord: true,
425     closeCharacters: /[\s()\[\]{};:>,]/,
426     closeOnUnfocus: true,
427     completeOnSingleClick: true,
428     container: null,
429     customKeys: null,
430     extraKeys: null
431   };
432
433   CodeMirror.defineOption("hintOptions", null);
434 });