c9-core/plugins/c9.ide.language.core/tooltip.js

337 wiersze
13 KiB
JavaScript

/**
* Cloud9 Language Foundation
*
* @copyright 2013, Ajax.org B.V.
*/
define(function(require, exports, module) {
main.consumes = [
"Plugin", "tabManager", "language", "ui", "ace", "util"
];
main.provides = ["language.tooltip"];
return main;
function main(options, imports, register) {
var language = imports.language;
var tabs = imports.tabManager;
var dom = require("ace/lib/dom");
var Plugin = imports.Plugin;
var ui = imports.ui;
var aceHandle = imports.ace;
var tree = require("treehugger/tree");
var assert = require("c9/assert");
var SyntaxDetector = require("./syntax_detector");
var util = imports.util;
var plugin = new Plugin("Ajax.org", main.consumes);
var ace, languageWorker, isVisible, labelHeight, adjustCompleterTop;
var isTopdown, tooltipEl, allowImmediateEmit, lastPos;
var cursormoveTimeout, onMouseDownTimeout;
var tooltipRegexes = {};
var lastCompletionTooltip = {};
function load() {
tooltipEl = dom.createElement("div");
tooltipEl.className = "language_tooltip dark";
language.getWorker(function(err, worker) {
if (err) return console.error(err);
languageWorker = worker;
worker.on("hint", function(event) {
var tab = tabs.focussedTab;
if (!tab || tab.path !== event.data.path)
return;
assert(tab.editor && tab.editor.ace, "Could find a tab but no editor for " + event.data.path);
onHint(event, tab.editor.ace);
}, plugin);
worker.on("tooltipRegex", function(event) {
tooltipRegexes[event.data.language] = event.data.tooltipRegex;
}, plugin);
language.on("cursormove", function(e) {
clearTimeout(cursormoveTimeout);
if (e.selection.rangeCount || !tabs.focussedTab)
return hide();
if (lastPos && !inRange(lastPos, e.pos)) {
// Just walked outside of tooltip range
if (lastPos.sl !== e.pos.row)
hide();
if (allowImmediateEmit) {
allowImmediateEmit = false;
return setTimeout(function() { // send after any change event
worker.emit("cursormove", { data: { pos: e.pos, line: e.doc.getLine(e.pos.row) }});
}, 0);
}
}
applyLastCompletionTooltip();
cursormoveTimeout = setTimeout(function() {
var latestPos = e.doc.selection.getCursor();
worker.emit("cursormove", { data: { pos: latestPos, line: e.doc.getLine(latestPos.row), now: e.now }});
cursormoveTimeout = null;
}, e.now ? 0 : 100);
}, plugin);
});
aceHandle.on("themeChange", function(e) {
var theme = e.theme;
if (!theme) return;
tooltipEl.className = "language_tooltip "
+ (theme.isDark ? "dark" : "");
}, plugin);
}
plugin.on("load", function() {
load();
});
plugin.on("unload", function() {
if (tooltipEl && tooltipEl.parentNode)
tooltipEl.parentNode.removeChild(tooltipEl);
});
function onHint(event, ace) {
var message = event.data.message;
var pos = event.data.pos;
var cursorPos = ace.getCursorPosition();
var line = ace.getSession().getDocument().getLine(cursorPos.row);
if (message && message.signatures) {
message = message.signatures.map(function(sig) {
var activeParam;
var doc = sig.name + "("
+ sig.parameters.map(function(p) {
if (p.active)
activeParam = p;
return p.active
? '<span class="language_activeparam">'
+ util.escapeXml(p.name)
+ "</span>"
: '<span class="language_param">' + util.escapeXml(p.name) + "</span>";
}).join(", ")
+ ")";
if (sig.returnType)
doc += " : " + util.escapeXml(sig.returnType);
if (activeParam && activeParam.type) {
doc += '&nbsp;&mdash; <span class="language_type">'
+ util.escapeXml(activeParam.name)
+ ':&nbsp;' + util.escapeXml(activeParam.type)
+ "</span>";
}
if (activeParam && (activeParam.docHtml || activeParam.doc)) {
doc += '<div class="language_paramhelp">'
// + '<span class="language_activeparamindent">' + fnName + '(</span>'
+ '<span class="language_activeparam">' + util.escapeXml(activeParam.name) + '</span>:'
+ '<span class="language_activeparamhelp">' + (activeParam.docHtml || util.escapeXml(activeParam.doc || activeParam.type)) + '</span></div>';
}
return doc;
}).join("<br />");
}
clearTimeout(onMouseDownTimeout);
if (ace.selection.rangeCount || !ace.selection.isEmpty())
return hide();
if (line !== event.data.line) {
// console.warn("Got outdated tooltip event from worker, retrying");
if (!cursormoveTimeout)
cursormoveTimeout = setTimeout(function() {
language.onCursorChange();
cursormoveTimeout = null;
}, 50);
if (lastPos && lastPos.sl !== cursorPos.row)
hide();
return;
}
if (message && inRange(pos, cursorPos)) {
var displayPos = event.data.displayPos || cursorPos;
show(displayPos.row, displayPos.column, message, ace);
lastPos = pos;
allowImmediateEmit = true;
lastCompletionTooltip.active = false;
}
else if (!lastCompletionTooltip.active) {
hide();
}
}
function inRange(pos, cursorPos) {
return tree.inRange(pos, { line: cursorPos.row, col: cursorPos.column });
}
var drawn = false;
function draw() {
if (drawn) return true;
drawn = true;
ui.insertCss(require("text!./complete.css"), plugin);
}
function show(row, column, html, _ace) {
draw();
ace = _ace;
var cursorPos = ace.getCursorPosition();
if (!isVisible) {
isVisible = true;
window.document.body.appendChild(tooltipEl);
ace.on("mousewheel", hide, plugin);
tabs.on("focus", hide, plugin);
window.document.addEventListener("mousedown", onMouseDown);
}
tooltipEl.innerHTML = html;
var position = ace.renderer.textToScreenCoordinates(row, column);
var cursorConfig = ace.renderer.$cursorLayer.config;
if (!cursorConfig)
return;
labelHeight = tooltipEl.getBoundingClientRect().height;
isTopdown = true;
if (row > cursorPos.row) // don't obscure cursor
isTopdown = true;
else if (row < cursorPos.row) // don't obscure cursor
isTopdown = false;
else if (position.pageY < labelHeight) // not enough space above us
isTopdown = true;
else if (position.pageY + labelHeight > window.innerHeight)
isTopdown = false;
var editorBottom = ace.renderer.scroller.getBoundingClientRect().bottom;
if (isTopdown && position.pageY > editorBottom)
position.pageY = editorBottom - cursorConfig.lineHeight;
tooltipEl.style.left = (position.pageX - 22) + "px";
if (!isTopdown)
tooltipEl.style.top = (position.pageY - labelHeight + 3) + "px";
else
tooltipEl.style.top = (position.pageY + cursorConfig.lineHeight + 2) + "px";
adjustCompleterTop && adjustCompleterTop(labelHeight);
}
function onMouseDown() {
clearTimeout(onMouseDownTimeout);
onMouseDownTimeout = setTimeout(hide, 300);
}
function getHeight() {
return isVisible && labelHeight || 0;
}
function isTopdown() {
return isTopdown;
}
function getRight() {
return isVisible && tooltipEl.getBoundingClientRect().right;
}
function hide(clearLastPos) {
if (clearLastPos)
lastPos = null;
if (isVisible) {
try {
tooltipEl.parentElement.removeChild(tooltipEl);
} catch (e) {
console.error(e);
}
window.document.removeEventListener("mousedown", onMouseDown);
ace.off("mousewheel", hide);
isVisible = false;
}
}
function getTooltipRegex(language, ace) {
return tooltipRegexes[language || getSyntax(ace)];
}
function getSyntax(ace) {
return ace && SyntaxDetector.getContextSyntax(
ace.session.doc,
ace.getCursorPosition(),
ace.session.syntax);
}
function setLastCompletion(completion, pos) {
// Here we store information about completions
// that may be usable as tooltips
if (!completion.guessTooltip)
return lastCompletionTooltip = {};
var simpleName = completion.replaceText.replace("^^", "").replace(/\(\)$/, "");
if (completion.name.indexOf(simpleName) !== 0)
return lastCompletionTooltip = {};
var matcher = "(" + util.escapeRegExp(simpleName) + ")\\(([^\\)]*)?";
lastCompletionTooltip = {
docHtml: completion.docHeadHtml,
doc: completion.docHead || completion.name,
row: pos.row,
matcher: new RegExp(matcher + "$"),
substringMatcher: new RegExp(matcher),
tab: tabs.focussedTab
};
}
function applyLastCompletionTooltip() {
var tab = tabs.focussedTab;
var ace = tab && tab.editor && tab.editor.ace;
if (!ace || lastCompletionTooltip.tab !== tab)
return;
var pos = ace.getCursorPosition();
var line = ace.getSession().getDocument().getLine(pos.row).substr(0, pos.column);
if (!line.match(lastCompletionTooltip.matcher))
return lastCompletionTooltip.active ? hide() : null;
var name = RegExp.$1;
var args = RegExp.$2;
var beforeMatch = line.substr(0, line.length - name.length - args.length);
var docHtml = beautifyCompletionDoc(args);
show(pos.row, beforeMatch.length, docHtml, ace);
lastPos = null;
lastCompletionTooltip.active = true;
}
function beautifyCompletionDoc(args) {
var docHtml = lastCompletionTooltip.docHtml || util.escapeXml(lastCompletionTooltip.doc);
if (!docHtml.match(lastCompletionTooltip.substringMatcher))
return docHtml;
var argIndex = args.split(",").length - 1;
return docHtml.replace(
lastCompletionTooltip.substringMatcher,
function(all, name, params) {
return name + "(" + (params || "").split(",").map(function(param, i) {
return i === argIndex
? '<span class="language_activeparam">' + param + '</span>'
: param;
});
}
);
}
plugin.freezePublicAPI({
hide: hide,
show: show,
getHeight: getHeight,
getRight: getRight,
isTopdown: isTopdown,
set adjustCompleterTop(f) {
adjustCompleterTop = f;
},
getTooltipRegex: getTooltipRegex,
setLastCompletion: setLastCompletion
});
/**
* @internal
*/
register(null, {
"language.tooltip": plugin
});
}
});