/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/JSUtils/ScopeManager.js
1 607 строк
59 KB
Boopesh Mahendran
Add Extract to Variable and Extract to Function features in js (#13980)
11 янв 2018, 09:16
11 янв 2018, 09:16
6a3feaf
Код
Авторство
О чём код?
/* * 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. * */ /* * Throughout this file, the term "outer scope" is used to refer to the outer- * most/global/root Scope objects for particular file. The term "inner scope" * is used to refer to a Scope object that is reachable via the child relation * from an outer scope. */ define(function (require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"); var CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"), DefaultDialogs = require("widgets/DefaultDialogs"), Dialogs = require("widgets/Dialogs"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), ExtensionUtils = require("utils/ExtensionUtils"), FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), LanguageManager = require("language/LanguageManager"), PreferencesManager = require("preferences/PreferencesManager"), ProjectManager = require("project/ProjectManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), NodeDomain = require("utils/NodeDomain"), InMemoryFile = require("document/InMemoryFile"); var HintUtils = require("./HintUtils"), MessageIds = require("./MessageIds"), Preferences = require("./Preferences"); var ternEnvironment = [], pendingTernRequests = {}, builtinFiles = ["ecmascript.json", "browser.json", "jquery.json"], builtinLibraryNames = [], isDocumentDirty = false, _hintCount = 0, currentModule = null, documentChanges = null, // bounds of document changes preferences = null, deferredPreferences = null; var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), _modulePath = FileUtils.getNativeModuleDirectoryPath(module), _nodePath = "node/TernNodeDomain", _absoluteModulePath = [_bracketsPath, _modulePath].join("/"), _domainPath = [_bracketsPath, _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. * * @return {Array.<string>} - array of library names. */ function getBuiltins() { return builtinLibraryNames; } /** * Read in the json files that have type information for the builtins, dom,etc */ function initTernEnv() { var path = [_absoluteModulePath, "node/node_modules/tern/defs/"].join("/"), files = builtinFiles, library; files.forEach(function (i) { FileSystem.resolve(path + i, function (err, file) { if (!err) { FileUtils.readAsText(file).done(function (text) { library = JSON.parse(text); builtinLibraryNames.push(library["!name"]); ternEnvironment.push(library); }).fail(function (error) { console.log("failed to read tern config file " + i); }); } else { console.log("failed to read tern config file " + i); } }); }); } initTernEnv(); /** * Init preferences from a file in the project root or builtin * defaults if no file is found; * * @param {string=} projectRootPath - new project root path. Only needed * for unit tests. */ function initPreferences(projectRootPath) { // Reject the old preferences if they have not completed. if (deferredPreferences && deferredPreferences.state() === "pending") { deferredPreferences.reject(); } deferredPreferences = $.Deferred(); var pr = ProjectManager.getProjectRoot(); // Open preferences relative to the project root // Normally there is a project root, but for unit tests we need to // pass in a project root. if (pr) { projectRootPath = pr.fullPath; } else if (!projectRootPath) { console.log("initPreferences: projectRootPath has no value"); } var path = projectRootPath + Preferences.FILE_NAME; FileSystem.resolve(path, function (err, file) { if (!err) { FileUtils.readAsText(file).done(function (text) { var configObj = null; try { configObj = JSON.parse(text); } catch (e) { // continue with null configObj which will result in // default settings. console.log("Error parsing preference file: " + path); if (e instanceof SyntaxError) { console.log(e.message); } } preferences = new Preferences(configObj); deferredPreferences.resolve(); }).fail(function (error) { preferences = new Preferences(); deferredPreferences.resolve(); }); } else { preferences = new Preferences(); deferredPreferences.resolve(); } }); } /** * Will initialize preferences only if they do not exist. * */ function ensurePreferences() { if (!deferredPreferences) { initPreferences(); } } /** * 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 (currentModule) { currentModule.postMessage(msg); } } /** * Test if the directory should be excluded from analysis. * * @param {!string} path - full directory path. * @return {boolean} true if excluded, false otherwise. */ function isDirectoryExcluded(path) { var excludes = preferences.getExcludedDirectories(); if (!excludes) { return false; } var testPath = ProjectManager.makeProjectRelativeIfPossible(path); testPath = FileUtils.stripTrailingSlash(testPath); return excludes.test(testPath); } /** * Test if the file path is in current editor * * @param {string} filePath file path to test for exclusion. * @return {boolean} true if in editor, false otherwise. */ function isFileBeingEdited(filePath) { var currentEditor = EditorManager.getActiveEditor(), currentDoc = currentEditor && currentEditor.document; return (currentDoc && currentDoc.file.fullPath === filePath); } /** * Test if the file path is an internal exclusion. * * @param {string} path file path to test for exclusion. * @return {boolean} true if excluded, false otherwise. */ function isFileExcludedInternal(path) { // The detectedExclusions are files detected to be troublesome with current versions of Tern. // detectedExclusions is an array of full paths. var detectedExclusions = PreferencesManager.get("jscodehints.detectedExclusions") || []; if (detectedExclusions && detectedExclusions.indexOf(path) !== -1) { return true; } return false; } /** * Test if the file should be excluded from analysis. * * @param {!File} file - file to test for exclusion. * @return {boolean} true if excluded, false otherwise. */ function isFileExcluded(file) { if (file.name[0] === ".") { return true; } var languageID = LanguageManager.getLanguageForPath(file.fullPath).getId(); if (languageID !== HintUtils.LANGUAGE_ID) { return true; } var excludes = preferences.getExcludedFiles(); if (excludes && excludes.test(file.name)) { return true; } if (isFileExcludedInternal(file.fullPath)) { return true; } return false; } /** * 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 * @param {{line: number, ch: number}} offset - the offset into the file the request is for * @param {string} type - the type of request * @return {jQuery.Promise} - the promise for the request */ function addPendingRequest(file, offset, type) { var requests, key = file + "@" + offset.line + "@" + offset.ch, $deferredRequest; // Reject detected exclusions if (isFileExcludedInternal(file)) { return (new $.Deferred()).reject().promise(); } if (_.has(pendingTernRequests, key)) { requests = pendingTernRequests[key]; } else { requests = {}; pendingTernRequests[key] = requests; } if (_.has(requests, type)) { $deferredRequest = requests[type]; } else { requests[type] = $deferredRequest = new $.Deferred(); } return $deferredRequest.promise(); } /** * Get any pending $.Deferred object waiting on the specified file and request type * @param {string} file - the file * @param {{line: number, ch: number}} offset - the offset into the file the request is for * @param {string} type - the type of request * @return {jQuery.Deferred} - the $.Deferred for the request */ function getPendingRequest(file, offset, type) { var key = file + "@" + offset.line + "@" + offset.ch; if (_.has(pendingTernRequests, key)) { var requests = pendingTernRequests[key], requestType = requests[type]; delete pendingTernRequests[key][type]; if (!Object.keys(requests).length) { delete pendingTernRequests[key]; } return requestType; } } /** * @param {string} file a relative path * @return {string} returns the path we resolved when we tried to parse the file, or undefined */ function getResolvedPath(file) { return currentModule.getResolvedPath(file); } /** * Get a Promise for the definition from TernJS, for the file & offset passed in. * @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 in the file the hints should be calculate at * @return {jQuery.Promise} - a promise that will resolve to definition when * it is done */ function getJumptoDef(fileInfo, offset) { postMessage({ type: MessageIds.TERN_JUMPTODEF_MSG, fileInfo: fileInfo, offset: offset }); return addPendingRequest(fileInfo.name, offset, MessageIds.TERN_JUMPTODEF_MSG); } /** * check to see if the text we are sending to Tern is too long. * @param {string} the text to check * @return {string} the text, or the empty text if the original was too long */ function filterText(text) { var newText = text; if (text.length > preferences.getMaxFileSize()) { newText = ""; } return newText; } /** * Get the text of a document, applying any size restrictions * if necessary * @param {Document} document - the document to get the text from * @return {string} the text, or the empty text if the original was too long */ function getTextFromDocument(document) { var text = document.getText(); text = filterText(text); return text; } /** * Handle the response from the tern node domain when * it responds with the references * * @param response - the response from the node domain */ function handleRename(response) { if (response.error) { EditorManager.getActiveEditor().displayErrorMessageAtCursor(response.error); return; } var file = response.file, offset = response.offset; var $deferredFindRefs = getPendingRequest(file, offset, MessageIds.TERN_REFS); if ($deferredFindRefs) { $deferredFindRefs.resolveWith(null, [response]); } } /** * Request Jump-To-Definition from Tern. * * @param {session} session - the session * @param {Document} document - the document * @param {{line: number, ch: number}} offset - the offset into the document * @return {jQuery.Promise} - The promise will not complete until tern * has completed. */ function requestJumptoDef(session, document, offset) { var path = document.file.fullPath, fileInfo = { type: MessageIds.TERN_FILE_INFO_TYPE_FULL, name: path, offsetLines: 0, text: filterText(session.getJavascriptText()) }; var ternPromise = getJumptoDef(fileInfo, offset); return {promise: ternPromise}; } /** * Handle the response from the tern node domain when * it responds with the definition * * @param response - the response from the node domain */ function handleJumptoDef(response) { var file = response.file, offset = response.offset; var $deferredJump = getPendingRequest(file, offset, MessageIds.TERN_JUMPTODEF_MSG); if ($deferredJump) { response.fullPath = getResolvedPath(response.resultFile); $deferredJump.resolveWith(null, [response]); } } /** * Handle the response from the tern node domain when * it responds with the scope data * * @param response - the response from the node domain */ function handleScopeData(response) { var file = response.file, offset = response.offset; var $deferredJump = getPendingRequest(file, offset, MessageIds.TERN_SCOPEDATA_MSG); if ($deferredJump) { $deferredJump.resolveWith(null, [response]); } } /** * Get a Promise for the completions from TernJS, for the file & offset passed in. * * @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 in the file the hints should be calculate at * @param {boolean} isProperty - true if getting a property hint, * otherwise getting an identifier hint. * @return {jQuery.Promise} - a promise that will resolve to an array of completions when * it is done */ function getTernHints(fileInfo, offset, isProperty) { /** * If the document is large and we have modified a small portions of it that * we are asking hints for, then send a partial document. */ postMessage({ type: MessageIds.TERN_COMPLETIONS_MSG, fileInfo: fileInfo, offset: offset, isProperty: isProperty }); return addPendingRequest(fileInfo.name, offset, MessageIds.TERN_COMPLETIONS_MSG); } /** * Get a Promise for the function type from TernJS. * @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 line, column info for what we want the function type of. * @return {jQuery.Promise} - a promise that will resolve to the function type of the function being called. */ function getTernFunctionType(fileInfo, offset) { postMessage({ type: MessageIds.TERN_CALLED_FUNC_TYPE_MSG, fileInfo: fileInfo, offset: offset }); return addPendingRequest(fileInfo.name, offset, MessageIds.TERN_CALLED_FUNC_TYPE_MSG); } /** * Given a starting and ending position, get a code fragment that is self contained * enough to be compiled. * * @param {!Session} session - the current session * @param {{line: number, ch: number}} start - the starting position of the changes * @return {{type: string, name: string, offsetLines: number, text: string}} */ function getFragmentAround(session, start) { var minIndent = null, minLine = null, endLine, cm = session.editor._codeMirror, tabSize = cm.getOption("tabSize"), document = session.editor.document, p, min, indent, line; // expand range backwards for (p = start.line - 1, min = Math.max(0, p - 100); p >= min; --p) { line = session.getLine(p); var fn = line.search(/\bfunction\b/); if (fn >= 0) { indent = CodeMirror.countColumn(line, null, tabSize); if (minIndent === null || minIndent > indent) { if (session.getToken({line: p, ch: fn + 1}).type === "keyword") { minIndent = indent; minLine = p; } } } } if (minIndent === null) { minIndent = 0; } if (minLine === null) { minLine = min; } var max = Math.min(cm.lastLine(), start.line + 100), endCh = 0; for (endLine = start.line + 1; endLine < max; ++endLine) { line = cm.getLine(endLine); if (line.length > 0) { indent = CodeMirror.countColumn(line, null, tabSize); if (indent <= minIndent) { endCh = line.length; break; } } } var from = {line: minLine, ch: 0}, to = {line: endLine, ch: endCh}; return {type: MessageIds.TERN_FILE_INFO_TYPE_PART, name: document.file.fullPath, offsetLines: from.line, text: document.getRange(from, to)}; } /** * Get an object that describes what tern needs to know about the updated * file to produce a hint. As a side-effect of this calls the document * changes are reset. * * @param {!Session} session - the current session * @param {boolean=} preventPartialUpdates - if true, disallow partial updates. * Optional, defaults to false. * @return {{type: string, name: string, offsetLines: number, text: string}} */ function getFileInfo(session, preventPartialUpdates) { var start = session.getCursor(), end = start, document = session.editor.document, path = document.file.fullPath, isHtmlFile = LanguageManager.getLanguageForPath(path).getId() === "html", result; if (isHtmlFile) { result = {type: MessageIds.TERN_FILE_INFO_TYPE_FULL, name: path, text: session.getJavascriptText()}; } else if (!documentChanges) { result = {type: MessageIds.TERN_FILE_INFO_TYPE_EMPTY, name: path, text: ""}; } else if (!preventPartialUpdates && session.editor.lineCount() > LARGE_LINE_COUNT && (documentChanges.to - documentChanges.from < LARGE_LINE_CHANGE) && documentChanges.from <= start.line && documentChanges.to > end.line) { result = getFragmentAround(session, start); } else { result = {type: MessageIds.TERN_FILE_INFO_TYPE_FULL, name: path, text: getTextFromDocument(document)}; } documentChanges = null; return result; } /** * Get the current offset. The offset is adjusted for "part" updates. * * @param {!Session} session - the current session * @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 default offset (optional). Will * use the cursor if not provided. * @return {{line: number, ch: number}} */ function getOffset(session, fileInfo, offset) { var newOffset; if (offset) { newOffset = {line: offset.line, ch: offset.ch}; } else { newOffset = session.getCursor(); } if (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) { newOffset.line = Math.max(0, newOffset.line - fileInfo.offsetLines); } return newOffset; } /** * Get a Promise for all of the known properties from TernJS, for the directory and file. * The properties will be used as guesses in tern. * @param {Session} session - the active hinting session * @param {Document} document - the document for which scope info is * desired * @return {jQuery.Promise} - The promise will not complete until the tern * request has completed. */ function requestGuesses(session, document) { var $deferred = $.Deferred(), fileInfo = getFileInfo(session), offset = getOffset(session, fileInfo); postMessage({ type: MessageIds.TERN_GET_GUESSES_MSG, fileInfo: fileInfo, offset: offset }); var promise = addPendingRequest(fileInfo.name, offset, MessageIds.TERN_GET_GUESSES_MSG); promise.done(function (guesses) { session.setGuesses(guesses); $deferred.resolve(); }).fail(function () { $deferred.reject(); }); return $deferred.promise(); } /** * 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.<string>, * properties:Array.<string>}} response - the response from node domain */ function handleTernCompletions(response) { var file = response.file, offset = response.offset, completions = response.completions, properties = response.properties, fnType = response.fnType, type = response.type, error = response.error, $deferredHints = getPendingRequest(file, offset, type); if ($deferredHints) { if (error) { $deferredHints.reject(); } else if (completions) { $deferredHints.resolveWith(null, [{completions: completions}]); } else if (properties) { $deferredHints.resolveWith(null, [{properties: properties}]); } else if (fnType) { $deferredHints.resolveWith(null, [fnType]); } } } /** * 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.<string>}} response - * the response from node domain contains the guesses for a * property lookup. */ function handleGetGuesses(response) { var path = response.file, type = response.type, offset = response.offset, $deferredHints = getPendingRequest(path, offset, type); if ($deferredHints) { $deferredHints.resolveWith(null, [response.properties]); } } /** * 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 node domain */ function handleUpdateFile(response) { var path = response.path, type = response.type, $deferredHints = getPendingRequest(path, OFFSET_ZERO, type); if ($deferredHints) { $deferredHints.resolve(); } } /** * Handle timed out inference * * @param {{path: string, type: string}} response - the response from node domain */ function handleTimedOut(response) { var detectedExclusions = PreferencesManager.get("jscodehints.detectedExclusions") || [], filePath = response.file; // Don't exclude the file currently being edited if (isFileBeingEdited(filePath)) { return; } // Handle file that is already excluded if (detectedExclusions.indexOf(filePath) !== -1) { console.log("JavaScriptCodeHints.handleTimedOut: file already in detectedExclusions array timed out: " + filePath); return; } // Save detected exclusion in project prefs so no further time is wasted on it detectedExclusions.push(filePath); PreferencesManager.set("jscodehints.detectedExclusions", detectedExclusions, { location: { scope: "project" } }); // Show informational dialog Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_INFO, Strings.DETECTED_EXCLUSION_TITLE, StringUtils.format( Strings.DETECTED_EXCLUSION_INFO, StringUtils.breakableUrl(filePath) ), [ { className : Dialogs.DIALOG_BTN_CLASS_PRIMARY, id : Dialogs.DIALOG_BTN_OK, text : Strings.OK } ] ); } 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 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 TernModule() { var ternPromise = null, addFilesPromise = null, rootTernDir = null, projectRoot = null, stopAddingFiles = false, resolvedFiles = {}, // file -> resolved file numInitialFiles = 0, numResolvedFiles = 0, numAddedFiles = 0, _ternNodeDomain = null; /** * @param {string} file a relative path * @return {string} returns the path we resolved when we tried to parse the file, or undefined */ function getResolvedPath(file) { return resolvedFiles[file]; } /** * Determine whether the current set of files are using modules to pull in * additional files. * * @return {boolean} - true if more files than the current directory have * been read in. */ function usingModules() { return numInitialFiles !== numResolvedFiles; } /** * 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 (ternModule) { // If an error came up during file handling, bail out now if (!_ternNodeDomain) { return; } if (config.debug) { console.debug("Sending message", msg); } _ternNodeDomain.exec("invokeTernCommand", msg); }); } /** * 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 (ternModule) { if (config.debug) { console.debug("Sending message", msg); } _ternNodeDomain.exec("invokeTernCommand", msg); }); } /** * Update tern with the new contents of a given file. * * @param {Document} document - the document to update * @return {jQuery.Promise} - the promise for the request */ function updateTernFile(document) { var path = document.file.fullPath; _postMessageByPass({ type : MessageIds.TERN_UPDATE_FILE_MSG, path : path, text : getTextFromDocument(document) }); return addPendingRequest(path, OFFSET_ZERO, MessageIds.TERN_UPDATE_FILE_MSG); } /** * Handle a request from the tern node domain for text of a file * * @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) { function replyWith(name, txt) { _postMessageByPass({ type: MessageIds.TERN_GET_FILE_MSG, file: name, text: txt }); } var name = request.file; /** * 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 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 * @return {jQuery.Promise} - the Promise returned from DocumentMangaer.getDocumentText() */ function getDocText(filePath) { if (!FileSystem.isAbsolutePath(filePath) || // don't handle URLs filePath.slice(0, 2) === "//") { // don't handle protocol-relative URLs like //example.com/main.js (see #10566) return (new $.Deferred()).reject().promise(); } var file = FileSystem.getFileForPath(filePath), promise = DocumentManager.getDocumentText(file); promise.done(function (docText) { resolvedFiles[name] = filePath; numResolvedFiles++; replyWith(name, filterText(docText)); }); return promise; } /** * Helper function to find any files in the project that end with the * name we are looking for. This is so we can find requirejs modules * when the baseUrl is unknown, or when the project root is not the same * as the script root (e.g. if you open the 'brackets' dir instead of 'brackets/src' dir). */ function findNameInProject() { // check for any files in project that end with the right path. var fileName = name.substring(name.lastIndexOf("/") + 1); function _fileFilter(entry) { return entry.name === fileName; } ProjectManager.getAllFiles(_fileFilter).done(function (files) { var file; files = files.filter(function (file) { var pos = file.fullPath.length - name.length; return pos === file.fullPath.lastIndexOf(name); }); if (files.length === 1) { file = files[0]; } if (file) { getDocText(file.fullPath).fail(function () { replyWith(name, ""); }); } else { replyWith(name, ""); } }); } if (!isFileExcludedInternal(name)) { getDocText(name).fail(function () { getDocText(rootTernDir + name).fail(function () { // check relative to project root getDocText(projectRoot + name) // last look for any files that end with the right path // in the project .fail(findNameInProject); }); }); } } /** * Prime the pump for a fast first lookup. * * @param {string} path - full path of file * @return {jQuery.Promise} - the promise for the request */ function primePump(path, isUntitledDoc) { _postMessageByPass({ type : MessageIds.TERN_PRIME_PUMP_MSG, path : path, isUntitledDoc : isUntitledDoc }); return addPendingRequest(path, OFFSET_ZERO, MessageIds.TERN_PRIME_PUMP_MSG); } /** * 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 node domain */ function handlePrimePumpCompletion(response) { var path = response.path, type = response.type, $deferredHints = getPendingRequest(path, OFFSET_ZERO, type); if ($deferredHints) { $deferredHints.resolve(); } } /** * Add new files to tern, keeping any previous files. * The tern server must be initialized before making * this call. * * @param {Array.<string>} files - array of file to add to tern. * @return {boolean} - true if more files may be added, false if maximum has been reached. */ function addFilesToTern(files) { // limit the number of files added to tern. var maxFileCount = preferences.getMaxFileCount(); if (numResolvedFiles + numAddedFiles < maxFileCount) { var available = maxFileCount - numResolvedFiles - numAddedFiles; if (available < files.length) { files = files.slice(0, available); } numAddedFiles += files.length; ternPromise.done(function (ternModule) { var msg = { type : MessageIds.TERN_ADD_FILES_MSG, files : files }; if (config.debug) { console.debug("Sending message", msg); } _ternNodeDomain.exec("invokeTernCommand", msg); }); } else { stopAddingFiles = true; } return stopAddingFiles; } /** * Add the files in the directory and subdirectories of a given directory * to tern. * * @param {string} dir - the root directory to add. * @param {function ()} doneCallback - called when all files have been * added to tern. */ function addAllFilesAndSubdirectories(dir, doneCallback) { FileSystem.resolve(dir, function (err, directory) { function visitor(entry) { if (entry.isFile) { if (!isFileExcluded(entry)) { // ignore .dotfiles and non-.js files addFilesToTern([entry.fullPath]); } } else { return !isDirectoryExcluded(entry.fullPath) && entry.name.indexOf(".") !== 0 && !stopAddingFiles; } } if (err) { return; } if (dir === FileSystem.getDirectoryForPath(rootTernDir)) { doneCallback(); return; } directory.visit(visitor, doneCallback); }); } /** * Init the Tern module that does all the code hinting work. */ function initTernModule() { var moduleDeferred = $.Deferred(); ternPromise = moduleDeferred.promise(); function prepareTern() { _ternNodeDomain.exec("setInterface", { messageIds : MessageIds }); _ternNodeDomain.exec("invokeTernCommand", { type: MessageIds.SET_CONFIG, config: config }); moduleDeferred.resolveWith(null, [_ternNodeDomain]); } 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); } 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_SCOPEDATA_MSG) { handleScopeData(response); } else if (type === MessageIds.TERN_REFS) { handleRename(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 if (type === "RE_INIT_TERN") { // Ensure the request is because of a node restart if (currentModule) { prepareTern(); // Mark the module with resetForced, then creation of TernModule will // happen again as part of '_maybeReset' call currentModule.resetForced = true; } } else { console.log("Tern Module: " + (response.log || response)); } }); _ternNodeDomain.promise().done(prepareTern); } } /** * Create a new tern server. */ function initTernServer(dir, files) { initTernModule(); numResolvedFiles = 0; numAddedFiles = 0; stopAddingFiles = false; numInitialFiles = files.length; ternPromise.done(function (ternModule) { var msg = { type : MessageIds.TERN_INIT_MSG, dir : dir, files : files, env : ternEnvironment, timeout : PreferencesManager.get("jscodehints.inferenceTimeout") }; _ternNodeDomain.exec("invokeTernCommand", msg); }); rootTernDir = dir + "/"; } /** * We can skip tern initialization if we are opening a file that has * already been added to tern. * * @param {string} newFile - full path of new file being opened in the editor. * @return {boolean} - true if tern initialization should be skipped, * false otherwise. */ function canSkipTernInitialization(newFile) { return resolvedFiles[newFile] !== undefined; } /** * Do the work to initialize a code hinting session. * * @param {Session} session - the active hinting session (TODO: currently unused) * @param {!Document} document - the document the editor has changed to * @param {?Document} previousDocument - the document the editor has changed from */ function doEditorChange(session, document, previousDocument) { var file = document.file, path = file.fullPath, dir = file.parentPath, pr; var addFilesDeferred = $.Deferred(); documentChanges = null; addFilesPromise = addFilesDeferred.promise(); pr = ProjectManager.getProjectRoot() ? ProjectManager.getProjectRoot().fullPath : null; // avoid re-initializing tern if possible. if (canSkipTernInitialization(path)) { // update the previous document in tern to prevent stale files. if (isDocumentDirty && previousDocument) { var updateFilePromise = updateTernFile(previousDocument); updateFilePromise.done(function () { primePump(path, document.isUntitled()); addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); } else { addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } isDocumentDirty = false; return; } if (previousDocument && previousDocument.isDirty) { updateTernFile(previousDocument); } isDocumentDirty = false; resolvedFiles = {}; projectRoot = pr; ensurePreferences(); deferredPreferences.done(function () { if (file instanceof InMemoryFile) { initTernServer(pr, []); var hintsPromise = primePump(path, true); hintsPromise.done(function () { addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); return; } FileSystem.resolve(dir, function (err, directory) { if (err) { console.error("Error resolving", dir); addFilesDeferred.resolveWith(null); return; } directory.getContents(function (err, contents) { if (err) { console.error("Error getting contents for", directory); addFilesDeferred.resolveWith(null); return; } var files = contents .filter(function (entry) { return entry.isFile && !isFileExcluded(entry); }) .map(function (entry) { return entry.fullPath; }); initTernServer(dir, files); var hintsPromise = primePump(path, false); hintsPromise.done(function () { if (!usingModules()) { // Read the subdirectories of the new file's directory. // Read them first in case there are too many files to // read in the project. addAllFilesAndSubdirectories(dir, function () { // If the file is in the project root, then read // all the files under the project root. var currentDir = (dir + "/"); if (projectRoot && currentDir !== projectRoot && currentDir.indexOf(projectRoot) === 0) { addAllFilesAndSubdirectories(projectRoot, function () { // prime the pump again but this time don't wait // for completion. primePump(path, false); addFilesDeferred.resolveWith(null, [_ternNodeDomain]); }); } else { addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } }); } else { addFilesDeferred.resolveWith(null, [_ternNodeDomain]); } }); }); }); }); } /** * Called each time a new editor becomes active. * * @param {Session} session - the active hinting session (TODO: currently unused by doEditorChange()) * @param {!Document} document - the document of the editor that has changed * @param {?Document} previousDocument - the document of the editor is changing from */ function handleEditorChange(session, document, previousDocument) { if (addFilesPromise === null) { doEditorChange(session, document, previousDocument); } else { addFilesPromise.done(function () { doEditorChange(session, document, previousDocument); }); } } /** * Do some cleanup when a project is closed. * * 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 resetModule() { function resetTernServer() { if (_ternNodeDomain.ready()) { _ternNodeDomain.exec('resetTernServer'); } } if (_ternNodeDomain) { if (addFilesPromise) { // If we're in the middle of added files, don't reset // until we're done addFilesPromise.done(resetTernServer).fail(resetTernServer); } else { resetTernServer(); } } } function whenReady(func) { addFilesPromise.done(func); } this.resetModule = resetModule; this.handleEditorChange = handleEditorChange; this.postMessage = postMessage; this.getResolvedPath = getResolvedPath; this.whenReady = whenReady; return this; } var resettingDeferred = null; /** * 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 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 module is ready. * The new (or current, if there was no reset) module is passed to the callback. */ function _maybeReset(session, document, force) { var newTernModule; // if we're in the middle of a reset, don't have to check // 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 module isn't // getting reset all the time. if (currentModule.resetForced || force || (!config.noReset && ++_hintCount > MAX_HINTS)) { if (config.debug) { console.debug("Resetting tern module"); } resettingDeferred = new $.Deferred(); 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(currentModule); return d.promise(); } } return resettingDeferred.promise(); } /** * Request a parameter hint from Tern. * * @param {Session} session - the active hinting session * @param {{line: number, ch: number}} functionOffset - the offset of the function call. * @return {jQuery.Promise} - The promise will not complete until the * hint has completed. */ function requestParameterHint(session, functionOffset) { var $deferredHints = $.Deferred(), fileInfo = getFileInfo(session, true), offset = getOffset(session, fileInfo, functionOffset), fnTypePromise = getTernFunctionType(fileInfo, offset); $.when(fnTypePromise).done( function (fnType) { session.setFnType(fnType); session.setFunctionCallPos(functionOffset); $deferredHints.resolveWith(null, [fnType]); } ).fail(function () { $deferredHints.reject(); }); return $deferredHints.promise(); } /** * Request hints from Tern. * * Note that successive calls to getScope may return the same objects, so * clients that wish to modify those objects (e.g., by annotating them based * on some temporary context) should copy them first. See, e.g., * Session.getHints(). * * @param {Session} session - the active hinting session * @param {Document} document - the document for which scope info is * desired * @return {jQuery.Promise} - The promise will not complete until the tern * hints have completed. */ function requestHints(session, document) { var $deferredHints = $.Deferred(), hintPromise, sessionType = session.getType(), fileInfo = getFileInfo(session), offset = getOffset(session, fileInfo, null); _maybeReset(session, document); hintPromise = getTernHints(fileInfo, offset, sessionType.property); $.when(hintPromise).done( function (completions, fnType) { if (completions.completions) { session.setTernHints(completions.completions); session.setGuesses(null); } else { session.setTernHints([]); session.setGuesses(completions.properties); } $deferredHints.resolveWith(null); } ).fail(function () { $deferredHints.reject(); }); return $deferredHints.promise(); } /** * Track the update area of the current document so we can tell if we can send * partial updates to tern or not. * * @param {Array.<{from: {line:number, ch: number}, to: {line:number, ch: number}, * text: Array<string>}>} changeList - the document changes from the current change event */ function trackChange(changeList) { var changed = documentChanges, i; if (changed === null) { documentChanges = changed = {from: changeList[0].from.line, to: changeList[0].from.line}; if (config.debug) { console.debug("ScopeManager: document has changed"); } } for (i = 0; i < changeList.length; i++) { var thisChange = changeList[i], end = thisChange.from.line + (thisChange.text.length - 1); if (thisChange.from.line < changed.to) { changed.to = changed.to - (thisChange.to.line - end); } if (end >= changed.to) { changed.to = end + 1; } if (changed.from > thisChange.from.line) { changed.from = thisChange.from.line; } } } /* * Called each time the file associated with the active editor changes. * Marks the file as being dirty. * * @param {from: {line:number, ch: number}, to: {line:number, ch: number}} */ function handleFileChange(changeList) { isDocumentDirty = true; trackChange(changeList); } /** * Called each time a new editor becomes active. * * @param {Session} session - the active hinting session * @param {Document} document - the document of the editor that has changed * @param {?Document} previousDocument - the document of the editor is changing from */ function handleEditorChange(session, document, previousDocument) { if (!currentModule) { currentModule = new TernModule(); } return currentModule.handleEditorChange(session, document, previousDocument); } /** * Do some cleanup when a project is closed. * Clean up previous analysis data from the module */ function handleProjectClose() { if (currentModule) { currentModule.resetModule(); } } /** * Read in project preferences when a new project is opened. * Look in the project root directory for a preference file. * * @param {string=} projectRootPath - new project root path(optional). * Only needed for unit tests. */ function handleProjectOpen(projectRootPath) { initPreferences(projectRootPath); } /** Used to avoid timing bugs in unit tests */ function _readyPromise() { return deferredPreferences; } /** * @private * * Update the configuration in the tern node domain. */ function _setConfig(configUpdate) { config = brackets._configureJSCodeHints.config; postMessage({ type: MessageIds.SET_CONFIG, config: configUpdate }); } exports._setConfig = _setConfig; exports._maybeReset = _maybeReset; exports.getBuiltins = getBuiltins; exports.getResolvedPath = getResolvedPath; exports.getTernHints = getTernHints; exports.handleEditorChange = handleEditorChange; exports.requestGuesses = requestGuesses; exports.handleFileChange = handleFileChange; exports.requestHints = requestHints; exports.requestJumptoDef = requestJumptoDef; exports.requestParameterHint = requestParameterHint; exports.handleProjectClose = handleProjectClose; exports.handleProjectOpen = handleProjectOpen; exports._readyPromise = _readyPromise; exports.filterText = filterText; exports.postMessage = postMessage; exports.addPendingRequest = addPendingRequest; });