Send a log message back from the node to the main thread
function _log(msg) {
console.log(msg);
}
Report exception
function _reportError(e, file) {
if (e instanceof Infer.TimedOut) {
// Post a message back to the main thread with timedout info
self.postMessage({
type: MessageIds.TERN_INFERENCE_TIMEDOUT,
file: file
});
} else {
_log("Error thrown in tern_node domain:" + e.message + "\n" + e.stack);
}
}
Callback handle to request contents of a file from the main thread
function _requestFileContent(name) {
self.postMessage({
type: MessageIds.TERN_GET_FILE_MSG,
file: name
});
}
Build an object that can be used as a request to tern.
function buildRequest(fileInfo, query, offset) {
query = {type: query};
query.start = offset;
query.end = offset;
query.file = (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) ? "#0" : fileInfo.name;
query.filter = false;
query.sort = false;
query.depths = true;
query.guess = true;
query.origins = true;
query.types = true;
query.expandWordForward = false;
query.lineCharPositions = true;
query.docs = true;
query.urls = true;
var request = {query: query, files: [], offset: offset, timeout: inferenceTimeout};
if (fileInfo.type !== MessageIds.TERN_FILE_INFO_TYPE_EMPTY) {
// Create a copy to mutate ahead
var fileInfoCopy = JSON.parse(JSON.stringify(fileInfo));
request.files.push(fileInfoCopy);
}
return request;
}
Create a "empty" update object.
function createEmptyUpdate(path) {
return {type: MessageIds.TERN_FILE_INFO_TYPE_EMPTY,
name: path,
offsetLines: 0,
text: ""};
}
Format the given parameter array. Handles separators between parameters, syntax for optional parameters, and the order of the parameter type and parameter name.
function formatParameterHint(params, appendSeparators, appendParameter, typesOnly) {
var result = "",
pendingOptional = false;
params.forEach(function (value, i) {
var param = value.type,
separators = "";
if (value.isOptional) {
// if an optional param is following by an optional parameter, then
// terminate the bracket. Otherwise enclose a required parameter
// in the same bracket.
if (pendingOptional) {
separators += "]";
}
pendingOptional = true;
}
if (i > 0) {
separators += ", ";
}
if (value.isOptional) {
separators += "[";
}
if (appendSeparators) {
appendSeparators(separators);
}
result += separators;
if (!typesOnly) {
param += " " + value.name;
}
if (appendParameter) {
appendParameter(param, i);
}
result += param;
});
if (pendingOptional) {
if (appendSeparators) {
appendSeparators("]");
}
result += "]";
}
return result;
}
Provide the contents of the requested file to tern
function getFile(name, next) {
// save the callback
fileCallBacks[name] = next;
setImmediate(function () {
try {
ExtractContent.extractContent(name, handleGetFile, _requestFileContent);
} catch (error) {
console.log(error);
}
});
}
Get definition location
function getJumptoDef(fileInfo, offset) {
var request = buildRequest(fileInfo, "definition", offset);
// request.query.typeOnly = true; // FIXME: tern doesn't work exactly right yet.
try {
ternServer.request(request, function (error, data) {
if (error) {
_log("Error returned from Tern 'definition' request: " + error);
self.postMessage({type: MessageIds.TERN_JUMPTODEF_MSG, file: fileInfo.name, offset: offset});
return;
}
var response = {
type: MessageIds.TERN_JUMPTODEF_MSG,
file: _getNormalizedFilename(fileInfo.name),
resultFile: data.file,
offset: offset,
start: data.start,
end: data.end
};
request = buildRequest(fileInfo, "type", offset);
// See if we can tell if the reference is to a Function type
ternServer.request(request, function (error, data) {
if (!error) {
response.isFunction = data.type.length > 2 && data.type.substring(0, 2) === "fn";
}
// Post a message back to the main thread with the definition
self.postMessage(response);
});
});
} catch (e) {
_reportError(e, fileInfo.name);
}
}
Given a Tern type object, convert it to an array of Objects, where each object describes a parameter.
function getParameters(inferFnType) {
// work around define functions before use warning.
var recordTypeToString, inferTypeToString, processInferFnTypeParameters, inferFnTypeToString;
Get all References location
function getRefs(fileInfo, offset) {
var request = buildRequest(fileInfo, "refs", offset);
try {
ternServer.request(request, function (error, data) {
if (error) {
_log("Error returned from Tern 'refs' request: " + error);
var response = {
type: MessageIds.TERN_REFS,
error: error.message
};
self.postMessage(response);
return;
}
var response = {
type: MessageIds.TERN_REFS,
file: fileInfo.name,
offset: offset,
references: data
};
// Post a message back to the main thread with the results
self.postMessage(response);
});
} catch (e) {
_reportError(e, fileInfo.name);
}
}
Get scope at the offset in the file
function getScopeData(fileInfo, offset) {
// Create a new tern Server
// Existing tern server resolves all the required modules which might take time
// We only need to analyze single file for getting the scope
ternOptions.plugins = {};
var ternServer = new Tern.Server(ternOptions);
ternServer.addFile(fileInfo.name, fileInfo.text);
var error;
var request = buildRequest(fileInfo, "completions", offset); // for primepump
try {
// primepump
ternServer.request(request, function (ternError, data) {
if (ternError) {
_log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError);
error = ternError.toString();
} else {
var file = ternServer.findFile(fileInfo.name);
var scope = Infer.scopeAt(file.ast, Tern.resolvePos(file, offset), file.scope);
if (scope) {
// Remove unwanted properties to remove cycles in the object
scope = JSON.parse(JSON.stringify(scope, function(key, value) {
if (["proto", "propertyOf", "onNewProp", "sourceFile", "maybeProps"].includes(key)) {
return undefined;
}
else if (key === "fnType") {
return value.name || "FunctionExpression";
}
else if (key === "props") {
for (var key in value) {
value[key] = value[key].propertyName;
}
return value;
} else if (key === "originNode") {
return value && {
start: value.start,
end: value.end,
type: value.type,
body: {
start: value.body.start,
end: value.body.end
}
};
}
return value;
}));
}
self.postMessage({
type: MessageIds.TERN_SCOPEDATA_MSG,
file: _getNormalizedFilename(fileInfo.name),
offset: offset,
scope: scope
});
}
});
} catch (e) {
_reportError(e, fileInfo.name);
} finally {
ternServer.reset();
Infer.resetGuessing();
}
}
Get the completions for the given offset
function getTernHints(fileInfo, offset, isProperty) {
var request = buildRequest(fileInfo, "completions", offset),
i;
//_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
try {
ternServer.request(request, function (error, data) {
var completions = [];
if (error) {
_log("Error returned from Tern 'completions' request: " + error);
} else {
//_log("found " + data.completions + " for " + file + "@" + offset);
completions = data.completions.map(function (completion) {
return {value: completion.name, type: completion.type, depth: completion.depth,
guess: completion.guess, origin: completion.origin, doc: completion.doc, url: completion.url};
});
}
if (completions.length > 0 || !isProperty) {
// Post a message back to the main thread with the completions
self.postMessage({type: MessageIds.TERN_COMPLETIONS_MSG,
file: _getNormalizedFilename(fileInfo.name),
offset: offset,
completions: completions
});
} else {
// if there are no completions, then get all the properties
getTernProperties(fileInfo, offset, MessageIds.TERN_COMPLETIONS_MSG);
}
});
} catch (e) {
_reportError(e, fileInfo.name);
}
}
Get all the known properties for guessing.
function getTernProperties(fileInfo, offset, type) {
var request = buildRequest(fileInfo, "properties", offset),
i;
//_log("tern properties: request " + request.type + dir + " " + file);
try {
ternServer.request(request, function (error, data) {
var properties = [];
if (error) {
_log("Error returned from Tern 'properties' request: " + error);
} else {
//_log("tern properties: completions = " + data.completions.length);
properties = data.completions.map(function (completion) {
return {value: completion, type: completion.type, guess: true};
});
}
// Post a message back to the main thread with the completions
self.postMessage({type: type,
file: _getNormalizedFilename(fileInfo.name),
offset: offset,
properties: properties
});
});
} catch (e) {
_reportError(e, fileInfo.name);
}
}
Add an array of files to tern.
function handleAddFiles(files) {
files.forEach(function (file) {
ternServer.addFile(file);
});
}
Get the function type for the given offset
function handleFunctionType(fileInfo, offset) {
var request = buildRequest(fileInfo, "type", offset),
error;
request.query.preferFunction = true;
var fnType = "";
try {
ternServer.request(request, function (ternError, data) {
if (ternError) {
_log("Error for Tern request: \n" + JSON.stringify(request) + "\n" + ternError);
error = ternError.toString();
} else {
var file = ternServer.findFile(fileInfo.name);
// convert query from partial to full offsets
var newOffset = offset;
if (fileInfo.type === MessageIds.TERN_FILE_INFO_TYPE_PART) {
newOffset = {line: offset.line + fileInfo.offsetLines, ch: offset.ch};
}
request = buildRequest(createEmptyUpdate(fileInfo.name), "type", newOffset);
var expr = Tern.findQueryExpr(file, request.query);
Infer.resetGuessing();
var type = Infer.expressionType(expr);
type = type.getFunctionType() || type.getType();
if (type) {
fnType = getParameters(type);
} else {
ternError = "No parameter type found";
_log(ternError);
}
}
});
} catch (e) {
_reportError(e, fileInfo.name);
}
// Post a message back to the main thread with the completions
self.postMessage({type: MessageIds.TERN_CALLED_FUNC_TYPE_MSG,
file: _getNormalizedFilename(fileInfo.name),
offset: offset,
fnType: fnType,
error: error
});
}
Handle a response from the main thread providing the contents of a file
function handleGetFile(file, text) {
var next = fileCallBacks[file];
if (next) {
try {
next(null, text);
} catch (e) {
_reportError(e, file);
}
}
delete fileCallBacks[file];
}
function _getNormalizedFilename(fileName) {
if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === -1) {
fileName = ternServer.projectDir + fileName;
}
return fileName;
}
function _getDenormalizedFilename(fileName) {
if (!isUntitledDoc && ternServer.projectDir && fileName.indexOf(ternServer.projectDir) === 0) {
fileName = fileName.slice(ternServer.projectDir.length);
}
return fileName;
}
Make a completions request to tern to force tern to resolve files and create a fast first lookup for the user.
function handlePrimePump(path) {
var fileName = _getDenormalizedFilename(path);
var fileInfo = createEmptyUpdate(fileName),
request = buildRequest(fileInfo, "completions", {line: 0, ch: 0});
try {
ternServer.request(request, function (error, data) {
// Post a message back to the main thread
self.postMessage({type: MessageIds.TERN_PRIME_PUMP_MSG,
path: _getNormalizedFilename(path)
});
});
} catch (e) {
_reportError(e, path);
}
}
Update the context of a file in tern.
function handleUpdateFile(path, text) {
ternServer.addFile(path, text);
self.postMessage({type: MessageIds.TERN_UPDATE_FILE_MSG,
path: path
});
// reset to get the best hints with the updated file.
ternServer.reset();
Infer.resetGuessing();
}
Convert an infer array type to a string.
Formatted using google closure style. For example:
"Array.<string, number>"
function inferArrTypeToString(inferArrType) {
var result = "Array.<";
result += inferArrType.props["<i>"].types.map(inferTypeToString).join(", ");
// workaround case where types is zero length
if (inferArrType.props["<i>"].types.length === 0) {
result += "Object";
}
result += ">";
return result;
}
Initialize the test domain with commands and events related to find in files.
function init(domainManager) {
if (!domainManager.hasDomain("TernNodeDomain")) {
domainManager.registerDomain("TernNodeDomain", {major: 0, minor: 1});
}
_domainManager = domainManager;
domainManager.registerCommand(
"TernNodeDomain", // domain name
"invokeTernCommand", // command name
invokeTernCommand, // command handler function
false, // this command is synchronous in Node
"Invokes a tern command on node",
[{name: "commandConfig", // parameters
type: "object",
description: "Object containing tern command configuration"}]
);
domainManager.registerCommand(
"TernNodeDomain", // domain name
"setInterface", // command name
setInterface, // command handler function
false, // this command is synchronous in Node
"Sets the shared message interface",
[{name: "msgInterface", // parameters
type: "object",
description: "Object containing messageId enums"}]
);
domainManager.registerCommand(
"TernNodeDomain", // domain name
"resetTernServer", // command name
resetTernServer, // command handler function
true, // this command is synchronous in Node
"Resets an existing tern server"
);
domainManager.registerEvent(
"TernNodeDomain", // domain name
"data", // event name
[
{
name: "data",
type: "Object",
description: "data to be returned to main thread"
}
]
);
setTimeout(checkInterfaceAndReInit, 1000);
}
exports.init = init;
Create a new tern server.
function initTernServer(env, files) {
ternOptions = {
defs: env,
async: true,
getFile: getFile,
plugins: {requirejs: {}, doc_comment: true, angular: true},
ecmaVersion: 9
};
// If a server is already created just reset the analysis data before marking it for GC
if (ternServer) {
ternServer.reset();
Infer.resetGuessing();
}
ternServer = new Tern.Server(ternOptions);
files.forEach(function (file) {
ternServer.addFile(file);
});
}
Resets an existing tern server.
function resetTernServer() {
// If a server is already created just reset the analysis data
if (ternServer) {
ternServer.reset();
Infer.resetGuessing();
// tell the main thread we're ready to start processing again
self.postMessage({type: MessageIds.TERN_WORKER_READY});
}
}
Updates the configuration, typically for debugging purposes.
function setConfig(configUpdate) {
config = configUpdate;
}
function _requestTernServer(commandConfig) {
var file, text, offset,
request = commandConfig,
type = request.type;
if (config.debug) {
_log("Message received " + type);
}
if (type === MessageIds.TERN_INIT_MSG) {
var env = request.env,
files = request.files;
inferenceTimeout = request.timeout;
initTernServer(env, files);
} else if (type === MessageIds.TERN_COMPLETIONS_MSG) {
offset = request.offset;
getTernHints(request.fileInfo, offset, request.isProperty);
} else if (type === MessageIds.TERN_GET_FILE_MSG) {
file = request.file;
text = request.text;
handleGetFile(file, text);
} else if (type === MessageIds.TERN_CALLED_FUNC_TYPE_MSG) {
offset = request.offset;
handleFunctionType(request.fileInfo, offset);
} else if (type === MessageIds.TERN_JUMPTODEF_MSG) {
offset = request.offset;
getJumptoDef(request.fileInfo, offset);
} else if (type === MessageIds.TERN_SCOPEDATA_MSG) {
offset = request.offset;
getScopeData(request.fileInfo, offset);
} else if (type === MessageIds.TERN_REFS) {
offset = request.offset;
getRefs(request.fileInfo, offset);
} else if (type === MessageIds.TERN_ADD_FILES_MSG) {
handleAddFiles(request.files);
} else if (type === MessageIds.TERN_PRIME_PUMP_MSG) {
isUntitledDoc = request.isUntitledDoc;
handlePrimePump(request.path);
} else if (type === MessageIds.TERN_GET_GUESSES_MSG) {
offset = request.offset;
getTernProperties(request.fileInfo, offset, MessageIds.TERN_GET_GUESSES_MSG);
} else if (type === MessageIds.TERN_UPDATE_FILE_MSG) {
handleUpdateFile(request.path, request.text);
} else if (type === MessageIds.SET_CONFIG) {
setConfig(request.config);
} else if (type === MessageIds.TERN_UPDATE_DIRTY_FILE) {
ExtractContent.updateFilesCache(request.name, request.action);
} else if (type === MessageIds.TERN_CLEAR_DIRTY_FILES_LIST) {
ExtractContent.clearFilesCache();
} else {
_log("Unknown message: " + JSON.stringify(request));
}
}
function invokeTernCommand(commandConfig) {
try {
_requestTernServer(commandConfig);
} catch (error) {
console.warn(error);
}
}
function setInterface(msgInterface) {
MessageIds = msgInterface.messageIds;
}
function checkInterfaceAndReInit() {
if (!MessageIds) {
// WTF - Worse than failure
// We are here as node process got restarted
// Request for ReInitialization of interface and Tern Server
self.postMessage({
type: "RE_INIT_TERN"
});
}
}