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;
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.
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();
}
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);
});
}
Used to avoid timing bugs in unit tests
function _readyPromise() {
return deferredPreferences;
}
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;
});
Add the files in the directory and subdirectories of a given directory 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);
});
}
Add new files to tern, keeping any previous files. The tern server must be initialized before making this call.
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 a pending request waiting for the tern-module to complete. If file is a detected exclusion, then reject 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();
}
We can skip tern initialization if we are opening a file that has already been added to tern.
function canSkipTernInitialization(newFile) {
return resolvedFiles[newFile] !== undefined;
}
Do the work to initialize a code hinting session.
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]);
}
});
});
});
});
}
Will initialize preferences only if they do not exist.
function ensurePreferences() {
if (!deferredPreferences) {
initPreferences();
}
}
check to see if the text we are sending to Tern is too long.
function filterText(text) {
var newText = text;
if (text.length > preferences.getMaxFileSize()) {
newText = "";
}
return newText;
}
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);
});
});
}
}
An array of library names that contain JavaScript builtins definitions.
function getBuiltins() {
return builtinLibraryNames;
}
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.
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;
}
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.
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;
}
Given a starting and ending position, get a code fragment that is self contained enough to be compiled.
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 a Promise for the definition from TernJS, for the file & offset passed in.
function getJumptoDef(fileInfo, offset) {
postMessage({
type: MessageIds.TERN_JUMPTODEF_MSG,
fileInfo: fileInfo,
offset: offset
});
return addPendingRequest(fileInfo.name, offset, MessageIds.TERN_JUMPTODEF_MSG);
}
Get the current offset. The offset is adjusted for "part" updates.
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 any pending $.Deferred object waiting on the specified file and request type
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;
}
}
function getResolvedPath(file) {
return currentModule.getResolvedPath(file);
}
function getResolvedPath(file) {
return resolvedFiles[file];
}
Get a Promise for the function type from TernJS.
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);
}
Get a Promise for the completions from TernJS, for the file & offset passed in.
function getTernHints(fileInfo, offset, isProperty) {
Get the text of a document, applying any size restrictions if necessary
function getTextFromDocument(document) {
var text = document.getText();
text = filterText(text);
return text;
}
Called each time a new editor becomes active.
function handleEditorChange(session, document, previousDocument) {
if (addFilesPromise === null) {
doEditorChange(session, document, previousDocument);
} else {
addFilesPromise.done(function () {
doEditorChange(session, document, previousDocument);
});
}
}
Called each time a new editor becomes active.
function handleEditorChange(session, document, previousDocument) {
if (!currentModule) {
currentModule = new TernModule();
}
return currentModule.handleEditorChange(session, document, previousDocument);
}
Called each time the file associated with the active editor changes. Marks the file as being dirty.
function handleFileChange(changeList) {
isDocumentDirty = true;
trackChange(changeList);
}
Handle the response from the tern node domain when it responds to the get guesses message.
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 with the definition
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 to the prime pump message.
function handlePrimePumpCompletion(response) {
var path = response.path,
type = response.type,
$deferredHints = getPendingRequest(path, OFFSET_ZERO, type);
if ($deferredHints) {
$deferredHints.resolve();
}
}
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);
}
Handle the response from the tern node domain when it responds with the references
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]);
}
}
Handle the response from the tern node domain when it responds with the scope data
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]);
}
}
Handle the response from the tern node domain when it responds with the list of completions
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 a request from the tern node domain for text of a file
function handleTernGetFile(request) {
function replyWith(name, txt) {
_postMessageByPass({
type: MessageIds.TERN_GET_FILE_MSG,
file: name,
text: txt
});
}
var name = request.file;
Handle timed out inference
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
});
});
Handle the response from the tern node domain when it responds to the update file message.
function handleUpdateFile(response) {
var path = response.path,
type = response.type,
$deferredHints = getPendingRequest(path, OFFSET_ZERO, type);
if ($deferredHints) {
$deferredHints.resolve();
}
}
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();
}
});
}
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 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 + "/";
}
Test if the directory should be excluded from analysis.
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
function isFileBeingEdited(filePath) {
var currentEditor = EditorManager.getActiveEditor(),
currentDoc = currentEditor && currentEditor.document;
return (currentDoc && currentDoc.file.fullPath === filePath);
}
Test if the file should be excluded from analysis.
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;
}
Test if the file path is an internal exclusion.
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;
}
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);
}
}
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);
});
}
Prime the pump for a fast first lookup.
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);
}
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.
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();
}
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().
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();
}
Request Jump-To-Definition from Tern.
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};
}
Request a parameter hint from Tern.
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();
}
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;
Track the update area of the current document so we can tell if we can send partial updates to tern or not.
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;
}
}
}
Update tern with the new contents of a given file.
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);
}