kopia lustrzana https://github.com/c9/core
1116 wiersze
43 KiB
JavaScript
1116 wiersze
43 KiB
JavaScript
/**
|
|
* Cloud9 Language Foundation
|
|
*
|
|
* @copyright 2013, Ajax.org B.V.
|
|
*/
|
|
define(function(require, exports, module) {
|
|
main.consumes = [
|
|
"Plugin", "ui", "tabManager", "ace", "language",
|
|
"menus", "commands", "c9", "tabManager",
|
|
"language.tooltip", "settings"
|
|
];
|
|
main.provides = ["language.complete"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var Plugin = imports.Plugin;
|
|
var ui = imports.ui;
|
|
var c9 = imports.c9;
|
|
var aceHandle = imports.ace;
|
|
var menus = imports.menus;
|
|
var tabs = imports.tabManager;
|
|
var commands = imports.commands;
|
|
var language = imports.language;
|
|
var tooltip = imports["language.tooltip"];
|
|
var settings = imports.settings;
|
|
var escapeHTML = require("ace/lib/lang").escapeHTML;
|
|
var lang = require("ace/lib/lang");
|
|
var SyntaxDetector = require("./syntax_detector");
|
|
var completeUtil = require("plugins/c9.ide.language/complete_util");
|
|
var Popup = require("ace/autocomplete/popup").AcePopup;
|
|
var completedp = require("./completedp");
|
|
var assert = require("c9/assert");
|
|
|
|
var snippetManager = require("ace/snippets").snippetManager;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new Plugin("Ajax.org", main.consumes);
|
|
var emit = plugin.getEmitter();
|
|
|
|
var theme;
|
|
|
|
var isInvokeScheduled = false;
|
|
var ignoreMouseOnce = false;
|
|
var enterCompletion = true;
|
|
var tooltipHeightAdjust = 0;
|
|
|
|
var commandKeyBeforePatch, textInputBeforePatch, aceBeforePatch;
|
|
var isDocShown;
|
|
var txtCompleterDoc; // ui elements
|
|
var docElement, lastAce, worker;
|
|
var matches, eventMatches, popup;
|
|
var lastUpDownEvent, forceOpen, $closeTrigger;
|
|
|
|
var idRegexes = {};
|
|
var completionRegexes = {};
|
|
|
|
var DEFAULT_ID_REGEX = completeUtil.DEFAULT_ID_REGEX;
|
|
|
|
var FETCH_DOC_DELAY = 1200;
|
|
var SHOW_DOC_DELAY = 1500;
|
|
var SHOW_DOC_DELAY_MOUSE_OVER = 100;
|
|
var HIDE_DOC_DELAY = 1000;
|
|
var AUTO_UPDATE_DELAY = 200;
|
|
var CRASHED_COMPLETION_TIMEOUT = 6000;
|
|
var MENU_WIDTH = 330;
|
|
var MENU_SHOWN_ITEMS = 8;
|
|
var EXTRA_LINE_HEIGHT = 4;
|
|
var REPEAT_IGNORE_RATE = 200;
|
|
|
|
var deferredInvoker = lang.deferredCall(function() {
|
|
isInvokeScheduled = false;
|
|
var ace = deferredInvoker.ace;
|
|
var pos = ace.getCursorPosition();
|
|
var line = ace.getSession().getDocument().getLine(pos.row);
|
|
var identifierRegex = getIdentifierRegex(null, ace);
|
|
var completionRegex = getCompletionRegex(null, ace);
|
|
if (completeUtil.precededByIdentifier(line, pos.column, null, ace)
|
|
|| (line[pos.column - 1] && line[pos.column - 1].match(identifierRegex))
|
|
|| (matchCompletionRegex(completionRegex, line, pos) && (line[pos.column - 1].match(identifierRegex) || !(line[pos.column] || "").match(identifierRegex)))
|
|
|| (language.isInferAvailable() && completeUtil.isRequireJSCall(line, pos.column, "", ace))) {
|
|
invoke({ autoInvoke: true });
|
|
}
|
|
else {
|
|
closeCompletionBox();
|
|
}
|
|
});
|
|
var drawDocInvoke = lang.deferredCall(function() {
|
|
if (!isPopupVisible()) return;
|
|
var match = matches[popup.getHoveredRow()] || matches[popup.getRow()];
|
|
if (match && (match.doc || match.$doc)) {
|
|
isDocShown = true;
|
|
showDocPopup();
|
|
}
|
|
isDrawDocInvokeScheduled = false;
|
|
});
|
|
var isDrawDocInvokeScheduled = false;
|
|
|
|
var requestDocInvoke = lang.deferredCall(function() {
|
|
if (!isPopupVisible()) return;
|
|
isDocsRequested = true;
|
|
if (!eventMatches.some(function(m) {
|
|
return m.noDoc;
|
|
}))
|
|
return;
|
|
invoke({ requestDocs: true });
|
|
});
|
|
var isDocsRequested;
|
|
|
|
var undrawDocInvoke = lang.deferredCall(function() {
|
|
if (!isPopupVisible()) {
|
|
isDocShown = false;
|
|
hideDocPopup();
|
|
}
|
|
});
|
|
|
|
var killCrashedCompletionInvoke = lang.deferredCall(function() {
|
|
closeCompletionBox();
|
|
});
|
|
|
|
var loaded = false;
|
|
function load() {
|
|
if (loaded) return false;
|
|
loaded = true;
|
|
|
|
language.once("initWorker", function(e) {
|
|
worker = e.worker;
|
|
|
|
worker.on("setIdentifierRegex", function(event) {
|
|
idRegexes[event.data.language] = event.data.identifierRegex;
|
|
});
|
|
worker.on("setCompletionRegex", function(event) {
|
|
completionRegexes[event.data.language] = event.data.completionRegex;
|
|
});
|
|
|
|
e.worker.on("complete", function(event) {
|
|
var tab = tabs.focussedTab;
|
|
if (!tab || (tab.path || tab.name) !== event.data.path)
|
|
return;
|
|
|
|
// TODO for background tabs editor.ace.session is wrong
|
|
if (tab.document !== tab.editor.ace.session.c9doc)
|
|
return;
|
|
|
|
assert(tab.editor, "Could find a tab but no editor for " + event.data.path);
|
|
onComplete(event, tab.editor);
|
|
});
|
|
});
|
|
|
|
menus.addItemByPath("Tools/~", new ui.divider(), 2000, plugin);
|
|
menus.addItemByPath("Tools/Show Autocomplete", new ui.item({
|
|
command: "complete"
|
|
}), 2100, plugin);
|
|
|
|
commands.addCommand({
|
|
name: "complete",
|
|
hint: "code complete",
|
|
bindKey: {
|
|
mac: "Ctrl-Space|Alt-Space",
|
|
win: "Ctrl-Space|Alt-Space"
|
|
},
|
|
isAvailable: function(editor) {
|
|
return editor && language.isEditorSupported({ editor: editor });
|
|
},
|
|
exec: function() {
|
|
invoke();
|
|
}
|
|
}, plugin);
|
|
|
|
commands.addCommand({
|
|
name: "completeoverwrite",
|
|
hint: "code complete & overwrite",
|
|
bindKey: {
|
|
mac: "Ctrl-Shift-Space|Alt-Shift-Space",
|
|
win: "Ctrl-Shift-Space|Alt-Shift-Space"
|
|
},
|
|
isAvailable: function(editor) {
|
|
return editor && language.isEditorSupported({ editor: editor });
|
|
},
|
|
exec: invoke.bind(null, false, true)
|
|
}, plugin);
|
|
|
|
aceHandle.on("themeChange", function(e) {
|
|
theme = e.theme;
|
|
if (!theme || !drawn) return;
|
|
|
|
txtCompleterDoc.className = "code_complete_doc_text"
|
|
+ (theme.isDark ? " dark" : "");
|
|
|
|
popup.setTheme({
|
|
cssClass: "code_complete_text",
|
|
isDark: theme.isDark,
|
|
padding: 0
|
|
});
|
|
popup.renderer.setStyle("dark", theme.isDark);
|
|
}, plugin);
|
|
|
|
settings.on("read", updateSettings);
|
|
settings.on("user/language", updateSettings);
|
|
settings.on("project/language", updateSettings);
|
|
}
|
|
|
|
var drawn;
|
|
function draw() {
|
|
if (drawn) return;
|
|
drawn = true;
|
|
|
|
// Import the CSS for the completion box
|
|
ui.insertCss(require("text!./complete.css"), plugin);
|
|
|
|
txtCompleterDoc = document.createElement("div");
|
|
txtCompleterDoc.className = "code_complete_doc_text"
|
|
+ (!theme || theme.isDark ? " dark" : "");
|
|
|
|
popup = new Popup(document.body);
|
|
popup.setTheme({
|
|
cssClass: "code_complete_text",
|
|
isDark: !theme || theme.isDark,
|
|
padding: 0
|
|
});
|
|
popup.$imageSize = 8 + 5 + 7 + 1;
|
|
// popup.renderer.scroller.style.padding = "1px 2px 1px 1px";
|
|
popup.renderer.$extraHeight = 4;
|
|
popup.renderer.setStyle("dark", !theme || theme.isDark);
|
|
|
|
completedp.initPopup(popup, c9.staticUrl);
|
|
//@TODO DEPRECATE: onKeyPress
|
|
function clearLastLine() { popup.onLastLine = false; }
|
|
popup.on("select", clearLastLine);
|
|
popup.on("change", clearLastLine);
|
|
|
|
// Ace Tree Interaction
|
|
popup.on("mouseover", function() {
|
|
if (ignoreMouseOnce) {
|
|
ignoreMouseOnce = false;
|
|
return;
|
|
}
|
|
|
|
if (!isDocsRequested)
|
|
requestDocInvoke.call();
|
|
if (!isDrawDocInvokeScheduled)
|
|
drawDocInvoke.schedule(SHOW_DOC_DELAY_MOUSE_OVER);
|
|
}, false);
|
|
|
|
popup.on("select", function() { updateDoc(true); });
|
|
popup.on("changeHoverMarker", function() { updateDoc(true); });
|
|
|
|
popup.on("click", function(e) {
|
|
onKeyPress(e, 0, 13);
|
|
e.stop();
|
|
});
|
|
|
|
emit("draw");
|
|
}
|
|
|
|
function updateSettings() {
|
|
setEnterCompletion(settings.get("user/language/@enterCompletion"));
|
|
}
|
|
|
|
/***** Helper Functions *****/
|
|
|
|
function isPopupVisible() {
|
|
return popup && popup.isOpen;
|
|
}
|
|
|
|
function getSyntax(ace) {
|
|
return SyntaxDetector.getContextSyntax(
|
|
ace.getSession().getDocument(),
|
|
ace.getCursorPosition(),
|
|
ace.getSession().syntax);
|
|
}
|
|
|
|
function isJavaScript(ace) {
|
|
return ["javascript", "jsx"].indexOf(getSyntax(ace)) > -1;
|
|
}
|
|
|
|
function isHtml(ace) {
|
|
return getSyntax(ace) === "html";
|
|
}
|
|
|
|
/**
|
|
* Replaces the preceeding identifier (`prefix`) with `newText`, where ^^
|
|
* indicate the cursor positions after the replacement.
|
|
* If the prefix is already followed by an identifier substring, that string
|
|
* is deleted.
|
|
*/
|
|
function replaceText(ace, match, deleteSuffix) {
|
|
if (!ace.inVirtualSelectionMode && ace.inMultiSelectMode) {
|
|
ace.forEachSelection(function() {
|
|
replaceText(ace, match, deleteSuffix);
|
|
}, null, { keepOrder: true });
|
|
if (ace.tabstopManager)
|
|
ace.tabstopManager.tabNext();
|
|
return;
|
|
}
|
|
|
|
var newText = match.replaceText;
|
|
var pos = ace.getCursorPosition();
|
|
var session = ace.getSession();
|
|
var line = session.getLine(pos.row);
|
|
var doc = session.getDocument();
|
|
var idRegex = match.identifierRegex || getIdentifierRegex(null, ace);
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
var postfix = completeUtil.retrieveFollowingIdentifier(line, pos.column, idRegex) || "";
|
|
|
|
var snippet = match.snippet;
|
|
if (!snippet) {
|
|
if (match.replaceText === "require(^^)" && isJavaScript(ace)) {
|
|
newText = "require(\"^^\")";
|
|
if (!isInvokeScheduled)
|
|
setTimeout(deferredInvoke.bind(null, false, ace), 0);
|
|
}
|
|
|
|
// Don't insert extra () in front of (
|
|
var endingParens = newText.substr(newText.length - 4) === "(^^)"
|
|
? 4
|
|
: newText.substr(newText.length - 2) === "()" ? 2 : 0;
|
|
if (endingParens) {
|
|
if (line.substr(pos.column + (deleteSuffix ? postfix.length : 0), 1) === "(")
|
|
newText = newText.substr(0, newText.length - endingParens);
|
|
if (postfix && line.substr(pos.column, postfix.length + 1) === postfix + "(") {
|
|
newText = newText.substr(0, newText.length - endingParens);
|
|
deleteSuffix = true;
|
|
}
|
|
}
|
|
|
|
// Ensure cursor marker
|
|
if (newText.indexOf("^^") === -1)
|
|
newText += "^^";
|
|
|
|
// Remove HTML duplicate '<' completions
|
|
var preId = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
if (isHtml(ace) && line[pos.column - preId.length - 1] === '<' && newText[0] === '<')
|
|
newText = newText.substring(1);
|
|
|
|
snippet = newText.replace(/[$]/g, "\\$").replace(/\^\^(.*)\^\^|\^\^/g, "${0:$1}");
|
|
// Remove cursor marker
|
|
newText = newText.replace(/\^\^/g, "");
|
|
}
|
|
|
|
tooltip.setLastCompletion(match, pos);
|
|
|
|
if (deleteSuffix || newText.slice(-postfix.length) === postfix || match.deleteSuffix)
|
|
doc.removeInLine(pos.row, pos.column - prefix.length, pos.column + postfix.length);
|
|
else
|
|
doc.removeInLine(pos.row, pos.column - prefix.length, pos.column);
|
|
|
|
snippetManager.insertSnippet(ace, snippet);
|
|
|
|
language.onCursorChange(null, null, true);
|
|
emit("replaceText", {
|
|
pos: pos,
|
|
prefix: prefix,
|
|
newText: newText,
|
|
match: match
|
|
});
|
|
|
|
if (matchCompletionRegex(getCompletionRegex(), doc.getLine(pos.row), ace.getCursorPosition()))
|
|
deferredInvoke(true);
|
|
}
|
|
|
|
function showCompletionBox(editor, m, prefix, line) {
|
|
var ace = editor.ace;
|
|
draw();
|
|
|
|
matches = m;
|
|
docElement = txtCompleterDoc;
|
|
|
|
// Monkey patch
|
|
if (!commandKeyBeforePatch) {
|
|
aceBeforePatch = ace;
|
|
commandKeyBeforePatch = ace.keyBinding.onCommandKey;
|
|
ace.keyBinding.onCommandKey = onKeyPress.bind(this);
|
|
textInputBeforePatch = ace.keyBinding.onTextInput;
|
|
ace.keyBinding.onTextInput = onTextInput.bind(this);
|
|
}
|
|
|
|
lastAce = ace;
|
|
|
|
populateCompletionBox(ace, matches);
|
|
window.document.addEventListener("mousedown", closeCompletionBox);
|
|
ace.on("mousewheel", closeCompletionBox);
|
|
|
|
var renderer = ace.renderer;
|
|
popup.setFontSize(ace.getFontSize());
|
|
var lineHeight = renderer.layerConfig.lineHeight;
|
|
|
|
var base = ace.getCursorPosition();
|
|
base.column -= prefix.length;
|
|
|
|
// Offset to the left for completion in string, e.g. 'require("a")'
|
|
if (base.column > 0 && line.substr(base.column - 1, 1).match(/["'"]/))
|
|
base.column--;
|
|
|
|
var loc = ace.renderer.textToScreenCoordinates(base.row, base.column);
|
|
var pos = { left: loc.pageX, top: loc.pageY };
|
|
pos.left -= popup.getTextLeftOffset();
|
|
tooltipHeightAdjust = 0;
|
|
|
|
popup.show(pos, lineHeight);
|
|
adjustToToolTipHeight(tooltip.getHeight());
|
|
updateDoc(true);
|
|
|
|
ignoreMouseOnce = !isPopupVisible();
|
|
emit("showPopup", { popup: popup });
|
|
}
|
|
|
|
function adjustToToolTipHeight(height) {
|
|
// Give function to tooltip to adjust completer
|
|
tooltip.adjustCompleterTop = adjustToToolTipHeight;
|
|
|
|
if (!isPopupVisible())
|
|
return;
|
|
|
|
var left = parseInt(popup.container.style.left, 10);
|
|
if (popup.isTopdown !== tooltip.isTopdown() || left > tooltip.getRight())
|
|
height = 0;
|
|
|
|
if (popup.isTopdown) {
|
|
var top = parseInt(popup.container.style.top, 10) - tooltipHeightAdjust;
|
|
height -= height ? 3 : 0;
|
|
top += height;
|
|
popup.container.style.top = top + "px";
|
|
}
|
|
else {
|
|
var bottom = parseInt(popup.container.style.bottom, 10) - tooltipHeightAdjust;
|
|
bottom += height;
|
|
popup.container.style.bottom = bottom + "px";
|
|
}
|
|
tooltipHeightAdjust = height;
|
|
if (isDocShown)
|
|
showDocPopup();
|
|
}
|
|
|
|
function closeCompletionBox(event) {
|
|
if (!popup)
|
|
return;
|
|
|
|
if (event && event.target) {
|
|
if (popup.container.contains(event.target)
|
|
|| docElement.contains(event.target))
|
|
return;
|
|
}
|
|
|
|
emit("hidePopup");
|
|
|
|
popup.hide();
|
|
hideDocPopup();
|
|
|
|
if (!lastAce) // no editor, try again later
|
|
return;
|
|
|
|
var ace = lastAce;
|
|
window.document.removeEventListener("mousedown", closeCompletionBox);
|
|
ace.off("mousewheel", closeCompletionBox);
|
|
|
|
if (commandKeyBeforePatch) {
|
|
aceBeforePatch.keyBinding.onCommandKey = commandKeyBeforePatch;
|
|
aceBeforePatch.keyBinding.onTextInput = textInputBeforePatch;
|
|
}
|
|
commandKeyBeforePatch = textInputBeforePatch = null;
|
|
|
|
undrawDocInvoke.schedule(HIDE_DOC_DELAY);
|
|
requestDocInvoke.cancel();
|
|
forceOpen = false;
|
|
}
|
|
|
|
function populateCompletionBox(ace, matches) {
|
|
// Get context info
|
|
var pos = ace.getCursorPosition();
|
|
var line = ace.getSession().getLine(pos.row);
|
|
var idRegex = getIdentifierRegex(null, ace);
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
|
|
// Set the highlight metadata
|
|
popup.ace = ace;
|
|
popup.matches = matches;
|
|
popup.prefix = prefix;
|
|
|
|
popup.ignoreGenericMatches = isIgnoreGenericEnabled(matches);
|
|
popup.matches = matches;
|
|
|
|
popup.calcPrefix = function(regex) {
|
|
return completeUtil.retrievePrecedingIdentifier(line, pos.column, regex);
|
|
};
|
|
|
|
setPopupDataKeepRow(matches);
|
|
}
|
|
|
|
function setPopupDataKeepRow(matches) {
|
|
var row = popup.getRow();
|
|
popup.setData(matches);
|
|
popup.setRow(row);
|
|
|
|
if (!popup.isOpen || row >= matches.length || row > 0 && !popup.data.every(function(m, i) {
|
|
return i > row || matches[i] && matches[i].name === m.name;
|
|
}))
|
|
popup.setRow(0);
|
|
}
|
|
|
|
function cleanupMatches(matches, ace, pos, line) {
|
|
if (isIgnoreGenericEnabled(matches)) {
|
|
// Disable generic matches when possible
|
|
matches = matches.filter(function(m) { return !m.isGeneric; });
|
|
}
|
|
|
|
if (ace.inMultiSelectMode) {
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column);
|
|
for (var i = 0; i < matches.length; i++) {
|
|
var m = matches[i];
|
|
if (m.replaceText === prefix)
|
|
matches.splice(i--, 1);
|
|
}
|
|
}
|
|
|
|
// Simpler look & feel in strings
|
|
if (inCommentOrString(ace, pos)) {
|
|
for (var i = 0; i < matches.length; i++) {
|
|
var m = matches[i];
|
|
if (m.meta === "snippet") {
|
|
matches.splice(i--, 1);
|
|
continue;
|
|
}
|
|
if (m.icon !== "package" && m.icon !== "event" && !m.isContextual)
|
|
m.icon = null;
|
|
var simpleName = m.replaceText.replace("^^", "").replace(/\(\)$/, "");
|
|
if (m.name.indexOf(simpleName) === 0)
|
|
m.name = m.replaceText = simpleName;
|
|
delete m.isContextual;
|
|
delete m.meta;
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function isIgnoreGenericEnabled(matches) {
|
|
var isNonGenericAvailable = false;
|
|
var isContextualAvailable = false;
|
|
for (var i = 0; i < matches.length; i++) {
|
|
if (!matches[i].isGeneric)
|
|
isNonGenericAvailable = true;
|
|
if (matches[i].isContextual)
|
|
isContextualAvailable = true;
|
|
}
|
|
return isNonGenericAvailable && isContextualAvailable;
|
|
}
|
|
|
|
function updateDoc(delayPopup) {
|
|
docElement.innerHTML = '<span class="code_complete_doc_body">';
|
|
var matches = popup.matches;
|
|
var selected = matches && (
|
|
matches[popup.getHoveredRow()] || matches[popup.getRow()]);
|
|
|
|
if (!selected)
|
|
return;
|
|
var docHead = selected.docHeadHtml || selected.docHead && escapeHTML(selected.docHead);
|
|
if (selected.type) {
|
|
var shortType = completedp.guidToShortString(selected.type);
|
|
if (shortType) {
|
|
docHead = docHead || selected.name + " : "
|
|
+ completedp.guidToLongString(selected.type) + "</div>";
|
|
}
|
|
}
|
|
|
|
selected.$doc = "";
|
|
|
|
// TODO: apply escapeHTML to selected.doc
|
|
if (selected.doc || selected.docHtml)
|
|
selected.$doc = '<p>' + (selected.doc || selected.docHtml) + '</p>';
|
|
|
|
if (selected.icon || selected.type)
|
|
selected.$doc = '<div class="code_complete_doc_head">'
|
|
+ (docHead || selected.name && escapeHTML(selected.name)) + '</div>'
|
|
+ (selected.$doc || "");
|
|
|
|
if (selected && selected.$doc) {
|
|
if (isDocShown) {
|
|
showDocPopup();
|
|
}
|
|
else {
|
|
hideDocPopup();
|
|
if (!isDrawDocInvokeScheduled || delayPopup) {
|
|
if (!isDocsRequested)
|
|
requestDocInvoke.schedule(FETCH_DOC_DELAY);
|
|
drawDocInvoke.schedule(SHOW_DOC_DELAY);
|
|
}
|
|
}
|
|
docElement.innerHTML += selected.$doc + '</span>';
|
|
}
|
|
else {
|
|
hideDocPopup();
|
|
}
|
|
if (selected && selected.docUrl)
|
|
docElement.innerHTML += '<p><a' +
|
|
' onclick="require(\'ext/preview/preview\').preview(\'' + selected.docUrl + '\'); return false;"' +
|
|
' href="' + selected.docUrl + '" target="c9doc">(more)</a></p>';
|
|
docElement.innerHTML += '</span>';
|
|
}
|
|
|
|
function showDocPopup() {
|
|
if (!isDocsRequested)
|
|
requestDocInvoke.call();
|
|
if (matches[0] && matches[0].nodoc === "always")
|
|
return;
|
|
|
|
var rect = popup.container.getBoundingClientRect();
|
|
if (!txtCompleterDoc.parentNode) {
|
|
document.body.appendChild(txtCompleterDoc);
|
|
}
|
|
txtCompleterDoc.style.top = popup.container.style.top;
|
|
txtCompleterDoc.style.bottom = popup.container.style.bottom;
|
|
|
|
if (window.innerWidth - rect.right < 320) {
|
|
txtCompleterDoc.style.right = window.innerWidth - rect.left + "px";
|
|
txtCompleterDoc.style.left = "";
|
|
} else {
|
|
txtCompleterDoc.style.left = (rect.right + 1) + "px";
|
|
txtCompleterDoc.style.right = "";
|
|
}
|
|
txtCompleterDoc.style.height = rect.height + "px";
|
|
txtCompleterDoc.style.display = "block";
|
|
}
|
|
|
|
function hideDocPopup() {
|
|
if (txtCompleterDoc)
|
|
txtCompleterDoc.style.display = "none";
|
|
}
|
|
|
|
/**
|
|
* Set a tooltip to show when a generic completion was just
|
|
* typed in by the user.
|
|
*/
|
|
function setTextInputToolTip(text) {
|
|
if (text !== "(")
|
|
return;
|
|
|
|
var foundOne = false;
|
|
var pos = lastAce.getCursorPosition();
|
|
var line = lastAce.getSession().getLine(pos.row);
|
|
for (var i = 0; i < matches.length; i++) {
|
|
// Find matches that give us a viable tooltip
|
|
if (!matches[i].isGeneric
|
|
|| (!matches[i].doc && matches[i].name === matches[i].replaceText)
|
|
|| !matches[i].replaceText.match(/\)$/))
|
|
continue;
|
|
var replaceText = matches[i].replaceText.replace("^^", "").replace(/\(\)$/, "");
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column - 1, matches[i].identifierRegex || getIdentifierRegex());
|
|
if (replaceText !== prefix)
|
|
continue;
|
|
if (foundOne)
|
|
tooltip.setLastCompletion(null, pos);
|
|
tooltip.setLastCompletion(matches[i], pos);
|
|
foundOne = true;
|
|
}
|
|
}
|
|
|
|
function onTextInput(text, pasted) {
|
|
var keyBinding = lastAce.keyBinding;
|
|
textInputBeforePatch.apply(keyBinding, arguments);
|
|
if (!pasted) {
|
|
setTextInputToolTip(text);
|
|
var matched = false;
|
|
for (var i = 0; i < matches.length && !matched; i++) {
|
|
var idRegex = matches[i].identifierRegex || getIdentifierRegex();
|
|
matched = idRegex.test(text);
|
|
}
|
|
var completionMatch = matchCompletionRegex(getCompletionRegex(), text, { column: text.length });
|
|
if (matched || completionMatch)
|
|
deferredInvoke(completionMatch);
|
|
else
|
|
closeCompletionBox();
|
|
}
|
|
}
|
|
|
|
function onKeyPress(e, hashKey, keyCode) {
|
|
if (keyCode && (e.metaKey || e.ctrlKey || e.altKey)) {
|
|
if (!e.altKey || keyCode != 32)
|
|
closeCompletionBox();
|
|
return;
|
|
}
|
|
|
|
var keyBinding = lastAce.keyBinding;
|
|
|
|
switch (keyCode) {
|
|
case 0: break;
|
|
case 32: // Space
|
|
case 35: // End
|
|
case 36: // Home
|
|
closeCompletionBox();
|
|
break;
|
|
case 27: // Esc
|
|
// special case for vim mode, needed because complete hijacks ace keybinding
|
|
if (lastAce.$vimModeHandler)
|
|
commandKeyBeforePatch.apply(keyBinding, arguments);
|
|
closeCompletionBox();
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
break;
|
|
case 8: // Backspace
|
|
commandKeyBeforePatch.apply(keyBinding, arguments);
|
|
deferredInvoke();
|
|
e.preventDefault();
|
|
break;
|
|
case 37:
|
|
case 39:
|
|
commandKeyBeforePatch.apply(keyBinding, arguments);
|
|
closeCompletionBox();
|
|
e.preventDefault();
|
|
break;
|
|
case 13: // Enter
|
|
case 9: // Tab
|
|
var ace = lastAce;
|
|
if (!enterCompletion && keyCode === 13) {
|
|
commandKeyBeforePatch(e, hashKey, keyCode);
|
|
closeCompletionBox();
|
|
break;
|
|
}
|
|
closeCompletionBox();
|
|
replaceText(ace, matches[popup.getRow()], e.shiftKey);
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation && e.stopImmediatePropagation();
|
|
break;
|
|
case 40: // Down
|
|
isDocShown = true;
|
|
var time = Date.now();
|
|
if (popup.getRow() == popup.matches.length - 1) {
|
|
if (!(lastUpDownEvent + REPEAT_IGNORE_RATE > time)
|
|
|| popup.matches.length === 1)
|
|
return closeCompletionBox();
|
|
}
|
|
else {
|
|
popup.setRow(popup.getRow() + 1);
|
|
}
|
|
lastUpDownEvent = time;
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
break;
|
|
case 38: // Up
|
|
isDocShown = true;
|
|
var time = new Date().getTime();
|
|
if ((!popup.getRow() && !(lastUpDownEvent + REPEAT_IGNORE_RATE > time))
|
|
|| popup.matches.length === 1)
|
|
return closeCompletionBox();
|
|
lastUpDownEvent = time;
|
|
if (popup.getRow())
|
|
popup.setRow(popup.getRow() - 1);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
break;
|
|
case 33: // PageUp
|
|
popup.gotoPageUp();
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
break;
|
|
case 34: // PageDown
|
|
popup.gotoPageDown();
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger code completion by firing an event to the worker.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Boolean} [options.autoInvoke] completion was triggered automatically
|
|
* @param {Boolean} [options.deleteSuffix] the suffix of the current identifier
|
|
* may be overwritten
|
|
* @param {Boolean} [options.predictOnly] only prediction is requested
|
|
*/
|
|
function invoke(options) {
|
|
var tab = tabs.focussedTab;
|
|
if (!tab || !language.isEditorSupported(tab))
|
|
return;
|
|
|
|
var ace = lastAce = tab.editor.ace;
|
|
options = options || {};
|
|
if (!options.predictOnly)
|
|
isDocsRequested = options.requestDocs;
|
|
|
|
if (ace.inMultiSelectMode) {
|
|
var row = ace.selection.lead.row;
|
|
// allow completion if all selections are empty and on the same line
|
|
var shouldClose = options.autoInvoke && !ace.selection.ranges.every(function(r) {
|
|
return r.cursor.row == row && r.isEmpty();
|
|
});
|
|
if (shouldClose && !forceOpen || !sameMultiselectPrefix(ace))
|
|
return closeCompletionBox();
|
|
else
|
|
forceOpen = true;
|
|
}
|
|
ace.addEventListener("change", deferredInvoke);
|
|
var pos = ace.getCursorPosition();
|
|
var line = ace.getSession().getLine(pos.row);
|
|
worker.emit("complete", { data: {
|
|
pos: pos,
|
|
line: line,
|
|
forceBox: true,
|
|
deleteSuffix: true,
|
|
noDoc: !options.requestDocs && !isDocShown,
|
|
predictOnly: options.predictOnly,
|
|
}});
|
|
if (options.autoInvoke)
|
|
killCrashedCompletionInvoke(CRASHED_COMPLETION_TIMEOUT);
|
|
}
|
|
|
|
function onComplete(event, editor) {
|
|
if (!lastAce || lastAce != editor.ace) {
|
|
console.error("[complete] received completion for wrong ace");
|
|
return;
|
|
}
|
|
|
|
var pos = editor.ace.getCursorPosition();
|
|
var line = editor.ace.getSession().getLine(pos.row);
|
|
isDocsRequested = !event.data.noDoc;
|
|
|
|
editor.ace.removeEventListener("change", deferredInvoke);
|
|
killCrashedCompletionInvoke.cancel();
|
|
|
|
if (!completeUtil.canCompleteForChangedLine(event.data.line, line, event.data.pos, pos, getIdentifierRegex(null, editor.ace)))
|
|
return;
|
|
if (event.data.isUpdate && !isPopupVisible() && eventMatches && eventMatches.length)
|
|
return;
|
|
|
|
var matches = eventMatches = event.data.matches;
|
|
matches = filterMatches(matches, line, pos);
|
|
matches = cleanupMatches(matches, editor.ace, pos, line);
|
|
|
|
if (matches.length === 1 && !event.data.forceBox) {
|
|
replaceText(editor.ace, matches[0], event.data.deleteSuffix);
|
|
}
|
|
else if (matches.length > 0) {
|
|
var idRegex = matches[0].identifierRegex || getIdentifierRegex();
|
|
var identifier = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
if (matches.length === 1 && (identifier === matches[0].replaceText || identifier + " " === matches[0].replaceText) && matches[0].replaceText)
|
|
closeCompletionBox();
|
|
else
|
|
showCompletionBox(editor, matches, identifier, line);
|
|
}
|
|
else {
|
|
closeCompletionBox();
|
|
}
|
|
}
|
|
|
|
function setEnterCompletion(enabled) {
|
|
enterCompletion = enabled;
|
|
}
|
|
|
|
function sameMultiselectPrefix(ace) {
|
|
var commonPrefix;
|
|
var idRegex = getIdentifierRegex();
|
|
return ace.selection.ranges.every(function(range) {
|
|
var pos = range.cursor;
|
|
var line = ace.session.getLine(pos.row);
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
if (commonPrefix === undefined)
|
|
commonPrefix = prefix;
|
|
return commonPrefix == prefix;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Incrementally update completion results while waiting for the worker.
|
|
*/
|
|
function onCompleteUpdate() {
|
|
var ace = lastAce;
|
|
if (!isPopupVisible() || !eventMatches)
|
|
return;
|
|
var pos = ace.getCursorPosition();
|
|
var line = ace.getSession().getLine(pos.row);
|
|
var idRegex = getIdentifierRegex();
|
|
var prefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, idRegex);
|
|
matches = filterMatches(eventMatches, line, pos);
|
|
matches = cleanupMatches(matches, ace, pos, line);
|
|
if (matches.length) {
|
|
showCompletionBox({ ace: ace }, matches, prefix, line);
|
|
} else {
|
|
closeCompletionBox();
|
|
$closeTrigger = ace.prevOp;
|
|
}
|
|
}
|
|
|
|
function filterMatches(matches, line, pos) {
|
|
var identifierRegex = getIdentifierRegex();
|
|
var defaultPrefix = completeUtil.retrievePrecedingIdentifier(line, pos.column, identifierRegex);
|
|
var results = matches.filter(function(match) {
|
|
var prefix = match.identifierRegex ? completeUtil.retrievePrecedingIdentifier(line, pos.column, match.identifierRegex) : defaultPrefix;
|
|
return match.name.indexOf(prefix) === 0;
|
|
});
|
|
|
|
// Always prefer current identifier (similar to worker.js)
|
|
var prefixLine = line.substr(0, pos.column);
|
|
for (var i = 0; i < results.length; i++) {
|
|
var m = results[i];
|
|
m.replaceText = m.replaceText || m.name;
|
|
if (results[i].isGeneric && results[i].$source !== "local")
|
|
continue;
|
|
var match = prefixLine.lastIndexOf(m.replaceText);
|
|
if (match > -1
|
|
&& match === pos.column - m.replaceText.length
|
|
&& completeUtil.retrievePrecedingIdentifier(line, pos.column, m.identifierRegex || identifierRegex)) {
|
|
results.splice(i, 1);
|
|
results.splice(0, 0, m);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function deferredInvoke(now, ace, fromBackspace) {
|
|
if (fromBackspace && (!$closeTrigger || ace.prevOp != $closeTrigger))
|
|
return;
|
|
ace = ace || lastAce;
|
|
now = now || !isPopupVisible();
|
|
var delay = now ? 0 : AUTO_UPDATE_DELAY;
|
|
if (!now) {
|
|
// Fire incremental update after document changes are known
|
|
setTimeout(onCompleteUpdate.bind(this), 0);
|
|
}
|
|
if (isInvokeScheduled)
|
|
return;
|
|
isInvokeScheduled = true;
|
|
deferredInvoker.ace = ace;
|
|
deferredInvoker.schedule(delay);
|
|
}
|
|
|
|
function getCompletionRegex(language, ace) {
|
|
// Try getting a regex, or return null, with matches nothing (undefined matches anything)
|
|
return completionRegexes[language || getSyntax(ace || lastAce)] || null;
|
|
}
|
|
|
|
function matchCompletionRegex(completionRegex, line, pos) {
|
|
if (!completionRegex)
|
|
return false;
|
|
var ch = line[pos.column - 1];
|
|
if (ch && completionRegex.test(ch))
|
|
return true;
|
|
if (completionRegex.source.match(/[^\\]\$\)*$/))
|
|
return completionRegex.test(line.substr(0, pos.column));
|
|
}
|
|
|
|
function getIdentifierRegex(language, ace) {
|
|
return idRegexes[language || getSyntax(ace || lastAce)] || DEFAULT_ID_REGEX;
|
|
}
|
|
|
|
function inCommentOrString(ace, pos) {
|
|
var token = ace.getSession().getTokenAt(pos.row, pos.column - 1);
|
|
return token && token.type && token.type.match(/^comment|^string/);
|
|
}
|
|
|
|
function addSnippet(data, plugin) {
|
|
if (typeof data == "string") {
|
|
data = { text: data };
|
|
var text = data.text;
|
|
var firstLine = text.split("\n", 1)[0].replace(/\#/g, "").trim();
|
|
firstLine.split(";").forEach(function(n) {
|
|
if (!n) return;
|
|
var info = n.split(":");
|
|
if (info.length != 2) return;
|
|
data[info[0].trim()] = info[1].trim();
|
|
});
|
|
}
|
|
if (data.include)
|
|
data.include = data.include.split(",").map(function(n) {
|
|
return n.trim();
|
|
});
|
|
if (!data.scope) throw new Error("Missing Snippet Scope");
|
|
|
|
data.scope = data.scope.split(",");
|
|
if (!data.snippets && data.text)
|
|
data.snippets = snippetManager.parseSnippetFile(data.text);
|
|
|
|
data.scope.forEach(function(scope) {
|
|
snippetManager.register(data.snippets, scope);
|
|
});
|
|
|
|
// if (snippet.include) {
|
|
// snippetManager.snippetMap[snippet.scope].includeScopes = snippet.include;
|
|
// snippet.include.forEach(function(x) {
|
|
// // loadSnippetFile("ace/mode/" + x);
|
|
// // @nightwing help!
|
|
// });
|
|
// }
|
|
|
|
plugin.addOther(function() {
|
|
snippetManager.unregister(data.snippets);
|
|
});
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function() {
|
|
load();
|
|
});
|
|
plugin.on("unload", function() {
|
|
loaded = false;
|
|
drawn = false;
|
|
theme = null;
|
|
isInvokeScheduled = null;
|
|
ignoreMouseOnce = null;
|
|
enterCompletion = null;
|
|
tooltipHeightAdjust = null;
|
|
commandKeyBeforePatch = null;
|
|
textInputBeforePatch = null;
|
|
aceBeforePatch = null;
|
|
isDocShown = null;
|
|
txtCompleterDoc = null;
|
|
docElement = null;
|
|
lastAce = null;
|
|
worker = null;
|
|
matches = null;
|
|
eventMatches = null;
|
|
popup = null;
|
|
lastUpDownEvent = null;
|
|
forceOpen = null;
|
|
$closeTrigger = null;
|
|
isDocShown = false;
|
|
isDocsRequested = false;
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
* Manages the code completer popup.
|
|
**/
|
|
plugin.freezePublicAPI({
|
|
/**
|
|
* Invoke the completer after a small delay,
|
|
* if there is a matching language handler that
|
|
* agrees to complete at the current cursor position.
|
|
*
|
|
* @param {Boolean} now Show without delay
|
|
* @param {ace} ace The current tab's editor.ace object
|
|
*/
|
|
deferredInvoke: deferredInvoke,
|
|
|
|
/**
|
|
* Force-invoke the completer immediately.
|
|
* @ignore
|
|
* @internal See {@link #deferredInvoke()}.
|
|
*/
|
|
invoke: invoke,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
getCompletionRegex: getCompletionRegex,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
matchCompletionRegex: matchCompletionRegex,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
getIdentifierRegex: getIdentifierRegex,
|
|
|
|
/**
|
|
* Close the completion popup.
|
|
*/
|
|
closeCompletionBox: closeCompletionBox,
|
|
|
|
/**
|
|
* Determines whether a completion popup is currently visible.
|
|
*/
|
|
isPopupVisible: isPopupVisible,
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
setEnterCompletion: setEnterCompletion,
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
$setShowDocDelay: function(value) {
|
|
SHOW_DOC_DELAY = value;
|
|
},
|
|
|
|
events: [
|
|
/**
|
|
* Fires when a code completion option is picked.
|
|
*
|
|
* @param {Object} pos
|
|
* @param {String} newText
|
|
* @param {Object} match
|
|
* @event replaceText
|
|
*/
|
|
"replaceText",
|
|
/**
|
|
* Fires when a completion popup is shown.
|
|
* @event showPopup
|
|
*/
|
|
"showPopup",
|
|
/**
|
|
* Fires when a completion popup is hidden.
|
|
* @event hidePopup
|
|
*/
|
|
"hidePopup"
|
|
],
|
|
|
|
/**
|
|
*
|
|
*/
|
|
addSnippet: addSnippet
|
|
});
|
|
|
|
register(null, {
|
|
"language.complete": plugin
|
|
});
|
|
}
|
|
});
|