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

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
});
}
});