diff --git a/src/extensions/default/JavaScriptCodeHints/MessageIds.js b/src/extensions/default/JavaScriptCodeHints/MessageIds.js index 30063df41c3..1d07ce36bcf 100644 --- a/src/extensions/default/JavaScriptCodeHints/MessageIds.js +++ b/src/extensions/default/JavaScriptCodeHints/MessageIds.js @@ -35,7 +35,9 @@ define(function (require, exports, module) { TERN_GET_GUESSES_MSG = "GetGuesses", TERN_WORKER_READY = "WorkerReady", TERN_INFERENCE_TIMEDOUT = "InferenceTimedOut", - SET_CONFIG = "SetConfig"; + SET_CONFIG = "SetConfig", + TERN_UPDATE_DIRTY_FILE = "UpdateDirtyFileEntry", + TERN_CLEAR_DIRTY_FILES_LIST = "ClearDirtyFilesList"; // Message parameter constants var TERN_FILE_INFO_TYPE_PART = "part", @@ -58,6 +60,8 @@ define(function (require, exports, module) { exports.TERN_FILE_INFO_TYPE_EMPTY = TERN_FILE_INFO_TYPE_EMPTY; exports.TERN_INFERENCE_TIMEDOUT = TERN_INFERENCE_TIMEDOUT; exports.SET_CONFIG = SET_CONFIG; + exports.TERN_UPDATE_DIRTY_FILE = TERN_UPDATE_DIRTY_FILE; + exports.TERN_CLEAR_DIRTY_FILES_LIST = TERN_CLEAR_DIRTY_FILES_LIST; }); diff --git a/src/extensions/default/JavaScriptCodeHints/ScopeManager.js b/src/extensions/default/JavaScriptCodeHints/ScopeManager.js index ad16337c304..ac3eb2c970d 100644 --- a/src/extensions/default/JavaScriptCodeHints/ScopeManager.js +++ b/src/extensions/default/JavaScriptCodeHints/ScopeManager.js @@ -28,8 +28,6 @@ * from an outer scope. */ -/*global Worker */ - define(function (require, exports, module) { "use strict"; @@ -48,6 +46,7 @@ define(function (require, exports, module) { ProjectManager = brackets.getModule("project/ProjectManager"), Strings = brackets.getModule("strings"), StringUtils = brackets.getModule("utils/StringUtils"), + NodeDomain = brackets.getModule("utils/NodeDomain"), InMemoryFile = brackets.getModule("document/InMemoryFile"); var HintUtils = require("HintUtils"), @@ -60,18 +59,23 @@ define(function (require, exports, module) { builtinLibraryNames = [], isDocumentDirty = false, _hintCount = 0, - currentWorker = null, + currentModule = null, documentChanges = null, // bounds of document changes preferences = null, deferredPreferences = null; - + + var _modulePath = FileUtils.getNativeModuleDirectoryPath(module), + _nodePath = "node/TernNodeDomain", + _domainPath = [_modulePath, _nodePath].join("/"); + + var MAX_HINTS = 30, // how often to reset the tern server LARGE_LINE_CHANGE = 100, LARGE_LINE_COUNT = 10000, OFFSET_ZERO = {line: 0, ch: 0}; var config = {}; - + /** * An array of library names that contain JavaScript builtins definitions. * @@ -85,7 +89,7 @@ define(function (require, exports, module) { * Read in the json files that have type information for the builtins, dom,etc */ function initTernEnv() { - var path = ExtensionUtils.getModulePath(module, "node_modules/tern/defs/"), + var path = ExtensionUtils.getModulePath(module, "node/node_modules/tern/defs/"), files = builtinFiles, library; @@ -174,12 +178,12 @@ define(function (require, exports, module) { } /** - * Send a message to the tern worker - if the worker is being initialized, + * Send a message to the tern module - if the module is being initialized, * the message will not be posted until initialization is complete */ function postMessage(msg) { - if (currentWorker) { - currentWorker.postMessage(msg); + if (currentModule) { + currentModule.postMessage(msg); } } @@ -261,7 +265,7 @@ define(function (require, exports, module) { } /** - * Add a pending request waiting for the tern-worker to complete. + * Add a pending request waiting for the tern-module to complete. * If file is a detected exclusion, then reject request. * * @param {string} file - the name of the file @@ -322,7 +326,7 @@ define(function (require, exports, module) { * @return {string} returns the path we resolved when we tried to parse the file, or undefined */ function getResolvedPath(file) { - return currentWorker.getResolvedPath(file); + return currentModule.getResolvedPath(file); } /** @@ -395,10 +399,10 @@ define(function (require, exports, module) { } /** - * Handle the response from the tern web worker when + * Handle the response from the tern node domain when * it responds with the definition * - * @param response - the response from the worker + * @param response - the response from the node domain */ function handleJumptoDef(response) { @@ -635,11 +639,11 @@ define(function (require, exports, module) { } /** - * Handle the response from the tern web worker when + * Handle the response from the tern node domain when * it responds with the list of completions * * @param {{file: string, offset: {line: number, ch: number}, completions:Array., - * properties:Array.}} response - the response from the worker + * properties:Array.}} response - the response from node domain */ function handleTernCompletions(response) { @@ -666,12 +670,12 @@ define(function (require, exports, module) { } /** - * Handle the response from the tern web worker when + * Handle the response from the tern node domain when * it responds to the get guesses message. * * @param {{file: string, type: string, offset: {line: number, ch: number}, * properties: Array.}} response - - * the response from the worker contains the guesses for a + * the response from node domain contains the guesses for a * property lookup. */ function handleGetGuesses(response) { @@ -686,10 +690,10 @@ define(function (require, exports, module) { } /** - * Handle the response from the tern web worker when + * Handle the response from the tern node domain when * it responds to the update file message. * - * @param {{path: string, type: string}} response - the response from the worker + * @param {{path: string, type: string}} response - the response from node domain */ function handleUpdateFile(response) { @@ -705,7 +709,7 @@ define(function (require, exports, module) { /** * Handle timed out inference * - * @param {{path: string, type: string}} response - the response from the worker + * @param {{path: string, type: string}} response - the response from node domain */ function handleTimedOut(response) { @@ -744,14 +748,31 @@ define(function (require, exports, module) { ] ); } + + DocumentManager.on("dirtyFlagChange", function (event, changedDoc) { + if (changedDoc.file.fullPath) { + postMessage({ + type: MessageIds.TERN_UPDATE_DIRTY_FILE, + name: changedDoc.file.fullPath, + action: changedDoc.isDirty + }); + } + }); + + // Clear dirty document list in tern node domain + ProjectManager.on("beforeProjectClose", function () { + postMessage({ + type: MessageIds.TERN_CLEAR_DIRTY_FILES_LIST + }); + }); /** - * Encapsulate all the logic to talk to the worker thread. This will create - * a new instance of a TernWorker, which the rest of the hinting code can use to talk - * to the worker, without worrying about initialization, priming the pump, etc. + * Encapsulate all the logic to talk to the tern module. This will create + * a new instance of a TernModule, which the rest of the hinting code can use to talk + * to the tern node domain, without worrying about initialization, priming the pump, etc. * */ - function TernWorker() { + function TernModule() { var ternPromise = null, addFilesPromise = null, rootTernDir = null, @@ -761,7 +782,7 @@ define(function (require, exports, module) { numInitialFiles = 0, numResolvedFiles = 0, numAddedFiles = 0, - _ternWorker = null; + _ternNodeDomain = null; /** * @param {string} file a relative path @@ -783,33 +804,33 @@ define(function (require, exports, module) { } /** - * Send a message to the tern worker - if the worker is being initialized, + * Send a message to the tern node domain - if the module is being initialized, * the message will not be posted until initialization is complete */ function postMessage(msg) { - addFilesPromise.done(function (ternWorker) { + addFilesPromise.done(function (ternModule) { // If an error came up during file handling, bail out now - if (!ternWorker) { + if (!_ternNodeDomain) { return; } if (config.debug) { console.debug("Sending message", msg); } - ternWorker.postMessage(msg); + _ternNodeDomain.exec("invokeTernCommand", msg); }); } /** - * Send a message to the tern worker - this is only for messages that + * Send a message to the tern node domain - this is only for messages that * need to be sent before and while the addFilesPromise is being resolved. */ function _postMessageByPass(msg) { - ternPromise.done(function (ternWorker) { + ternPromise.done(function (ternModule) { if (config.debug) { console.debug("Sending message", msg); } - ternWorker.postMessage(msg); + _ternNodeDomain.exec("invokeTernCommand", msg); }); } @@ -832,10 +853,10 @@ define(function (require, exports, module) { } /** - * Handle a request from the worker for text of a file + * Handle a request from the tern node domain for text of a file * - * @param {{file:string}} request - the request from the worker. Should be an Object containing the name - * of the file tern wants the contents of + * @param {{file:string}} request - the request from the tern node domain. Should be an Object containing the name + * of the file tern wants the contents of */ function handleTernGetFile(request) { @@ -851,7 +872,7 @@ define(function (require, exports, module) { /** * Helper function to get the text of a given document and send it to tern. - * If DocumentManager successfully gets the file's text then we'll send it to the tern worker. + * If DocumentManager successfully gets the file's text then we'll send it to the tern node domain. * The Promise for getDocumentText() is returned so that custom fail functions can be used. * * @param {string} filePath - the path of the file to get the text of @@ -938,10 +959,10 @@ define(function (require, exports, module) { } /** - * Handle the response from the tern web worker when + * Handle the response from the tern node domain when * it responds to the prime pump message. * - * @param {{path: string, type: string}} response - the response from the worker + * @param {{path: string, type: string}} response - the response from node domain */ function handlePrimePumpCompletion(response) { @@ -973,7 +994,7 @@ define(function (require, exports, module) { } numAddedFiles += files.length; - ternPromise.done(function (worker) { + ternPromise.done(function (ternModule) { var msg = { type : MessageIds.TERN_ADD_FILES_MSG, files : files @@ -982,7 +1003,7 @@ define(function (require, exports, module) { if (config.debug) { console.debug("Sending message", msg); } - worker.postMessage(msg); + _ternNodeDomain.exec("invokeTernCommand", msg); }); } else { @@ -1028,70 +1049,74 @@ define(function (require, exports, module) { } /** - * Init the web worker that does all the code hinting work. - * - * If a worker already exists, then this will terminate that worker and - * start a new worker - this helps alleviate leaks that may be ocurring in - * the code that the worker runs. + * Init the Tern module that does all the code hinting work. */ - function initTernWorker() { - if (_ternWorker) { - _ternWorker.terminate(); - } - var workerDeferred = $.Deferred(); - ternPromise = workerDeferred.promise(); - var path = ExtensionUtils.getModulePath(module, "tern-worker.js"); - _ternWorker = new Worker(path); - - _ternWorker.addEventListener("message", function (e) { - if (config.debug) { - console.debug("Message received", e); - } - - var response = e.data, - type = response.type; - - if (type === MessageIds.TERN_COMPLETIONS_MSG || - type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) { - // handle any completions the worker calculated - handleTernCompletions(response); - } else if (type === MessageIds.TERN_GET_FILE_MSG) { - // handle a request for the contents of a file - handleTernGetFile(response); - } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { - handleJumptoDef(response); - } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { - handlePrimePumpCompletion(response); - } else if (type === MessageIds.TERN_GET_GUESSES_MSG) { - handleGetGuesses(response); - } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) { - handleUpdateFile(response); - } else if (type === MessageIds.TERN_INFERENCE_TIMEDOUT) { - handleTimedOut(response); - } else if (type === MessageIds.TERN_WORKER_READY) { - workerDeferred.resolveWith(null, [_ternWorker]); - } else { - console.log("Worker: " + (response.log || response)); - } - }); + function initTernModule() { + var moduleDeferred = $.Deferred(); + ternPromise = moduleDeferred.promise(); + if (_ternNodeDomain) { + _ternNodeDomain.exec("resetTernServer"); + moduleDeferred.resolveWith(null, [_ternNodeDomain]); + } else { + _ternNodeDomain = new NodeDomain("TernNodeDomain", _domainPath); + _ternNodeDomain.on("data", function (evt, data) { + if (config.debug) { + console.log("Message received", data.type); + } - // Set the initial configuration for the worker - _ternWorker.postMessage({ - type: MessageIds.SET_CONFIG, - config: config - }); + var response = data, + type = response.type; + + if (type === MessageIds.TERN_COMPLETIONS_MSG || + type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) { + // handle any completions the tern server calculated + handleTernCompletions(response); + } else if (type === MessageIds.TERN_GET_FILE_MSG) { + // handle a request for the contents of a file + handleTernGetFile(response); + } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { + handleJumptoDef(response); + } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { + handlePrimePumpCompletion(response); + } else if (type === MessageIds.TERN_GET_GUESSES_MSG) { + handleGetGuesses(response); + } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) { + handleUpdateFile(response); + } else if (type === MessageIds.TERN_INFERENCE_TIMEDOUT) { + handleTimedOut(response); + } else if (type === MessageIds.TERN_WORKER_READY) { + moduleDeferred.resolveWith(null, [_ternNodeDomain]); + } else { + console.log("Tern Module: " + (response.log || response)); + } + }); + + _ternNodeDomain.promise().done(function () { + + _ternNodeDomain.exec("setInterface", { + messageIds : MessageIds + }); + + _ternNodeDomain.exec("invokeTernCommand", { + type: MessageIds.SET_CONFIG, + config: config + }); + moduleDeferred.resolveWith(null, [_ternNodeDomain]); + }); + } } + /** * Create a new tern server. */ function initTernServer(dir, files) { - initTernWorker(); + initTernModule(); numResolvedFiles = 0; numAddedFiles = 0; stopAddingFiles = false; numInitialFiles = files.length; - ternPromise.done(function (worker) { + ternPromise.done(function (ternModule) { var msg = { type : MessageIds.TERN_INIT_MSG, dir : dir, @@ -1099,17 +1124,7 @@ define(function (require, exports, module) { env : ternEnvironment, timeout : PreferencesManager.get("jscodehints.inferenceTimeout") }; - - if (worker) { - if (config.debug) { - console.debug("Sending message", msg); - } - worker.postMessage(msg); - } else { - if (config.debug) { - console.debug("Worker null. Cannot send message", msg); - } - } + _ternNodeDomain.exec("invokeTernCommand", msg); }); rootTernDir = dir + "/"; } @@ -1154,16 +1169,20 @@ define(function (require, exports, module) { var updateFilePromise = updateTernFile(previousDocument); updateFilePromise.done(function () { primePump(path, document.isUntitled()); - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); } else { - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } isDocumentDirty = false; return; } - + + if (previousDocument && previousDocument.isDirty) { + updateTernFile(previousDocument); + } + isDocumentDirty = false; resolvedFiles = {}; projectRoot = pr; @@ -1174,7 +1193,7 @@ define(function (require, exports, module) { initTernServer(pr, []); var hintsPromise = primePump(path, true); hintsPromise.done(function () { - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); return; } @@ -1219,15 +1238,14 @@ define(function (require, exports, module) { // prime the pump again but this time don't wait // for completion. primePump(path, false); - - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); } else { - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } }); } else { - addFilesDeferred.resolveWith(null, [_ternWorker]); + addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } }); }); @@ -1255,33 +1273,23 @@ define(function (require, exports, module) { /** * Do some cleanup when a project is closed. * - * We can clean up the web worker we use to calculate hints now, since - * we know we will need to re-init it in any new project that is opened. + * We can clean up the node tern server we use to calculate hints now, since + * we know we will need to re-init it in any new project that is opened. */ - function closeWorker() { - function terminateWorker() { - var worker = _ternWorker; - - // Worker can be null if an error condition came up previously - if (!worker) { - return; + function resetModule() { + function resetTernServer() { + if (_ternNodeDomain.ready()) { + _ternNodeDomain.exec('resetTernServer'); } - setTimeout(function () { - // give pending requests a chance to finish - worker.terminate(); - worker = null; - }, 1000); - _ternWorker = null; - resolvedFiles = {}; } - - if (_ternWorker) { + + if (_ternNodeDomain) { if (addFilesPromise) { - // If we're in the middle of added files, don't terminate - // until we're done or we might get NPEs - addFilesPromise.done(terminateWorker).fail(terminateWorker); + // If we're in the middle of added files, don't reset + // until we're done + addFilesPromise.done(resetTernServer).fail(resetTernServer); } else { - terminateWorker(); + resetTernServer(); } } } @@ -1289,8 +1297,8 @@ define(function (require, exports, module) { function whenReady(func) { addFilesPromise.done(func); } - - this.closeWorker = closeWorker; + + this.resetModule = resetModule; this.handleEditorChange = handleEditorChange; this.postMessage = postMessage; this.getResolvedPath = getResolvedPath; @@ -1302,55 +1310,50 @@ define(function (require, exports, module) { var resettingDeferred = null; /** - * reset the tern worker thread, if necessary. - * - * To avoid memory leaks in the worker thread we periodically kill - * the web worker instance, and start a new one. To avoid a performance - * hit when we do this we start up a new worker, and don't kill the old - * one unitl the new one is initialized. + * reset the tern module, if necessary. * * During debugging, you can turn this automatic resetting behavior off * by running this in the console: * brackets._configureJSCodeHints({ noReset: true }) * * This function is also used in unit testing with the "force" flag to - * reset the worker for each test to start with a clean environment. + * reset the module for each test to start with a clean environment. * * @param {Session} session * @param {Document} document * @param {boolean} force true to force a reset regardless of how long since the last one - * @return {Promise} Promise resolved when the worker is ready. - * The new (or current, if there was no reset) worker is passed to the callback. + * @return {Promise} Promise resolved when the module is ready. + * The new (or current, if there was no reset) module is passed to the callback. */ function _maybeReset(session, document, force) { - var newWorker; + var newTernModule; // if we're in the middle of a reset, don't have to check - // the new worker will be online soon + // the new module will be online soon if (!resettingDeferred) { // We don't reset if the debugging flag is set - // because it's easier to debug if the worker isn't - // getting shut down all the time. + // because it's easier to debug if the module isn't + // getting reset all the time. if (force || (!config.noReset && ++_hintCount > MAX_HINTS)) { if (config.debug) { - console.debug("Resetting tern worker"); + console.debug("Resetting tern module"); } resettingDeferred = new $.Deferred(); - newWorker = new TernWorker(); - newWorker.handleEditorChange(session, document, null); - newWorker.whenReady(function () { - // tell the old worker to shut down - currentWorker.closeWorker(); - currentWorker = newWorker; - resettingDeferred.resolve(currentWorker); + newTernModule = new TernModule(); + newTernModule.handleEditorChange(session, document, null); + newTernModule.whenReady(function () { + // reset the old module + currentModule.resetModule(); + currentModule = newTernModule; + resettingDeferred.resolve(currentModule); // all done reseting resettingDeferred = null; }); _hintCount = 0; } else { var d = new $.Deferred(); - d.resolve(currentWorker); + d.resolve(currentModule); return d.promise(); } } @@ -1482,22 +1485,20 @@ define(function (require, exports, module) { */ function handleEditorChange(session, document, previousDocument) { - if (!currentWorker) { - currentWorker = new TernWorker(); + if (!currentModule) { + currentModule = new TernModule(); } - return currentWorker.handleEditorChange(session, document, previousDocument); + + return currentModule.handleEditorChange(session, document, previousDocument); } /** * Do some cleanup when a project is closed. - * - * We can clean up the web worker we use to calculate hints now, since - * we know we will need to re-init it in any new project that is opened. + * Clean up previous analysis data from the module */ function handleProjectClose() { - if (currentWorker) { - currentWorker.closeWorker(); - currentWorker = null; + if (currentModule) { + currentModule.resetModule(); } } @@ -1519,8 +1520,8 @@ define(function (require, exports, module) { /** * @private - * - * Update the configuration in the worker. + * + * Update the configuration in the tern node domain. */ function _setConfig(configUpdate) { config = brackets._configureJSCodeHints.config; diff --git a/src/extensions/default/JavaScriptCodeHints/node/ExtractFileContent.js b/src/extensions/default/JavaScriptCodeHints/node/ExtractFileContent.js new file mode 100644 index 00000000000..c31d79226b8 --- /dev/null +++ b/src/extensions/default/JavaScriptCodeHints/node/ExtractFileContent.js @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2017 - present Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*eslint-env node */ +/*jslint node: true */ + +"use strict"; + +var fs = require("fs"), + _dirtyFilesCache = {}; + +/** + * Clears the cache for dirty file paths + */ +function clearDirtyFilesCache() { + _dirtyFilesCache = {}; +} + +/** + * Updates the files cache with fullpath when dirty flag changes for a document + * If the doc is being marked as dirty then an entry is created in the cache + * If the doc is being marked as clean then the corresponsing entry gets cleared from cache + * + * @param {String} name - fullpath of the document + * @param {boolean} action - whether the document is dirty + */ +function updateDirtyFilesCache(name, action) { + if (action) { + _dirtyFilesCache[name] = true; + } else { + if (_dirtyFilesCache[name]) { + delete _dirtyFilesCache[name]; + } + } +} + +/** + * Extract content locally from the file system used fs.readFile() + * + * @param {String} fileName - fullpath of the document + * @param {Function} callback - callback handle to post the content back + */ +function _readFile(fileName, callback) { + fs.readFile(fileName, "utf8", function (err, data) { + var content = ""; + if (!err) { + content = data; + } + callback.apply(null, [fileName, content]); + }); +} + +/** + * Extracts file content for the given file name(1st param) and invokes the callback handle(2nd param) with + * extracted file content. Content can be extracted locally from the file system used fs.readFile() + * or conditionally from main context(brackets main thread) by using the 3rd param + * + * @param {String} fileName - fullpath of the document + * @param {Function} callback - callback handle to post the content back + * @param {Object} extractFromMainContext - content request handle wrapper from main thread + */ +function extractContent(fileName, callback, extractFromMainContext) { + // Ask the main thread context to provide the updated file content + // We can't yet use node io to read, to utilize shells encoding detection + extractFromMainContext.apply(null, [fileName]); +} + +exports.extractContent = extractContent; +exports.clearFilesCache = clearDirtyFilesCache; +exports.updateFilesCache = updateDirtyFilesCache; + diff --git a/src/extensions/default/JavaScriptCodeHints/node/TernNodeDomain.js b/src/extensions/default/JavaScriptCodeHints/node/TernNodeDomain.js new file mode 100644 index 00000000000..51d6fff416c --- /dev/null +++ b/src/extensions/default/JavaScriptCodeHints/node/TernNodeDomain.js @@ -0,0 +1,835 @@ +/* + * Copyright (c) 2017 - present Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*eslint-env node */ +/*jslint node: true */ +/*global setImmediate */ + + + +"use strict"; + +var config = {}; +var _domainManager; +var MessageIds; +var self = { + postMessage: function (data) { + _domainManager.emitEvent("TernNodeDomain", "data", [data]); + } +}; + +var Tern = require("tern"), + Infer = require("tern/lib/infer"); + +require("tern/plugin/requirejs"); +require("tern/plugin/doc_comment"); +require("tern/plugin/angular"); + + +var ExtractContent = require("./ExtractFileContent"); + +var ternServer = null, + isUntitledDoc = false, + inferenceTimeout; + +// Save the tern callbacks for when we get the contents of the file +var fileCallBacks = {}; + +/** + * Send a log message back from the node to the main thread + * @private + * @param {string} msg - the log message + */ +function _log(msg) { + console.log(msg); +} + +/** + * Report exception + * @private + * @param {Error} e - the error object + */ +function _reportError(e, file) { + if (e instanceof Infer.TimedOut) { + // Post a message back to the main thread with timedout info + self.postMessage({ + type: MessageIds.TERN_INFERENCE_TIMEDOUT, + file: file + }); + } else { + _log("Error thrown in tern_node domain:" + e.message + "\n" + e.stack); + } +} + +/** + * Handle a response from the main thread providing the contents of a file + * @param {string} file - the name of the file + * @param {string} text - the contents of the file + */ +function handleGetFile(file, text) { + var next = fileCallBacks[file]; + if (next) { + try { + next(null, text); + } catch (e) { + _reportError(e, file); + } + } + delete fileCallBacks[file]; +} + +function _getNormalizedFilename(fileName) { + if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === -1) { + fileName = ternServer.projectDir + fileName; + } + return fileName; +} + +function _getDenormalizedFilename(fileName) { + if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === 0) { + fileName = fileName.slice(ternServer.projectDir.length); + } + return fileName; +} + +/** + * Callback handle to request contents of a file from the main thread + * @param {string} file - the name of the file + */ +function _requestFileContent(name) { + self.postMessage({ + type: MessageIds.TERN_GET_FILE_MSG, + file: name + }); +} + +/** + * Provide the contents of the requested file to tern + * @param {string} name - the name of the file + * @param {Function} next - the function to call with the text of the file + * once it has been read in. + */ +function getFile(name, next) { + // save the callback + fileCallBacks[name] = next; + + setImmediate(function () { + try { + ExtractContent.extractContent(name, handleGetFile, _requestFileContent); + } catch (error) { + console.log(error); + } + }); +} + +/** + * Create a new tern server. + * + * @param {Object} env - an Object with the environment, as read in from + * the json files in thirdparty/tern/defs + * @param {Array.} files - a list of filenames tern should be aware of + */ +function initTernServer(env, files) { + var ternOptions = { + defs: env, + async: true, + getFile: getFile, + plugins: {requirejs: {}, doc_comment: true, angular: true} + }; + + // If a server is already created just reset the analysis data before marking it for GC + if (ternServer) { + ternServer.reset(); + Infer.resetGuessing(); + } + + ternServer = new Tern.Server(ternOptions); + + files.forEach(function (file) { + ternServer.addFile(file); + }); + +} + +/** + * Resets an existing tern server. + */ +function resetTernServer() { + // If a server is already created just reset the analysis data + if (ternServer) { + ternServer.reset(); + Infer.resetGuessing(); + // tell the main thread we're ready to start processing again + self.postMessage({type: MessageIds.TERN_WORKER_READY}); + } +} + +/** + * Create a "empty" update object. + * + * @param {string} path - full path of the file. + * @return {{type: string, name: string, offsetLines: number, text: string}} - + * "empty" update. + + */ +function createEmptyUpdate(path) { + return {type: MessageIds.TERN_FILE_INFO_TYPE_EMPTY, + name: path, + offsetLines: 0, + text: ""}; +} + +/** + * Build an object that can be used as a request to tern. + * + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {string} query - the type of request being made + * @param {{line: number, ch: number}} offset - + */ +function buildRequest(fileInfo, query, offset) { + query = {type: query}; + query.start = offset; + query.end = offset; + query.file = (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) ? "#0" : fileInfo.name; + query.filter = false; + query.sort = false; + query.depths = true; + query.guess = true; + query.origins = true; + query.types = true; + query.expandWordForward = false; + query.lineCharPositions = true; + query.docs = true; + query.urls = true; + + var request = {query: query, files: [], offset: offset, timeout: inferenceTimeout}; + if (fileInfo.type !== MessageIds.TERN_FILE_INFO_TYPE_EMPTY) { + // Create a copy to mutate ahead + var fileInfoCopy = JSON.parse(JSON.stringify(fileInfo)); + request.files.push(fileInfoCopy); + } + + return request; +} + +/** + * Get definition location + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {{line: number, ch: number}} offset - the offset into the + * file for cursor + */ +function getJumptoDef(fileInfo, offset) { + var request = buildRequest(fileInfo, "definition", offset); + // request.query.typeOnly = true; // FIXME: tern doesn't work exactly right yet. + + try { + ternServer.request(request, function (error, data) { + if (error) { + _log("Error returned from Tern 'definition' request: " + error); + self.postMessage({type: MessageIds.TERN_JUMPTODEF_MSG, file: fileInfo.name, offset: offset}); + return; + } + var response = { + type: MessageIds.TERN_JUMPTODEF_MSG, + file: _getNormalizedFilename(fileInfo.name), + resultFile: data.file, + offset: offset, + start: data.start, + end: data.end + }; + + request = buildRequest(fileInfo, "type", offset); + // See if we can tell if the reference is to a Function type + ternServer.request(request, function (error, data) { + if (!error) { + response.isFunction = data.type.length > 2 && data.type.substring(0, 2) === "fn"; + } + + // Post a message back to the main thread with the definition + self.postMessage(response); + }); + + }); + } catch (e) { + _reportError(e, fileInfo.name); + } +} + +/** + * Get all the known properties for guessing. + * + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {{line: number, ch: number}} offset - + * the offset into the file where we want completions for + * @param {string} type - the type of the message to reply with. + */ +function getTernProperties(fileInfo, offset, type) { + + var request = buildRequest(fileInfo, "properties", offset), + i; + //_log("tern properties: request " + request.type + dir + " " + file); + try { + ternServer.request(request, function (error, data) { + var properties = []; + if (error) { + _log("Error returned from Tern 'properties' request: " + error); + } else { + //_log("tern properties: completions = " + data.completions.length); + properties = data.completions.map(function (completion) { + return {value: completion, type: completion.type, guess: true}; + }); + } + // Post a message back to the main thread with the completions + self.postMessage({type: type, + file: _getNormalizedFilename(fileInfo.name), + offset: offset, + properties: properties + }); + }); + } catch (e) { + _reportError(e, fileInfo.name); + } +} + +/** + * Get the completions for the given offset + * + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {{line: number, ch: number}} offset - + * the offset into the file where we want completions for + * @param {boolean} isProperty - true if getting a property hint, + * otherwise getting an identifier hint. + */ +function getTernHints(fileInfo, offset, isProperty) { + var request = buildRequest(fileInfo, "completions", offset), + i; + //_log("request " + dir + " " + file + " " + offset /*+ " " + text */); + try { + ternServer.request(request, function (error, data) { + var completions = []; + if (error) { + _log("Error returned from Tern 'completions' request: " + error); + } else { + //_log("found " + data.completions + " for " + file + "@" + offset); + completions = data.completions.map(function (completion) { + return {value: completion.name, type: completion.type, depth: completion.depth, + guess: completion.guess, origin: completion.origin, doc: completion.doc, url: completion.url}; + }); + } + + if (completions.length > 0 || !isProperty) { + // Post a message back to the main thread with the completions + self.postMessage({type: MessageIds.TERN_COMPLETIONS_MSG, + file: _getNormalizedFilename(fileInfo.name), + offset: offset, + completions: completions + }); + } else { + // if there are no completions, then get all the properties + getTernProperties(fileInfo, offset, MessageIds.TERN_COMPLETIONS_MSG); + } + }); + } catch (e) { + _reportError(e, fileInfo.name); + } +} + +/** + * Given a Tern type object, convert it to an array of Objects, where each object describes + * a parameter. + * + * @param {!Infer.Fn} inferFnType - type to convert. + * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter. + */ +function getParameters(inferFnType) { + + // work around define functions before use warning. + var recordTypeToString, inferTypeToString, processInferFnTypeParameters, inferFnTypeToString; + + /** + * Convert an infer array type to a string. + * + * Formatted using google closure style. For example: + * + * "Array." + * + * @param {Infer.Arr} inferArrType + * + * @return {string} - array formatted in google closure style. + * + */ + function inferArrTypeToString(inferArrType) { + var result = "Array.<"; + + result += inferArrType.props[""].types.types.map(inferTypeToString).join(", "); + + // workaround case where types is zero length + if (inferArrType.props[""].types.length === 0) { + result += "Object"; + } + result += ">"; + + return result; + } + + /** + * Convert properties to a record type annotation. + * + * @param {Object} props + * @return {string} - record type annotation + */ + recordTypeToString = function (props) { + var result = "{", + first = true, + prop; + + result += Object.keys(props).map(function (key) { + return key + ": " + inferTypeToString(props[key]); + }).join(", "); + + result += "}"; + + return result; + }; + + /** + * Convert an infer type to a string. + * + * @param {*} inferType - one of the Infer's types; Infer.Prim, Infer.Arr, Infer.ANull. Infer.Fn functions are + * not handled here. + * + * @return {string} + * + */ + inferTypeToString = function (inferType) { + var result; + + if (inferType instanceof Infer.AVal) { + inferType = inferType.types[0]; + } + + if (inferType instanceof Infer.Prim) { + result = inferType.toString(); + if (result === "string") { + result = "String"; + } else if (result === "number") { + result = "Number"; + } else if (result === "boolean") { + result = "Boolean"; + } + } else if (inferType instanceof Infer.Arr) { + result = inferArrTypeToString(inferType); + } else if (inferType instanceof Infer.Fn) { + result = inferFnTypeToString(inferType); + } else if (inferType instanceof Infer.Obj) { + if (inferType.name === undefined) { + result = recordTypeToString(inferType.props); + } else { + result = inferType.name; + } + } else { + result = "Object"; + } + + return result; + }; + + /** + * Format the given parameter array. Handles separators between + * parameters, syntax for optional parameters, and the order of the + * parameter type and parameter name. + * + * @param {!Array.<{name: string, type: string, isOptional: boolean}>} params - + * array of parameter descriptors + * @param {function(string)=} appendSeparators - callback function to append separators. + * The separator is passed to the callback. + * @param {function(string, number)=} appendParameter - callback function to append parameter. + * The formatted parameter type and name is passed to the callback along with the + * current index of the parameter. + * @param {boolean=} typesOnly - only show parameter types. The + * default behavior is to include both parameter names and types. + * @return {string} - formatted parameter hint + */ + function formatParameterHint(params, appendSeparators, appendParameter, typesOnly) { + var result = "", + pendingOptional = false; + + params.forEach(function (value, i) { + var param = value.type, + separators = ""; + + if (value.isOptional) { + // if an optional param is following by an optional parameter, then + // terminate the bracket. Otherwise enclose a required parameter + // in the same bracket. + if (pendingOptional) { + separators += "]"; + } + + pendingOptional = true; + } + + if (i > 0) { + separators += ", "; + } + + if (value.isOptional) { + separators += "["; + } + + if (appendSeparators) { + appendSeparators(separators); + } + + result += separators; + + if (!typesOnly) { + param += " " + value.name; + } + + if (appendParameter) { + appendParameter(param, i); + } + + result += param; + + }); + + if (pendingOptional) { + if (appendSeparators) { + appendSeparators("]"); + } + + result += "]"; + } + + return result; + } + + /** + * Convert an infer function type to a Google closure type string. + * + * @param {Infer.Fn} inferType - type to convert. + * @return {string} - function type as a string. + */ + inferFnTypeToString = function (inferType) { + var result = "function(", + params = processInferFnTypeParameters(inferType); + + result += /*HintUtils2.*/formatParameterHint(params, null, null, true); + if (inferType.retval) { + result += "):"; + result += inferTypeToString(inferType.retval); + } + + return result; + }; + + /** + * Convert an infer function type to string. + * + * @param {*} inferType - one of the Infer's types; Infer.Fn, Infer.Prim, Infer.Arr, Infer.ANull + * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter. + */ + processInferFnTypeParameters = function (inferType) { + var params = [], + i; + + for (i = 0; i < inferType.args.length; i++) { + var param = {}, + name = inferType.argNames[i], + type = inferType.args[i]; + + if (!name) { + name = "param" + (i + 1); + } + + if (name[name.length - 1] === "?") { + name = name.substring(0, name.length - 1); + param.isOptional = true; + } + + param.name = name; + param.type = inferTypeToString(type); + params.push(param); + } + + return params; + }; + + return processInferFnTypeParameters(inferFnType); +} + +/** + * Get the function type for the given offset + * + * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo + * - type of update, name of file, and the text of the update. + * For "full" updates, the whole text of the file is present. For "part" updates, + * the changed portion of the text. For "empty" updates, the file has not been modified + * and the text is empty. + * @param {{line: number, ch: number}} offset - + * the offset into the file where we want completions for + */ +function handleFunctionType(fileInfo, offset) { + var request = buildRequest(fileInfo, "type", offset), + error; + + request.query.preferFunction = true; + + var fnType = ""; + try { + ternServer.request(request, function (ternError, data) { + + if (ternError) { + _log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError); + error = ternError.toString(); + } else { + var file = ternServer.findFile(fileInfo.name); + + // convert query from partial to full offsets + var newOffset = offset; + if (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) { + newOffset = {line: offset.line + fileInfo.offsetLines, ch: offset.ch}; + } + + request = buildRequest(createEmptyUpdate(fileInfo.name), "type", newOffset); + + var expr = Tern.findQueryExpr(file, request.query); + Infer.resetGuessing(); + var type = Infer.expressionType(expr); + type = type.getFunctionType() || type.getType(); + + if (type) { + fnType = getParameters(type); + } else { + ternError = "No parameter type found"; + _log(ternError); + } + } + }); + } catch (e) { + _reportError(e, fileInfo.name); + } + + // Post a message back to the main thread with the completions + self.postMessage({type: MessageIds.TERN_CALLED_FUNC_TYPE_MSG, + file: _getNormalizedFilename(fileInfo.name), + offset: offset, + fnType: fnType, + error: error + }); +} + +/** + * Add an array of files to tern. + * + * @param {Array.} files - each string in the array is the full + * path of a file. + */ +function handleAddFiles(files) { + files.forEach(function (file) { + ternServer.addFile(file); + }); +} + +/** + * Update the context of a file in tern. + * + * @param {string} path - full path of file. + * @param {string} text - content of the file. + */ +function handleUpdateFile(path, text) { + + ternServer.addFile(path, text); + + self.postMessage({type: MessageIds.TERN_UPDATE_FILE_MSG, + path: path + }); + + // reset to get the best hints with the updated file. + ternServer.reset(); + Infer.resetGuessing(); +} + +/** + * Make a completions request to tern to force tern to resolve files + * and create a fast first lookup for the user. + * @param {string} path - the path of the file + */ +function handlePrimePump(path) { + var fileName = _getDenormalizedFilename(path); + var fileInfo = createEmptyUpdate(fileName), + request = buildRequest(fileInfo, "completions", {line: 0, ch: 0}); + + try { + ternServer.request(request, function (error, data) { + // Post a message back to the main thread + self.postMessage({type: MessageIds.TERN_PRIME_PUMP_MSG, + path: _getNormalizedFilename(path) + }); + }); + } catch (e) { + _reportError(e, path); + } +} + +/** + * Updates the configuration, typically for debugging purposes. + * + * @param {Object} configUpdate new configuration + */ +function setConfig(configUpdate) { + config = configUpdate; +} + +function _requestTernServer(commandConfig) { + var file, text, offset, + request = commandConfig, + type = request.type; + if (config.debug) { + _log("Message received " + type); + } + + if (type === MessageIds.TERN_INIT_MSG) { + var env = request.env, + files = request.files; + inferenceTimeout = request.timeout; + initTernServer(env, files); + } else if (type === MessageIds.TERN_COMPLETIONS_MSG) { + offset = request.offset; + getTernHints(request.fileInfo, offset, request.isProperty); + } else if (type === MessageIds.TERN_GET_FILE_MSG) { + file = request.file; + text = request.text; + handleGetFile(file, text); + } else if (type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) { + offset = request.offset; + handleFunctionType(request.fileInfo, offset); + } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { + offset = request.offset; + getJumptoDef(request.fileInfo, offset); + } else if (type === MessageIds.TERN_ADD_FILES_MSG) { + handleAddFiles(request.files); + } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { + isUntitledDoc = request.isUntitledDoc; + handlePrimePump(request.path); + } else if (type === MessageIds.TERN_GET_GUESSES_MSG) { + offset = request.offset; + getTernProperties(request.fileInfo, offset, MessageIds.TERN_GET_GUESSES_MSG); + } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) { + handleUpdateFile(request.path, request.text); + } else if (type === MessageIds.SET_CONFIG) { + setConfig(request.config); + } else if (type === MessageIds.TERN_UPDATE_DIRTY_FILE) { + ExtractContent.updateFilesCache(request.name, request.action); + } else if (type === MessageIds.TERN_CLEAR_DIRTY_FILES_LIST) { + ExtractContent.clearFilesCache(); + } else { + _log("Unknown message: " + JSON.stringify(request)); + } +} + +function invokeTernCommand(commandConfig) { + try { + _requestTernServer(commandConfig); + } catch (error) { + console.warn(error); + } +} + +function setInterface(msgInterface) { + MessageIds = msgInterface.messageIds; +} + + /** + * Initialize the test domain with commands and events related to find in files. + * @param {DomainManager} domainManager The DomainManager for the TernNodeDomain + */ +function init(domainManager) { + if (!domainManager.hasDomain("TernNodeDomain")) { + domainManager.registerDomain("TernNodeDomain", {major: 0, minor: 1}); + } + + _domainManager = domainManager; + + domainManager.registerCommand( + "TernNodeDomain", // domain name + "invokeTernCommand", // command name + invokeTernCommand, // command handler function + false, // this command is synchronous in Node + "Invokes a tern command on node", + [{name: "commandConfig", // parameters + type: "object", + description: "Object containing tern command configuration"}] + ); + + domainManager.registerCommand( + "TernNodeDomain", // domain name + "setInterface", // command name + setInterface, // command handler function + false, // this command is synchronous in Node + "Sets the shared message interface", + [{name: "msgInterface", // parameters + type: "object", + description: "Object containing messageId enums"}] + ); + + domainManager.registerCommand( + "TernNodeDomain", // domain name + "resetTernServer", // command name + resetTernServer, // command handler function + true, // this command is synchronous in Node + "Resets an existing tern server" + ); + + domainManager.registerEvent( + "TernNodeDomain", // domain name + "data", // event name + [ + { + name: "data", + type: "Object", + description: "data to be returned to main thread" + } + ] + ); +} + +exports.init = init; diff --git a/src/extensions/default/JavaScriptCodeHints/node/package.json b/src/extensions/default/JavaScriptCodeHints/node/package.json new file mode 100644 index 00000000000..fd37424a27b --- /dev/null +++ b/src/extensions/default/JavaScriptCodeHints/node/package.json @@ -0,0 +1,7 @@ +{ + "name": "brackets-javascript-code-hints", + "dependencies": { + "acorn": "5.1.1", + "tern": "0.21.0" + } +} diff --git a/src/extensions/default/JavaScriptCodeHints/tern-worker.js b/src/extensions/default/JavaScriptCodeHints/tern-worker.js deleted file mode 100644 index 74c867cb7c6..00000000000 --- a/src/extensions/default/JavaScriptCodeHints/tern-worker.js +++ /dev/null @@ -1,681 +0,0 @@ -/* - * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - -/*global self, importScripts, require */ - -importScripts("thirdparty/requirejs/require.js"); - -var config = {}; - -(function () { - "use strict"; - - var MessageIds, HintUtils2; - var Tern, Infer; - require(["./MessageIds", "./HintUtils2"], function (messageIds, hintUtils2) { - MessageIds = messageIds; - HintUtils2 = hintUtils2; - var ternRequire = require.config({baseUrl: "./node_modules"}); - ternRequire(["tern/lib/tern", "tern/lib/infer", "tern/plugin/requirejs", "tern/plugin/doc_comment", "tern/plugin/angular"], function (tern, infer, requirejs, docComment) { - Tern = tern; - Infer = infer; - - var ternServer = null, - inferenceTimeout, - isUntitledDoc = false; - - // Save the tern callbacks for when we get the contents of the file - var fileCallBacks = {}; - - /** - * Provide the contents of the requested file to tern - * @param {string} name - the name of the file - * @param {Function} next - the function to call with the text of the file - * once it has been read in. - */ - function getFile(name, next) { - // save the callback - fileCallBacks[name] = next; - - // post a message back to the main thread to get the file contents - self.postMessage({ - type: MessageIds.TERN_GET_FILE_MSG, - file: name - }); - } - - /** - * Send a log message back from the worker to the main thread - * @private - * @param {string} msg - the log message - */ - function _log(msg) { - self.postMessage({log: msg }); - } - - /** - * Report exception - * @private - * @param {Error} e - the error object - */ - function _reportError(e, file) { - if (e instanceof Infer.TimedOut) { - // Post a message back to the main thread with timedout info - self.postMessage({ - type: MessageIds.TERN_INFERENCE_TIMEDOUT, - file: file - }); - } else { - _log("Error thrown in tern_worker:" + e.message + "\n" + e.stack); - } - } - - /** - * Handle a response from the main thread providing the contents of a file - * @param {string} file - the name of the file - * @param {string} text - the contents of the file - */ - function handleGetFile(file, text) { - var next = fileCallBacks[file]; - if (next) { - try { - next(null, text); - } catch (e) { - _reportError(e, file); - } - } - delete fileCallBacks[file]; - } - - function _getNormalizedFilename(fileName) { - if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === -1) { - fileName = ternServer.projectDir + fileName; - } - return fileName; - } - - function _getDenormalizedFilename(fileName) { - if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === 0) { - fileName = fileName.slice(ternServer.projectDir.length); - } - return fileName; - } - - /** - * Create a new tern server. - * - * @param {Object} env - an Object with the environment, as read in from - * the json files in node_modules/tern/defs - * @param {Array.} files - a list of filenames tern should be aware of - */ - function initTernServer(dir, env, files) { - var ternOptions = { - projectDir: dir, - defs: env, - async: true, - getFile: getFile, - plugins: {requirejs: {}, doc_comment: true, angular: true} - }; - ternServer = new Tern.Server(ternOptions); - - files.forEach(function (file) { - ternServer.addFile(file); - }); - - } - - /** - * Create a "empty" update object. - * - * @param {string} path - full path of the file. - * @return {{type: string, name: string, offsetLines: number, text: string}} - - * "empty" update. - - */ - function createEmptyUpdate(path) { - return {type: MessageIds.TERN_FILE_INFO_TYPE_EMPTY, - name: path, - offsetLines: 0, - text: ""}; - } - - /** - * Build an object that can be used as a request to tern. - * - * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo - * - type of update, name of file, and the text of the update. - * For "full" updates, the whole text of the file is present. For "part" updates, - * the changed portion of the text. For "empty" updates, the file has not been modified - * and the text is empty. - * @param {string} query - the type of request being made - * @param {{line: number, ch: number}} offset - - */ - function buildRequest(fileInfo, query, offset) { - query = {type: query}; - query.start = offset; - query.end = offset; - query.file = (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) ? "#0" : fileInfo.name; - query.filter = false; - query.sort = false; - query.depths = true; - query.guess = true; - query.origins = true; - query.types = true; - query.expandWordForward = false; - query.lineCharPositions = true; - query.docs = true; - query.urls = true; - - var request = {query: query, files: [], offset: offset, timeout: inferenceTimeout}; - if (fileInfo.type !== MessageIds.TERN_FILE_INFO_TYPE_EMPTY) { - request.files.push(fileInfo); - } - - return request; - } - - /** - * Get definition location - * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo - * - type of update, name of file, and the text of the update. - * For "full" updates, the whole text of the file is present. For "part" updates, - * the changed portion of the text. For "empty" updates, the file has not been modified - * and the text is empty. - * @param {{line: number, ch: number}} offset - the offset into the - * file for cursor - */ - function getJumptoDef(fileInfo, offset) { - var request = buildRequest(fileInfo, "definition", offset); - // request.query.typeOnly = true; // FIXME: tern doesn't work exactly right yet. - - try { - ternServer.request(request, function (error, data) { - if (error) { - _log("Error returned from Tern 'definition' request: " + error); - self.postMessage({type: MessageIds.TERN_JUMPTODEF_MSG, file: fileInfo.name, offset: offset}); - return; - } - - var response = {type: MessageIds.TERN_JUMPTODEF_MSG, - file: _getNormalizedFilename(fileInfo.name), - resultFile: data.file, - offset: offset, - start: data.start, - end: data.end - }; - - request = buildRequest(fileInfo, "type", offset); - // See if we can tell if the reference is to a Function type - ternServer.request(request, function (error, data) { - if (!error) { - response.isFunction = data.type.length > 2 && data.type.substring(0, 2) === "fn"; - } - - // Post a message back to the main thread with the definition - self.postMessage(response); - }); - - }); - } catch (e) { - _reportError(e, fileInfo.name); - } - } - - /** - * Get all the known properties for guessing. - * - * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo - * - type of update, name of file, and the text of the update. - * For "full" updates, the whole text of the file is present. For "part" updates, - * the changed portion of the text. For "empty" updates, the file has not been modified - * and the text is empty. - * @param {{line: number, ch: number}} offset - - * the offset into the file where we want completions for - * @param {string} type - the type of the message to reply with. - */ - function getTernProperties(fileInfo, offset, type) { - - var request = buildRequest(fileInfo, "properties", offset), - i; - //_log("tern properties: request " + request.type + dir + " " + file); - try { - ternServer.request(request, function (error, data) { - var properties = []; - if (error) { - _log("Error returned from Tern 'properties' request: " + error); - } else { - //_log("tern properties: completions = " + data.completions.length); - for (i = 0; i < data.completions.length; ++i) { - var property = data.completions[i]; - properties.push({value: property, type: property.type, guess: true}); - } - } - - // Post a message back to the main thread with the completions - self.postMessage({type: type, - file: _getNormalizedFilename(fileInfo.name), - offset: offset, - properties: properties - }); - }); - } catch (e) { - _reportError(e, fileInfo.name); - } - } - - /** - * Get the completions for the given offset - * - * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo - * - type of update, name of file, and the text of the update. - * For "full" updates, the whole text of the file is present. For "part" updates, - * the changed portion of the text. For "empty" updates, the file has not been modified - * and the text is empty. - * @param {{line: number, ch: number}} offset - - * the offset into the file where we want completions for - * @param {boolean} isProperty - true if getting a property hint, - * otherwise getting an identifier hint. - */ - function getTernHints(fileInfo, offset, isProperty) { - - var request = buildRequest(fileInfo, "completions", offset), - i; - - //_log("request " + dir + " " + file + " " + offset /*+ " " + text */); - try { - ternServer.request(request, function (error, data) { - var completions = []; - if (error) { - _log("Error returned from Tern 'completions' request: " + error); - } else { - //_log("found " + data.completions.length + " for " + file + "@" + offset); - for (i = 0; i < data.completions.length; ++i) { - var completion = data.completions[i]; - completions.push({value: completion.name, type: completion.type, depth: completion.depth, - guess: completion.guess, origin: completion.origin, doc: completion.doc, url: completion.url}); - } - } - - if (completions.length > 0 || !isProperty) { - // Post a message back to the main thread with the completions - self.postMessage({type: MessageIds.TERN_COMPLETIONS_MSG, - file: _getNormalizedFilename(fileInfo.name), - offset: offset, - completions: completions - }); - } else { - // if there are no completions, then get all the properties - getTernProperties(fileInfo, offset, MessageIds.TERN_COMPLETIONS_MSG); - } - }); - } catch (e) { - _reportError(e, fileInfo.name); - } - } - - /** - * Given a Tern type object, convert it to an array of Objects, where each object describes - * a parameter. - * - * @param {!Infer.Fn} inferFnType - type to convert. - * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter. - */ - function getParameters(inferFnType) { - - // work around define functions before use warning. - var recordTypeToString, inferTypeToString, processInferFnTypeParameters, inferFnTypeToString; - - /** - * Convert an infer array type to a string. - * - * Formatted using google closure style. For example: - * - * "Array." - * - * @param {Infer.Arr} inferArrType - * - * @return {string} - array formatted in google closure style. - * - */ - function inferArrTypeToString(inferArrType) { - var result = "Array.<"; - - inferArrType.props[""].types.forEach(function (value, i) { - if (i > 0) { - result += ", "; - } - result += inferTypeToString(value); - }); - - // workaround case where types is zero length - if (inferArrType.props[""].types.length === 0) { - result += "Object"; - } - result += ">"; - - return result; - } - - /** - * Convert properties to a record type annotation. - * - * @param {Object} props - * @return {string} - record type annotation - */ - recordTypeToString = function (props) { - var result = "{", - first = true, - prop; - - for (prop in props) { - if (Object.prototype.hasOwnProperty.call(props, prop)) { - if (!first) { - result += ", "; - } - - first = false; - result += prop + ": " + inferTypeToString(props[prop]); - } - } - - result += "}"; - - return result; - }; - - /** - * Convert an infer type to a string. - * - * @param {*} inferType - one of the Infer's types; Infer.Prim, Infer.Arr, Infer.ANull. Infer.Fn functions are - * not handled here. - * - * @return {string} - * - */ - inferTypeToString = function (inferType) { - var result; - - if (inferType instanceof Infer.AVal) { - inferType = inferType.types[0]; - } - - if (inferType instanceof Infer.Prim) { - result = inferType.toString(); - if (result === "string") { - result = "String"; - } else if (result === "number") { - result = "Number"; - } else if (result === "boolean") { - result = "Boolean"; - } - } else if (inferType instanceof Infer.Arr) { - result = inferArrTypeToString(inferType); - } else if (inferType instanceof Infer.Fn) { - result = inferFnTypeToString(inferType); - } else if (inferType instanceof Infer.Obj) { - if (inferType.name === undefined) { - result = recordTypeToString(inferType.props); - } else { - result = inferType.name; - } - } else { - result = "Object"; - } - - return result; - }; - - /** - * Convert an infer function type to a Google closure type string. - * - * @param {Infer.Fn} inferType - type to convert. - * @return {string} - function type as a string. - */ - inferFnTypeToString = function (inferType) { - var result = "function(", - params = processInferFnTypeParameters(inferType); - - result += HintUtils2.formatParameterHint(params, null, null, true); - if (inferType.retval) { - result += "):"; - result += inferTypeToString(inferType.retval); - } - - return result; - }; - - /** - * Convert an infer function type to string. - * - * @param {*} inferType - one of the Infer's types; Infer.Fn, Infer.Prim, Infer.Arr, Infer.ANull - * @return {Array<{name: string, type: string, isOptional: boolean}>} where each entry in the array is a parameter. - */ - processInferFnTypeParameters = function (inferType) { - var params = [], - i; - - for (i = 0; i < inferType.args.length; i++) { - var param = {}, - name = inferType.argNames[i], - type = inferType.args[i]; - - if (!name) { - name = "param" + (i + 1); - } - - if (name[name.length - 1] === "?") { - name = name.substring(0, name.length - 1); - param.isOptional = true; - } - - param.name = name; - param.type = inferTypeToString(type); - params.push(param); - } - - return params; - }; - - return processInferFnTypeParameters(inferFnType); - } - - /** - * Get the function type for the given offset - * - * @param {{type: string, name: string, offsetLines: number, text: string}} fileInfo - * - type of update, name of file, and the text of the update. - * For "full" updates, the whole text of the file is present. For "part" updates, - * the changed portion of the text. For "empty" updates, the file has not been modified - * and the text is empty. - * @param {{line: number, ch: number}} offset - - * the offset into the file where we want completions for - */ - function handleFunctionType(fileInfo, offset) { - var request = buildRequest(fileInfo, "type", offset), - error; - - request.query.preferFunction = true; - - var fnType = ""; - try { - ternServer.request(request, function (ternError, data) { - - if (ternError) { - _log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError); - error = ternError.toString(); - } else { - var file = ternServer.findFile(fileInfo.name); - - // convert query from partial to full offsets - var newOffset = offset; - if (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) { - newOffset = {line: offset.line + fileInfo.offsetLines, ch: offset.ch}; - } - - request = buildRequest(createEmptyUpdate(fileInfo.name), "type", newOffset); - - var expr = Tern.findQueryExpr(file, request.query); - Infer.resetGuessing(); - var type = Infer.expressionType(expr); - type = type.getFunctionType() || type.getType(); - - if (type) { - fnType = getParameters(type); - } else { - ternError = "No parameter type found"; - _log(ternError); - } - } - }); - } catch (e) { - _reportError(e, fileInfo.name); - } - - // Post a message back to the main thread with the completions - self.postMessage({type: MessageIds.TERN_CALLED_FUNC_TYPE_MSG, - file: _getNormalizedFilename(fileInfo.name), - offset: offset, - fnType: fnType, - error: error - }); - } - - /** - * Add an array of files to tern. - * - * @param {Array.} files - each string in the array is the full - * path of a file. - */ - function handleAddFiles(files) { - files.forEach(function (file) { - ternServer.addFile(file); - }); - } - - /** - * Update the context of a file in tern. - * - * @param {string} path - full path of file. - * @param {string} text - content of the file. - */ - function handleUpdateFile(path, text) { - - ternServer.addFile(path, text); - - self.postMessage({type: MessageIds.TERN_UPDATE_FILE_MSG, - path: path - }); - - // reset to get the best hints with the updated file. - ternServer.reset(); - } - - /** - * Make a completions request to tern to force tern to resolve files - * and create a fast first lookup for the user. - * @param {string} path - the path of the file - */ - function handlePrimePump(path) { - var fileName = _getDenormalizedFilename(path); - var fileInfo = createEmptyUpdate(fileName), - request = buildRequest(fileInfo, "completions", {line: 0, ch: 0}); - - try { - ternServer.request(request, function (error, data) { - if (error) { - _log("Error returned from Tern 'completions' request: " + error); - self.postMessage({type: MessageIds.TERN_PRIME_PUMP_MSG, file: fileInfo.name, path: path}); - return; - } - - // Post a message back to the main thread - self.postMessage({type: MessageIds.TERN_PRIME_PUMP_MSG, - path: _getNormalizedFilename(path) - }); - }); - } catch (e) { - _reportError(e, path); - } - } - - /** - * Updates the configuration, typically for debugging purposes. - * - * @param {Object} configUpdate new configuration - */ - function setConfig(configUpdate) { - config = configUpdate; - } - - self.addEventListener("message", function (e) { - var file, text, offset, - request = e.data, - type = request.type; - - if (config.debug) { - _log("Message received " + type); - } - - if (type === MessageIds.TERN_INIT_MSG) { - - var dir = request.dir, - env = request.env, - files = request.files; - inferenceTimeout = request.timeout; - - initTernServer(dir, env, files); - } else if (type === MessageIds.TERN_COMPLETIONS_MSG) { - offset = request.offset; - getTernHints(request.fileInfo, offset, request.isProperty); - } else if (type === MessageIds.TERN_GET_FILE_MSG) { - file = request.file; - text = request.text; - handleGetFile(file, text); - } else if (type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) { - offset = request.offset; - handleFunctionType(request.fileInfo, offset); - } else if (type === MessageIds.TERN_JUMPTODEF_MSG) { - offset = request.offset; - getJumptoDef(request.fileInfo, offset); - } else if (type === MessageIds.TERN_ADD_FILES_MSG) { - handleAddFiles(request.files); - } else if (type === MessageIds.TERN_PRIME_PUMP_MSG) { - isUntitledDoc = request.isUntitledDoc; - handlePrimePump(request.path); - } else if (type === MessageIds.TERN_GET_GUESSES_MSG) { - offset = request.offset; - getTernProperties(request.fileInfo, offset, MessageIds.TERN_GET_GUESSES_MSG); - } else if (type === MessageIds.TERN_UPDATE_FILE_MSG) { - handleUpdateFile(request.path, request.text); - } else if (type === MessageIds.SET_CONFIG) { - setConfig(request.config); - } else { - _log("Unknown message: " + JSON.stringify(request)); - } - }); - // tell the main thread we're ready to start processing messages - self.postMessage({type: MessageIds.TERN_WORKER_READY}); - }); - }); - -}());