/**
* 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 = '';
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) + "";
}
}
selected.$doc = "";
// TODO: apply escapeHTML to selected.doc
if (selected.doc || selected.docHtml)
selected.$doc = ' ' + (selected.doc || selected.docHtml) + '