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;
    }
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;
    }
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 ; }
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.
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 .
TernModule - TernModule constructor