Mercurial
comparison .cms/lib/codemirror/addon/tern/tern.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others | |
2 // Distributed under an MIT license: https://codemirror.net/5/LICENSE | |
3 | |
4 // Glue code between CodeMirror and Tern. | |
5 // | |
6 // Create a CodeMirror.TernServer to wrap an actual Tern server, | |
7 // register open documents (CodeMirror.Doc instances) with it, and | |
8 // call its methods to activate the assisting functions that Tern | |
9 // provides. | |
10 // | |
11 // Options supported (all optional): | |
12 // * defs: An array of JSON definition data structures. | |
13 // * plugins: An object mapping plugin names to configuration | |
14 // options. | |
15 // * getFile: A function(name, c) that can be used to access files in | |
16 // the project that haven't been loaded yet. Simply do c(null) to | |
17 // indicate that a file is not available. | |
18 // * fileFilter: A function(value, docName, doc) that will be applied | |
19 // to documents before passing them on to Tern. | |
20 // * switchToDoc: A function(name, doc) that should, when providing a | |
21 // multi-file view, switch the view or focus to the named file. | |
22 // * showError: A function(editor, message) that can be used to | |
23 // override the way errors are displayed. | |
24 // * completionTip: Customize the content in tooltips for completions. | |
25 // Is passed a single argument—the completion's data as returned by | |
26 // Tern—and may return a string, DOM node, or null to indicate that | |
27 // no tip should be shown. By default the docstring is shown. | |
28 // * typeTip: Like completionTip, but for the tooltips shown for type | |
29 // queries. | |
30 // * responseFilter: A function(doc, query, request, error, data) that | |
31 // will be applied to the Tern responses before treating them | |
32 // | |
33 // | |
34 // It is possible to run the Tern server in a web worker by specifying | |
35 // these additional options: | |
36 // * useWorker: Set to true to enable web worker mode. You'll probably | |
37 // want to feature detect the actual value you use here, for example | |
38 // !!window.Worker. | |
39 // * workerScript: The main script of the worker. Point this to | |
40 // wherever you are hosting worker.js from this directory. | |
41 // * workerDeps: An array of paths pointing (relative to workerScript) | |
42 // to the Acorn and Tern libraries and any Tern plugins you want to | |
43 // load. Or, if you minified those into a single script and included | |
44 // them in the workerScript, simply leave this undefined. | |
45 | |
46 (function(mod) { | |
47 if (typeof exports == "object" && typeof module == "object") // CommonJS | |
48 mod(require("../../lib/codemirror")); | |
49 else if (typeof define == "function" && define.amd) // AMD | |
50 define(["../../lib/codemirror"], mod); | |
51 else // Plain browser env | |
52 mod(CodeMirror); | |
53 })(function(CodeMirror) { | |
54 "use strict"; | |
55 // declare global: tern | |
56 | |
57 CodeMirror.TernServer = function(options) { | |
58 var self = this; | |
59 this.options = options || {}; | |
60 var plugins = this.options.plugins || (this.options.plugins = {}); | |
61 if (!plugins.doc_comment) plugins.doc_comment = true; | |
62 this.docs = Object.create(null); | |
63 if (this.options.useWorker) { | |
64 this.server = new WorkerServer(this); | |
65 } else { | |
66 this.server = new tern.Server({ | |
67 getFile: function(name, c) { return getFile(self, name, c); }, | |
68 async: true, | |
69 defs: this.options.defs || [], | |
70 plugins: plugins | |
71 }); | |
72 } | |
73 this.trackChange = function(doc, change) { trackChange(self, doc, change); }; | |
74 | |
75 this.cachedArgHints = null; | |
76 this.activeArgHints = null; | |
77 this.jumpStack = []; | |
78 | |
79 this.getHint = function(cm, c) { return hint(self, cm, c); }; | |
80 this.getHint.async = true; | |
81 }; | |
82 | |
83 CodeMirror.TernServer.prototype = { | |
84 addDoc: function(name, doc) { | |
85 var data = {doc: doc, name: name, changed: null}; | |
86 this.server.addFile(name, docValue(this, data)); | |
87 CodeMirror.on(doc, "change", this.trackChange); | |
88 return this.docs[name] = data; | |
89 }, | |
90 | |
91 delDoc: function(id) { | |
92 var found = resolveDoc(this, id); | |
93 if (!found) return; | |
94 CodeMirror.off(found.doc, "change", this.trackChange); | |
95 delete this.docs[found.name]; | |
96 this.server.delFile(found.name); | |
97 }, | |
98 | |
99 hideDoc: function(id) { | |
100 closeArgHints(this); | |
101 var found = resolveDoc(this, id); | |
102 if (found && found.changed) sendDoc(this, found); | |
103 }, | |
104 | |
105 complete: function(cm) { | |
106 cm.showHint({hint: this.getHint}); | |
107 }, | |
108 | |
109 showType: function(cm, pos, c) { showContextInfo(this, cm, pos, "type", c); }, | |
110 | |
111 showDocs: function(cm, pos, c) { showContextInfo(this, cm, pos, "documentation", c); }, | |
112 | |
113 updateArgHints: function(cm) { updateArgHints(this, cm); }, | |
114 | |
115 jumpToDef: function(cm) { jumpToDef(this, cm); }, | |
116 | |
117 jumpBack: function(cm) { jumpBack(this, cm); }, | |
118 | |
119 rename: function(cm) { rename(this, cm); }, | |
120 | |
121 selectName: function(cm) { selectName(this, cm); }, | |
122 | |
123 request: function (cm, query, c, pos) { | |
124 var self = this; | |
125 var doc = findDoc(this, cm.getDoc()); | |
126 var request = buildRequest(this, doc, query, pos); | |
127 var extraOptions = request.query && this.options.queryOptions && this.options.queryOptions[request.query.type] | |
128 if (extraOptions) for (var prop in extraOptions) request.query[prop] = extraOptions[prop]; | |
129 | |
130 this.server.request(request, function (error, data) { | |
131 if (!error && self.options.responseFilter) | |
132 data = self.options.responseFilter(doc, query, request, error, data); | |
133 c(error, data); | |
134 }); | |
135 }, | |
136 | |
137 destroy: function () { | |
138 closeArgHints(this) | |
139 if (this.worker) { | |
140 this.worker.terminate(); | |
141 this.worker = null; | |
142 } | |
143 } | |
144 }; | |
145 | |
146 var Pos = CodeMirror.Pos; | |
147 var cls = "CodeMirror-Tern-"; | |
148 var bigDoc = 250; | |
149 | |
150 function getFile(ts, name, c) { | |
151 var buf = ts.docs[name]; | |
152 if (buf) | |
153 c(docValue(ts, buf)); | |
154 else if (ts.options.getFile) | |
155 ts.options.getFile(name, c); | |
156 else | |
157 c(null); | |
158 } | |
159 | |
160 function findDoc(ts, doc, name) { | |
161 for (var n in ts.docs) { | |
162 var cur = ts.docs[n]; | |
163 if (cur.doc == doc) return cur; | |
164 } | |
165 if (!name) for (var i = 0;; ++i) { | |
166 n = "[doc" + (i || "") + "]"; | |
167 if (!ts.docs[n]) { name = n; break; } | |
168 } | |
169 return ts.addDoc(name, doc); | |
170 } | |
171 | |
172 function resolveDoc(ts, id) { | |
173 if (typeof id == "string") return ts.docs[id]; | |
174 if (id instanceof CodeMirror) id = id.getDoc(); | |
175 if (id instanceof CodeMirror.Doc) return findDoc(ts, id); | |
176 } | |
177 | |
178 function trackChange(ts, doc, change) { | |
179 var data = findDoc(ts, doc); | |
180 | |
181 var argHints = ts.cachedArgHints; | |
182 if (argHints && argHints.doc == doc && cmpPos(argHints.start, change.to) >= 0) | |
183 ts.cachedArgHints = null; | |
184 | |
185 var changed = data.changed; | |
186 if (changed == null) | |
187 data.changed = changed = {from: change.from.line, to: change.from.line}; | |
188 var end = change.from.line + (change.text.length - 1); | |
189 if (change.from.line < changed.to) changed.to = changed.to - (change.to.line - end); | |
190 if (end >= changed.to) changed.to = end + 1; | |
191 if (changed.from > change.from.line) changed.from = change.from.line; | |
192 | |
193 if (doc.lineCount() > bigDoc && change.to - changed.from > 100) setTimeout(function() { | |
194 if (data.changed && data.changed.to - data.changed.from > 100) sendDoc(ts, data); | |
195 }, 200); | |
196 } | |
197 | |
198 function sendDoc(ts, doc) { | |
199 ts.server.request({files: [{type: "full", name: doc.name, text: docValue(ts, doc)}]}, function(error) { | |
200 if (error) window.console.error(error); | |
201 else doc.changed = null; | |
202 }); | |
203 } | |
204 | |
205 // Completion | |
206 | |
207 function hint(ts, cm, c) { | |
208 ts.request(cm, {type: "completions", types: true, docs: true, urls: true}, function(error, data) { | |
209 if (error) return showError(ts, cm, error); | |
210 var completions = [], after = ""; | |
211 var from = data.start, to = data.end; | |
212 if (cm.getRange(Pos(from.line, from.ch - 2), from) == "[\"" && | |
213 cm.getRange(to, Pos(to.line, to.ch + 2)) != "\"]") | |
214 after = "\"]"; | |
215 | |
216 for (var i = 0; i < data.completions.length; ++i) { | |
217 var completion = data.completions[i], className = typeToIcon(completion.type); | |
218 if (data.guess) className += " " + cls + "guess"; | |
219 completions.push({text: completion.name + after, | |
220 displayText: completion.displayName || completion.name, | |
221 className: className, | |
222 data: completion}); | |
223 } | |
224 | |
225 var obj = {from: from, to: to, list: completions}; | |
226 var tooltip = null; | |
227 CodeMirror.on(obj, "close", function() { remove(tooltip); }); | |
228 CodeMirror.on(obj, "update", function() { remove(tooltip); }); | |
229 CodeMirror.on(obj, "select", function(cur, node) { | |
230 remove(tooltip); | |
231 var content = ts.options.completionTip ? ts.options.completionTip(cur.data) : cur.data.doc; | |
232 if (content) { | |
233 tooltip = makeTooltip(node.parentNode.getBoundingClientRect().right + window.pageXOffset, | |
234 node.getBoundingClientRect().top + window.pageYOffset, content, cm, cls + "hint-doc"); | |
235 } | |
236 }); | |
237 c(obj); | |
238 }); | |
239 } | |
240 | |
241 function typeToIcon(type) { | |
242 var suffix; | |
243 if (type == "?") suffix = "unknown"; | |
244 else if (type == "number" || type == "string" || type == "bool") suffix = type; | |
245 else if (/^fn\(/.test(type)) suffix = "fn"; | |
246 else if (/^\[/.test(type)) suffix = "array"; | |
247 else suffix = "object"; | |
248 return cls + "completion " + cls + "completion-" + suffix; | |
249 } | |
250 | |
251 // Type queries | |
252 | |
253 function showContextInfo(ts, cm, pos, queryName, c) { | |
254 ts.request(cm, queryName, function(error, data) { | |
255 if (error) return showError(ts, cm, error); | |
256 if (ts.options.typeTip) { | |
257 var tip = ts.options.typeTip(data); | |
258 } else { | |
259 var tip = elt("span", null, elt("strong", null, data.type || "not found")); | |
260 if (data.doc) | |
261 tip.appendChild(document.createTextNode(" — " + data.doc)); | |
262 if (data.url) { | |
263 tip.appendChild(document.createTextNode(" ")); | |
264 var child = tip.appendChild(elt("a", null, "[docs]")); | |
265 child.href = data.url; | |
266 child.target = "_blank"; | |
267 } | |
268 } | |
269 tempTooltip(cm, tip, ts); | |
270 if (c) c(); | |
271 }, pos); | |
272 } | |
273 | |
274 // Maintaining argument hints | |
275 | |
276 function updateArgHints(ts, cm) { | |
277 closeArgHints(ts); | |
278 | |
279 if (cm.somethingSelected()) return; | |
280 var state = cm.getTokenAt(cm.getCursor()).state; | |
281 var inner = CodeMirror.innerMode(cm.getMode(), state); | |
282 if (inner.mode.name != "javascript") return; | |
283 var lex = inner.state.lexical; | |
284 if (lex.info != "call") return; | |
285 | |
286 var ch, argPos = lex.pos || 0, tabSize = cm.getOption("tabSize"); | |
287 for (var line = cm.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) { | |
288 var str = cm.getLine(line), extra = 0; | |
289 for (var pos = 0;;) { | |
290 var tab = str.indexOf("\t", pos); | |
291 if (tab == -1) break; | |
292 extra += tabSize - (tab + extra) % tabSize - 1; | |
293 pos = tab + 1; | |
294 } | |
295 ch = lex.column - extra; | |
296 if (str.charAt(ch) == "(") {found = true; break;} | |
297 } | |
298 if (!found) return; | |
299 | |
300 var start = Pos(line, ch); | |
301 var cache = ts.cachedArgHints; | |
302 if (cache && cache.doc == cm.getDoc() && cmpPos(start, cache.start) == 0) | |
303 return showArgHints(ts, cm, argPos); | |
304 | |
305 ts.request(cm, {type: "type", preferFunction: true, end: start}, function(error, data) { | |
306 if (error || !data.type || !(/^fn\(/).test(data.type)) return; | |
307 ts.cachedArgHints = { | |
308 start: start, | |
309 type: parseFnType(data.type), | |
310 name: data.exprName || data.name || "fn", | |
311 guess: data.guess, | |
312 doc: cm.getDoc() | |
313 }; | |
314 showArgHints(ts, cm, argPos); | |
315 }); | |
316 } | |
317 | |
318 function showArgHints(ts, cm, pos) { | |
319 closeArgHints(ts); | |
320 | |
321 var cache = ts.cachedArgHints, tp = cache.type; | |
322 var tip = elt("span", cache.guess ? cls + "fhint-guess" : null, | |
323 elt("span", cls + "fname", cache.name), "("); | |
324 for (var i = 0; i < tp.args.length; ++i) { | |
325 if (i) tip.appendChild(document.createTextNode(", ")); | |
326 var arg = tp.args[i]; | |
327 tip.appendChild(elt("span", cls + "farg" + (i == pos ? " " + cls + "farg-current" : ""), arg.name || "?")); | |
328 if (arg.type != "?") { | |
329 tip.appendChild(document.createTextNode(":\u00a0")); | |
330 tip.appendChild(elt("span", cls + "type", arg.type)); | |
331 } | |
332 } | |
333 tip.appendChild(document.createTextNode(tp.rettype ? ") ->\u00a0" : ")")); | |
334 if (tp.rettype) tip.appendChild(elt("span", cls + "type", tp.rettype)); | |
335 var place = cm.cursorCoords(null, "page"); | |
336 var tooltip = ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip, cm) | |
337 setTimeout(function() { | |
338 tooltip.clear = onEditorActivity(cm, function() { | |
339 if (ts.activeArgHints == tooltip) closeArgHints(ts) }) | |
340 }, 20) | |
341 } | |
342 | |
343 function parseFnType(text) { | |
344 var args = [], pos = 3; | |
345 | |
346 function skipMatching(upto) { | |
347 var depth = 0, start = pos; | |
348 for (;;) { | |
349 var next = text.charAt(pos); | |
350 if (upto.test(next) && !depth) return text.slice(start, pos); | |
351 if (/[{\[\(]/.test(next)) ++depth; | |
352 else if (/[}\]\)]/.test(next)) --depth; | |
353 ++pos; | |
354 } | |
355 } | |
356 | |
357 // Parse arguments | |
358 if (text.charAt(pos) != ")") for (;;) { | |
359 var name = text.slice(pos).match(/^([^, \(\[\{]+): /); | |
360 if (name) { | |
361 pos += name[0].length; | |
362 name = name[1]; | |
363 } | |
364 args.push({name: name, type: skipMatching(/[\),]/)}); | |
365 if (text.charAt(pos) == ")") break; | |
366 pos += 2; | |
367 } | |
368 | |
369 var rettype = text.slice(pos).match(/^\) -> (.*)$/); | |
370 | |
371 return {args: args, rettype: rettype && rettype[1]}; | |
372 } | |
373 | |
374 // Moving to the definition of something | |
375 | |
376 function jumpToDef(ts, cm) { | |
377 function inner(varName) { | |
378 var req = {type: "definition", variable: varName || null}; | |
379 var doc = findDoc(ts, cm.getDoc()); | |
380 ts.server.request(buildRequest(ts, doc, req), function(error, data) { | |
381 if (error) return showError(ts, cm, error); | |
382 if (!data.file && data.url) { window.open(data.url); return; } | |
383 | |
384 if (data.file) { | |
385 var localDoc = ts.docs[data.file], found; | |
386 if (localDoc && (found = findContext(localDoc.doc, data))) { | |
387 ts.jumpStack.push({file: doc.name, | |
388 start: cm.getCursor("from"), | |
389 end: cm.getCursor("to")}); | |
390 moveTo(ts, doc, localDoc, found.start, found.end); | |
391 return; | |
392 } | |
393 } | |
394 showError(ts, cm, "Could not find a definition."); | |
395 }); | |
396 } | |
397 | |
398 if (!atInterestingExpression(cm)) | |
399 dialog(cm, "Jump to variable", function(name) { if (name) inner(name); }); | |
400 else | |
401 inner(); | |
402 } | |
403 | |
404 function jumpBack(ts, cm) { | |
405 var pos = ts.jumpStack.pop(), doc = pos && ts.docs[pos.file]; | |
406 if (!doc) return; | |
407 moveTo(ts, findDoc(ts, cm.getDoc()), doc, pos.start, pos.end); | |
408 } | |
409 | |
410 function moveTo(ts, curDoc, doc, start, end) { | |
411 doc.doc.setSelection(start, end); | |
412 if (curDoc != doc && ts.options.switchToDoc) { | |
413 closeArgHints(ts); | |
414 ts.options.switchToDoc(doc.name, doc.doc); | |
415 } | |
416 } | |
417 | |
418 // The {line,ch} representation of positions makes this rather awkward. | |
419 function findContext(doc, data) { | |
420 var before = data.context.slice(0, data.contextOffset).split("\n"); | |
421 var startLine = data.start.line - (before.length - 1); | |
422 var start = Pos(startLine, (before.length == 1 ? data.start.ch : doc.getLine(startLine).length) - before[0].length); | |
423 | |
424 var text = doc.getLine(startLine).slice(start.ch); | |
425 for (var cur = startLine + 1; cur < doc.lineCount() && text.length < data.context.length; ++cur) | |
426 text += "\n" + doc.getLine(cur); | |
427 if (text.slice(0, data.context.length) == data.context) return data; | |
428 | |
429 var cursor = doc.getSearchCursor(data.context, 0, false); | |
430 var nearest, nearestDist = Infinity; | |
431 while (cursor.findNext()) { | |
432 var from = cursor.from(), dist = Math.abs(from.line - start.line) * 10000; | |
433 if (!dist) dist = Math.abs(from.ch - start.ch); | |
434 if (dist < nearestDist) { nearest = from; nearestDist = dist; } | |
435 } | |
436 if (!nearest) return null; | |
437 | |
438 if (before.length == 1) | |
439 nearest.ch += before[0].length; | |
440 else | |
441 nearest = Pos(nearest.line + (before.length - 1), before[before.length - 1].length); | |
442 if (data.start.line == data.end.line) | |
443 var end = Pos(nearest.line, nearest.ch + (data.end.ch - data.start.ch)); | |
444 else | |
445 var end = Pos(nearest.line + (data.end.line - data.start.line), data.end.ch); | |
446 return {start: nearest, end: end}; | |
447 } | |
448 | |
449 function atInterestingExpression(cm) { | |
450 var pos = cm.getCursor("end"), tok = cm.getTokenAt(pos); | |
451 if (tok.start < pos.ch && tok.type == "comment") return false; | |
452 return /[\w)\]]/.test(cm.getLine(pos.line).slice(Math.max(pos.ch - 1, 0), pos.ch + 1)); | |
453 } | |
454 | |
455 // Variable renaming | |
456 | |
457 function rename(ts, cm) { | |
458 var token = cm.getTokenAt(cm.getCursor()); | |
459 if (!/\w/.test(token.string)) return showError(ts, cm, "Not at a variable"); | |
460 dialog(cm, "New name for " + token.string, function(newName) { | |
461 ts.request(cm, {type: "rename", newName: newName, fullDocs: true}, function(error, data) { | |
462 if (error) return showError(ts, cm, error); | |
463 applyChanges(ts, data.changes); | |
464 }); | |
465 }); | |
466 } | |
467 | |
468 function selectName(ts, cm) { | |
469 var name = findDoc(ts, cm.doc).name; | |
470 ts.request(cm, {type: "refs"}, function(error, data) { | |
471 if (error) return showError(ts, cm, error); | |
472 var ranges = [], cur = 0; | |
473 var curPos = cm.getCursor(); | |
474 for (var i = 0; i < data.refs.length; i++) { | |
475 var ref = data.refs[i]; | |
476 if (ref.file == name) { | |
477 ranges.push({anchor: ref.start, head: ref.end}); | |
478 if (cmpPos(curPos, ref.start) >= 0 && cmpPos(curPos, ref.end) <= 0) | |
479 cur = ranges.length - 1; | |
480 } | |
481 } | |
482 cm.setSelections(ranges, cur); | |
483 }); | |
484 } | |
485 | |
486 var nextChangeOrig = 0; | |
487 function applyChanges(ts, changes) { | |
488 var perFile = Object.create(null); | |
489 for (var i = 0; i < changes.length; ++i) { | |
490 var ch = changes[i]; | |
491 (perFile[ch.file] || (perFile[ch.file] = [])).push(ch); | |
492 } | |
493 for (var file in perFile) { | |
494 var known = ts.docs[file], chs = perFile[file];; | |
495 if (!known) continue; | |
496 chs.sort(function(a, b) { return cmpPos(b.start, a.start); }); | |
497 var origin = "*rename" + (++nextChangeOrig); | |
498 for (var i = 0; i < chs.length; ++i) { | |
499 var ch = chs[i]; | |
500 known.doc.replaceRange(ch.text, ch.start, ch.end, origin); | |
501 } | |
502 } | |
503 } | |
504 | |
505 // Generic request-building helper | |
506 | |
507 function buildRequest(ts, doc, query, pos) { | |
508 var files = [], offsetLines = 0, allowFragments = !query.fullDocs; | |
509 if (!allowFragments) delete query.fullDocs; | |
510 if (typeof query == "string") query = {type: query}; | |
511 query.lineCharPositions = true; | |
512 if (query.end == null) { | |
513 query.end = pos || doc.doc.getCursor("end"); | |
514 if (doc.doc.somethingSelected()) | |
515 query.start = doc.doc.getCursor("start"); | |
516 } | |
517 var startPos = query.start || query.end; | |
518 | |
519 if (doc.changed) { | |
520 if (doc.doc.lineCount() > bigDoc && allowFragments !== false && | |
521 doc.changed.to - doc.changed.from < 100 && | |
522 doc.changed.from <= startPos.line && doc.changed.to > query.end.line) { | |
523 files.push(getFragmentAround(doc, startPos, query.end)); | |
524 query.file = "#0"; | |
525 var offsetLines = files[0].offsetLines; | |
526 if (query.start != null) query.start = Pos(query.start.line - -offsetLines, query.start.ch); | |
527 query.end = Pos(query.end.line - offsetLines, query.end.ch); | |
528 } else { | |
529 files.push({type: "full", | |
530 name: doc.name, | |
531 text: docValue(ts, doc)}); | |
532 query.file = doc.name; | |
533 doc.changed = null; | |
534 } | |
535 } else { | |
536 query.file = doc.name; | |
537 } | |
538 for (var name in ts.docs) { | |
539 var cur = ts.docs[name]; | |
540 if (cur.changed && cur != doc) { | |
541 files.push({type: "full", name: cur.name, text: docValue(ts, cur)}); | |
542 cur.changed = null; | |
543 } | |
544 } | |
545 | |
546 return {query: query, files: files}; | |
547 } | |
548 | |
549 function getFragmentAround(data, start, end) { | |
550 var doc = data.doc; | |
551 var minIndent = null, minLine = null, endLine, tabSize = 4; | |
552 for (var p = start.line - 1, min = Math.max(0, p - 50); p >= min; --p) { | |
553 var line = doc.getLine(p), fn = line.search(/\bfunction\b/); | |
554 if (fn < 0) continue; | |
555 var indent = CodeMirror.countColumn(line, null, tabSize); | |
556 if (minIndent != null && minIndent <= indent) continue; | |
557 minIndent = indent; | |
558 minLine = p; | |
559 } | |
560 if (minLine == null) minLine = min; | |
561 var max = Math.min(doc.lastLine(), end.line + 20); | |
562 if (minIndent == null || minIndent == CodeMirror.countColumn(doc.getLine(start.line), null, tabSize)) | |
563 endLine = max; | |
564 else for (endLine = end.line + 1; endLine < max; ++endLine) { | |
565 var indent = CodeMirror.countColumn(doc.getLine(endLine), null, tabSize); | |
566 if (indent <= minIndent) break; | |
567 } | |
568 var from = Pos(minLine, 0); | |
569 | |
570 return {type: "part", | |
571 name: data.name, | |
572 offsetLines: from.line, | |
573 text: doc.getRange(from, Pos(endLine, end.line == endLine ? null : 0))}; | |
574 } | |
575 | |
576 // Generic utilities | |
577 | |
578 var cmpPos = CodeMirror.cmpPos; | |
579 | |
580 function elt(tagname, cls /*, ... elts*/) { | |
581 var e = document.createElement(tagname); | |
582 if (cls) e.className = cls; | |
583 for (var i = 2; i < arguments.length; ++i) { | |
584 var elt = arguments[i]; | |
585 if (typeof elt == "string") elt = document.createTextNode(elt); | |
586 e.appendChild(elt); | |
587 } | |
588 return e; | |
589 } | |
590 | |
591 function dialog(cm, text, f) { | |
592 if (cm.openDialog) { | |
593 var fragment = document.createDocumentFragment(); | |
594 fragment.appendChild(document.createTextNode(text + ": ")); | |
595 var input = document.createElement("input"); | |
596 input.type = "text"; | |
597 fragment.appendChild(input); | |
598 cm.openDialog(fragment, f); | |
599 } else { | |
600 f(prompt(text, "")); | |
601 } | |
602 } | |
603 | |
604 // Tooltips | |
605 | |
606 function tempTooltip(cm, content, ts) { | |
607 if (cm.state.ternTooltip) remove(cm.state.ternTooltip); | |
608 var where = cm.cursorCoords(); | |
609 var tip = cm.state.ternTooltip = makeTooltip(where.right + 1, where.bottom, content, cm); | |
610 function maybeClear() { | |
611 old = true; | |
612 if (!mouseOnTip) clear(); | |
613 } | |
614 function clear() { | |
615 cm.state.ternTooltip = null; | |
616 if (tip.parentNode) fadeOut(tip) | |
617 clearActivity() | |
618 } | |
619 var mouseOnTip = false, old = false; | |
620 CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; }); | |
621 CodeMirror.on(tip, "mouseout", function(e) { | |
622 var related = e.relatedTarget || e.toElement | |
623 if (!related || !CodeMirror.contains(tip, related)) { | |
624 if (old) clear(); | |
625 else mouseOnTip = false; | |
626 } | |
627 }); | |
628 setTimeout(maybeClear, ts.options.hintDelay ? ts.options.hintDelay : 1700); | |
629 var clearActivity = onEditorActivity(cm, clear) | |
630 } | |
631 | |
632 function onEditorActivity(cm, f) { | |
633 cm.on("cursorActivity", f) | |
634 cm.on("blur", f) | |
635 cm.on("scroll", f) | |
636 cm.on("setDoc", f) | |
637 return function() { | |
638 cm.off("cursorActivity", f) | |
639 cm.off("blur", f) | |
640 cm.off("scroll", f) | |
641 cm.off("setDoc", f) | |
642 } | |
643 } | |
644 | |
645 function makeTooltip(x, y, content, cm, className) { | |
646 var node = elt("div", cls + "tooltip" + " " + (className || ""), content); | |
647 node.style.left = x + "px"; | |
648 node.style.top = y + "px"; | |
649 var container = ((cm.options || {}).hintOptions || {}).container || document.body; | |
650 container.appendChild(node); | |
651 | |
652 var pos = cm.cursorCoords(); | |
653 var winW = window.innerWidth; | |
654 var winH = window.innerHeight; | |
655 var box = node.getBoundingClientRect(); | |
656 var hints = document.querySelector(".CodeMirror-hints"); | |
657 var overlapY = box.bottom - winH; | |
658 var overlapX = box.right - winW; | |
659 | |
660 if (hints && overlapX > 0) { | |
661 node.style.left = 0; | |
662 var box = node.getBoundingClientRect(); | |
663 node.style.left = (x = x - hints.offsetWidth - box.width) + "px"; | |
664 overlapX = box.right - winW; | |
665 } | |
666 if (overlapY > 0) { | |
667 var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); | |
668 if (curTop - height > 0) { // Fits above cursor | |
669 node.style.top = (pos.top - height) + "px"; | |
670 } else if (height > winH) { | |
671 node.style.height = (winH - 5) + "px"; | |
672 node.style.top = (pos.bottom - box.top) + "px"; | |
673 } | |
674 } | |
675 if (overlapX > 0) { | |
676 if (box.right - box.left > winW) { | |
677 node.style.width = (winW - 5) + "px"; | |
678 overlapX -= (box.right - box.left) - winW; | |
679 } | |
680 node.style.left = (x - overlapX) + "px"; | |
681 } | |
682 | |
683 return node; | |
684 } | |
685 | |
686 function remove(node) { | |
687 var p = node && node.parentNode; | |
688 if (p) p.removeChild(node); | |
689 } | |
690 | |
691 function fadeOut(tooltip) { | |
692 tooltip.style.opacity = "0"; | |
693 setTimeout(function() { remove(tooltip); }, 1100); | |
694 } | |
695 | |
696 function showError(ts, cm, msg) { | |
697 if (ts.options.showError) | |
698 ts.options.showError(cm, msg); | |
699 else | |
700 tempTooltip(cm, String(msg), ts); | |
701 } | |
702 | |
703 function closeArgHints(ts) { | |
704 if (ts.activeArgHints) { | |
705 if (ts.activeArgHints.clear) ts.activeArgHints.clear() | |
706 remove(ts.activeArgHints) | |
707 ts.activeArgHints = null | |
708 } | |
709 } | |
710 | |
711 function docValue(ts, doc) { | |
712 var val = doc.doc.getValue(); | |
713 if (ts.options.fileFilter) val = ts.options.fileFilter(val, doc.name, doc.doc); | |
714 return val; | |
715 } | |
716 | |
717 // Worker wrapper | |
718 | |
719 function WorkerServer(ts) { | |
720 var worker = ts.worker = new Worker(ts.options.workerScript); | |
721 worker.postMessage({type: "init", | |
722 defs: ts.options.defs, | |
723 plugins: ts.options.plugins, | |
724 scripts: ts.options.workerDeps}); | |
725 var msgId = 0, pending = {}; | |
726 | |
727 function send(data, c) { | |
728 if (c) { | |
729 data.id = ++msgId; | |
730 pending[msgId] = c; | |
731 } | |
732 worker.postMessage(data); | |
733 } | |
734 worker.onmessage = function(e) { | |
735 var data = e.data; | |
736 if (data.type == "getFile") { | |
737 getFile(ts, data.name, function(err, text) { | |
738 send({type: "getFile", err: String(err), text: text, id: data.id}); | |
739 }); | |
740 } else if (data.type == "debug") { | |
741 window.console.log(data.message); | |
742 } else if (data.id && pending[data.id]) { | |
743 pending[data.id](data.err, data.body); | |
744 delete pending[data.id]; | |
745 } | |
746 }; | |
747 worker.onerror = function(e) { | |
748 for (var id in pending) pending[id](e); | |
749 pending = {}; | |
750 }; | |
751 | |
752 this.addFile = function(name, text) { send({type: "add", name: name, text: text}); }; | |
753 this.delFile = function(name) { send({type: "del", name: name}); }; | |
754 this.request = function(body, c) { send({type: "req", body: body}, c); }; | |
755 } | |
756 }); |