/** * 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) + '

'; if (selected.icon || selected.type) selected.$doc = '
' + (docHead || selected.name && escapeHTML(selected.name)) + '
' + (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 + '
'; } else { hideDocPopup(); } if (selected && selected.docUrl) docElement.innerHTML += '

(more)

'; docElement.innerHTML += ''; } 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 }); } });