c9-core/plugins/c9.ide.language.javascript..../worker/tern_worker.js

882 wiersze
28 KiB
JavaScript

define(function(require, exports, module) {
var acornHelper = require("./acorn_helper");
var tern = require("tern/lib/tern");
var baseLanguageHandler = require('plugins/c9.ide.language/base_handler');
var handler = module.exports = Object.create(baseLanguageHandler);
var tree = require("treehugger/tree");
var util = require("plugins/c9.ide.language/worker_util");
var completeUtil = require("plugins/c9.ide.language/complete_util");
var filterDocumentation = require("plugins/c9.ide.language.jsonalyzer/worker/ctags/ctags_util").filterDocumentation;
var getParameterDocs = require("plugins/c9.ide.language.jsonalyzer/worker/ctags/ctags_util").getParameterDocs;
var architectResolver = null;
var inferCompleter = require("plugins/c9.ide.language.javascript.infer/infer_completer");
var TERN_DEFS = [];
// TODO: only include meteor completions if project has a .meteor folder,
// or if we find 1 or more meteor globals anywhere
// TODO: maybe enable this meteor plugin again?
// meteor: require("./lib/tern-meteor/meteor") && true,
// TODO: use https://github.com/borisyankov/DefinitelyTyped
// Listing these plugins here makes sure they're part of the build process
var BUILTIN_PLUGINS = {
// angular: require("tern/plugin/angular"), // crashes the browser see https://github.com/ternjs/tern/issues/725
// commonjs: require("tern/plugin/commonjs"), // doesn't work in client
// complete_strings: require("tern/plugin/complete_strings"), // not useful to us
doc_comment: require("tern/plugin/doc_comment"),
es_modules: require("tern/plugin/es_modules"),
modules: require("tern/plugin/modules"),
node: require("tern/plugin/node"),
node_resolve: require("tern/plugin/node_resolve"),
requirejs: require("tern/plugin/requirejs"),
// webpack: require("tern/plugin/webpack"), // doesn't work in client
architect_resolver: architectResolver,
};
var ternWorker;
var ternServerOptions = {};
var ternRequestOptions = {};
var fileCache = {};
var dirCache = {};
var firstClassDefs = [];
var lastAddPath;
var lastAddValue;
var lastCacheRead = 0;
var MAX_CACHE_AGE = 60 * 1000 * 10;
var MAX_FILE_SIZE = 200 * 1024;
var PRIORITY_DEFAULT = 5;
var PRIORITY_LIBRARY_GLOBAL = 0;
/* function to perfom mixin
*/
function mix() {
var arg, prop, child = {};
for (arg = 0; arg < arguments.length; arg += 1) {
if (!arguments[arg]) {
continue;
}
for (prop in arguments[arg]) {
if (arguments[arg].hasOwnProperty(prop)) {
child[prop] = arguments[arg][prop];
}
}
}
return child;
}
handler.handlesLanguage = function(language) {
// Note that we don't really support jsx here,
// but rather tolerate it using error recovery...
return language === "javascript" || language === "jsx";
};
handler.getCompletionRegex = function() {
return (/^[\.]$/);
};
handler.getMaxFileSizeSupported = function() {
// .25 of current base_handler default
return .25 * 10 * 1000 * 80;
};
handler.$recacheCompletionLength = 3;
handler.init = function(callback) {
initTern();
inferCompleter.setExtraModules(ternWorker.cx.definitions.node);
ternWorker.on("beforeLoad", function(e) {
var file = e.name;
var dir = dirname(e.name);
if (dir[0] != "/")
return;
if (!dirCache[dir])
util.$watchDir(dir, handler);
fileCache[file] = fileCache[file] || {};
dirCache[dir] = dirCache[dir] || {};
dirCache[dir].used = Date.now();
dirCache[dir][file] = true;
lastCacheRead = Date.now();
});
handler.sender.on("tern_set_def_enabled", function(e) {
setDefEnabled(e.data.name, e.data.def, e.data.enabled, e.data.options);
});
handler.sender.on("tern_set_server_options", function(e) {
setOptions(e.data);
});
handler.sender.on("tern_set_request_options", function(e) {
if (e.data) {
ternRequestOptions = e.data;
}
});
handler.sender.on("tern_get_plugins", function(e) {
var pluginName;
var plugins = [];
var pluginToList;
for (pluginName in ternWorker.options.plugins) {
pluginToList = {
name: pluginName,
enabled: ternWorker.options.plugins[pluginName]
};
plugins.push(pluginToList);
}
handler.sender.emit("tern_read_plugins", plugins);
});
handler.sender.on("tern_update_plugins", function(e) {
updatePlugins(e.data);
});
util.$onWatchDirChange(onWatchDirChange);
setInterval(garbageCollect, 60000);
callback();
};
function initTern() {
ternWorker = new tern.Server({
async: ternServerOptions.async !== undefined ? ternServerOptions.async : true,
defs: ternServerOptions.defs !== undefined ? ternServerOptions.defs : TERN_DEFS,
plugins: ternServerOptions.plugins !== undefined ? ternServerOptions.plugins : {},
dependencyBudget: ternServerOptions.dependencyBudget !== undefined ? ternServerOptions.dependencyBudget : MAX_FILE_SIZE,
reuseInstances: ternServerOptions.reuseInstances !== undefined ? ternServerOptions.reuseInstances : true,
normalizeFilename: function(file) {
if (file[0] != "/")
file = "/" + file;
if (!file.match(/[\/\\][^/\\]*\.[^/\\]*$/))
file += ".js";
return file;
},
getFile: ternServerOptions.getFile !== undefined ? ternServerOptions.getFile : function(file, callback) {
// TODO we can use file cache in navigate to find a folder for unresolved modules
if (file == handler.path)
return done(null, handler.doc.getValue());
util.stat(file, function(err, stat) {
if (stat && stat.size > MAX_FILE_SIZE) {
err = new Error("File is too large to include");
err.code = "ESIZE";
}
if (err)
return done(err);
fileCache[file] = fileCache[file] || {};
fileCache[file].mtime = stat.mtime;
util.readFile(file, { allowUnsaved: true }, function(err, data) {
if (err) return done(err);
lastAddPath = null; // invalidate cache
done(null, data);
});
});
function done(err, result) {
try {
callback(err, result);
}
catch (err) {
console.error(err.stack);
}
}
}
});
}
var setOptions = module.exports.setOptions = function(options) {
for (var o in options) {
ternWorker.options[o] = ternServerOptions[o] = options[o];
}
};
/**
* Example:
*
* ```
* updatePlugins({
* lightning: {
* name: "lightning",
* enabled: true
* }
* });
* ```
*/
var updatePlugins = module.exports.updatePlugins = function(plugins) {
var requiresReset = false;
for (var p in plugins) {
var targetPlugin = plugins[p];
var plugin = ternWorker.options.plugins[targetPlugin.name];
if (targetPlugin.name == "angular")
continue;
if (targetPlugin.firstClass)
firstClassDefs.push(targetPlugin.name);
if (typeof plugin === "undefined" && typeof targetPlugin.path === "string") {
// Register new plugin
var loaded = require(targetPlugin.path);
if (!loaded) {
console.error("Could not load", targetPlugin.path);
continue;
}
ternServerOptions.plugins = ternServerOptions.plugins || {};
ternServerOptions.plugins[targetPlugin.name] = targetPlugin.enabled;
if (targetPlugin.name === "architect_resolver")
architectResolver = loaded;
requiresReset = true;
}
else {
if (plugin !== targetPlugin.enabled) {
ternServerOptions.plugins = ternServerOptions.plugins || {};
ternServerOptions.plugins[targetPlugin.name] = targetPlugin.enabled;
requiresReset = true;
}
}
}
if (requiresReset)
initTern();
// Delete identifier also declared by "browser"
ternWorker.defs.forEach(function(d) {
if (d["!name"] === "node")
delete d.console;
});
};
function onWatchDirChange(e) {
var dir = e.data.path.replace(/\/?$/, "/");
e.data.files.forEach(function(stat) {
var file = dir + stat.name;
if (!fileCache[file] || fileCache[file].mtime >= stat.mtime)
return;
ternWorker.delFile(file);
delete fileCache[file];
lastAddPath = null; // invalidate local file cache
});
}
function garbageCollect() {
var minAge = lastCacheRead - MAX_CACHE_AGE;
for (var file in fileCache) {
if (fileCache[file].used < minAge) {
ternWorker.delFile(file);
delete fileCache[file];
if (lastAddPath === file)
lastAddPath = null;
}
}
for (var dir in dirCache) {
if (dirCache[dir].used < minAge) {
handler.sender.emit("unwatchDir", { path: dir });
delete dirCache[file];
}
}
}
handler.onDocumentOpen = function(path, doc, oldPath, callback) {
setJSXMode(path);
callback();
};
handler.analyze = function(value, ast, options, callback) {
if (fileCache[this.path])
return callback();
// Pre-analyze the first time we see a file, loading any imports
fileCache[this.path] = {
mtime: 0, // prefer reloading since we may be unsaved
used: Date.now()
};
addTernFile(this.path, value);
if (!architectResolver)
return callback();
architectResolver.onceReady(function() {
handler.$flush(function(err) {
if (err) console.error(err.stack || err);
callback();
});
});
};
handler.complete = function(doc, fullAst, pos, options, callback) {
// Don't show completions for definitions
var node = options.node;
if (!node ||
["FArg", "Function", "Arrow", "VarDecl", "VarDeclInit", "ConstDecl", "ConstDeclInit",
"LetDecl", "LetDeclInit", "PropertyInit", "Label", "String"].indexOf(node.cons) > -1)
return callback();
addTernFile(this.path, doc.getValue());
var line = doc.getLine(pos.row);
var prefix = util.getPrecedingIdentifier(line, pos.column);
var defaultOptions = {
type: "completions",
pos: pos,
types: true,
origins: true,
docs: true,
urls: true,
guess: true,
caseInsensitive: false,
};
var ternOptions = mix(defaultOptions, ternRequestOptions[defaultOptions.type]);
handler.$request(ternOptions, function(err, result) {
if (err) {
console.error(err.stack || err);
return callback();
}
callback(result.completions.map(function(match) {
// Avoid random suggestions like angular.js properties on any object
if (match.guess && match.type && match.type !== "fn()?)")
return;
if (match.type === "?")
delete match.type;
var isContextual = node.cons === "PropAccess" && !match.guess;
if (!isContextual && match.origin === "browser" && prefix.length < 3)
return; // skip completions like onchange (from window.onchange)
var isFromLibrary = match.origin && match.origin[0] !== "/" && firstClassDefs.indexOf(match.origin) === -1;
var priority = PRIORITY_DEFAULT;
var icon = getIcon(match, priority);
// Clean up messy node completions
if (match.name[0] === '"') {
if (match.origin !== "node")
return;
match.name = match.name.replace(/"(.*)"/, "$1");
icon = "package";
}
var isFunction = match.type && match.type.match(/^fn\(/);
var isAnonymous = match.type && match.type.match(/^{/);
var fullName;
var fullNameTyped;
if (isFunction) {
var sig = getSignature(match);
var parameters = sig.parameters;
fullName = match.name + "(" + parameters.map(function(p) {
return p.name;
}).join(", ") + ")";
fullNameTyped = match.name + "(" + parameters.map(function(p) {
return p.name + (p.type ? " : " + p.type : "");
}).join(", ") + ")";
if (sig.returnType)
fullNameTyped = fullNameTyped + " : " + sig.returnType;
}
else {
fullName = fullNameTyped = match.name;
if (match.type)
fullNameTyped = fullNameTyped + " : " + match.type;
}
var doc = (match.type && !isFunction && !isAnonymous ? "Type: " + match.type + "<p>" : "")
+ (match.doc ? filterDocumentation(match.doc) : "");
if (match.doc === "Every function in JavaScript is actually a Function object.")
doc = "";
return {
id: match.name,
name: fullName,
replaceText: match.name + (isFunction ? "(^^)" : ""),
icon: icon,
priority: priority,
isContextual: isContextual,
docHead: fullNameTyped,
doc: (match.origin && isFromLibrary ? "Origin: " + match.origin + "<p>" : "") + doc,
docUrl: match.url,
isFunction: isFunction,
url: match.url
};
}).filter(function(c) {
return c;
}));
});
};
handler.jumpToDefinition = function(doc, fullAst, pos, options, callback) {
addTernFile(this.path, doc.getValue());
var defaultOptions = {
type: "definition",
pos: pos,
types: true,
origins: true,
docs: true,
urls: true,
caseInsensitive: false,
};
var ternOptions = mix(defaultOptions, ternRequestOptions[defaultOptions.type]);
this.$request(ternOptions, function(err, result) {
if (err) {
console.error(err.stack || err);
return callback();
}
if (!result.file)
return callback();
if (!result.file.match(/[\/\\][^/\\]*\.[^/\\]*$/))
result.file += ".js";
callback({
path: result.file,
row: result.start.line,
column: result.start.ch,
icon: getIcon(result, PRIORITY_DEFAULT)
});
});
};
/* UNDONE: getRenamePositions(); doesn't appear to properly handle local references
e.g. var foo = child_process.exec(); foo(); -> foo can't be renamed
handler.getRenamePositions = function(doc, fullAst, pos, options, callback) {
var defaultOptions = addTernFile(this.path, doc.getValue());
{
type: "definition",
pos: pos,
types: true,
origins: true,
docs: true,
urls: true,
caseInsensitive: false,
};
var ternOptions = mix(defaultOptions, ternRequestOptions[defaultOptions.type]);
this.$request(ternOptions, function(err, def) {
if (err) {
console.error(err.stack || err);
return callback();
}
if (handler.path !== def.file) {
console.error("Multi-file rename not supported");
return callback();
}
var defaultOptions = {
type: "refs",
pos: pos,
types: true,
origins: true,
docs: true,
urls: true,
caseInsensitive: false,
};
var options = mix(defaultOptions, ternRequestOptions[defaultOptions.type]);
handler.$request(options, function(err, refs) {
if (err) {
console.error(err.stack || err);
return callback();
}
var allIds = [def].concat(refs.refs);
var selected = allIds.filter(function(id) {
return pos.row === id.start.line
&& id.start.ch <= pos.column && pos.column < id.end.ch;
});
if (!selected.length) {
console.error("Could not find selected identifier");
return callback();
}
callback({
length: def.end.ch - def.start.ch,
pos: { row: selected[0].start.line, column: selected[0].start.ch },
others: allIds.filter(function(ref) {
return ref.file === handler.path;
}).map(function(ref) {
return { row: ref.start.line, column: ref.start.ch };
}),
});
});
});
};
*/
handler.tooltip = function(doc, fullAst, cursorPos, options, callback) {
var node = options.node;
if (!node)
return callback();
var argIndex = -1;
var callNode = getCallNode(node, cursorPos);
var displayPos;
if (callNode) {
var argPos = { row: callNode[1].getPos().sl, column: callNode[1].getPos().sc };
if (argPos.row >= 9999999999)
argPos = cursorPos;
var endLine = callNode.getPos().el;
if (callNode[1].length && callNode[1].getPos().el !== callNode.getPos().el)
endLine--; // put tooltip near end of arguments, not end of call
displayPos = { row: endLine, column: callNode[1].getPos().sc };
argIndex = this.getArgIndex(callNode, doc, cursorPos);
}
else if (node.isMatch('Var(_)')) {
displayPos = { row: node.getPos().sl, column: node.getPos().sc };
argIndex = -1;
// Don't display tooltip at end of identifier (may just have been typed in)
if (cursorPos.column === node.getPos().ec)
return callback();
}
else {
return callback();
}
if (argIndex === -1 && callNode)
return callback();
if (!callNode)
return callback(); // TODO: support this case??
addTernFile(this.path, doc.getValue());
var defaultOptions = {
type: "type",
pos: { row: callNode[0].getPos().el, column: callNode[0].getPos().ec },
types: true,
origins: true,
docs: true,
urls: true,
caseInsensitive: false,
preferFunction: true,
};
var ternOptions = mix(defaultOptions, ternRequestOptions[defaultOptions.type]);
this.$request(ternOptions, function(err, result) {
if (err) {
console.error(err.stack || err);
return callback();
}
if (!result.type || !result.name || !result.type.match(/^fn\(/))
return callback();
var rangeNode = callNode && callNode.getPos().sc < 99999 ? callNode : node;
var sig = getSignature(result);
if (sig.parameters[argIndex])
sig.parameters[argIndex].active = true;
var parameterDocs = getParameterDocs(result.doc);
sig.parameters.forEach(function(p) {
if (p.type === "?")
delete p.type;
if (parameterDocs["_" + p.name])
p.docHtml = parameterDocs["_" + p.name];
});
if (sig.returnType === "?")
delete sig.returnType;
if (sig.returnType === "[]")
sig.returnType = "Array";
callback({
hint: {
signatures: [{
name: result.name.replace(/.*\./, ""),
docHtml: result.doc && result.doc.replace(/^\* /g, ""),
parameters: sig.parameters,
returnType: sig.returnType
}],
},
displayPos: displayPos,
pos: rangeNode.getPos()
});
});
};
/**
* Gets the index of the selected function argument, or returns -1 if N/A.
*/
handler.getArgIndex = function(node, doc, cursorPos) {
var cursorTreePos = { line: cursorPos.row, col: cursorPos.column };
var result = -1;
node.rewrite(
'Call(e, args)', "New(e, args)", function(b) {
// Try to determine at which argument the cursor is located in order
// to be able to show a label
result = -1;
var line = doc.getLine(cursorPos.row);
if (line[b.args.getPos().ec + 1] && line[b.args.getPos().ec + 1].match(/[ ,]/))
b.args.getPos().ec++;
if (b.args.length === 0 && this.getPos().ec - 1 === cursorPos.column) {
result = 0;
}
else if (b.args.length === 0 && line.substr(cursorPos.column).match(/^\s*\)/)) {
result = 0;
}
else if (!tree.inRange(this.getPos(), cursorTreePos, true)) {
return this;
}
else if (cursorPos.row === this.getPos().sl && line.substr(0, cursorPos.column + 1).match(/,\s*\)$/)) {
result = b.args.length;
return this;
}
for (var i = 0; i < b.args.length; i++) {
if (b.args[i].cons === "ERROR" && result === -1) {
result = i;
break;
}
b.args[i].traverseTopDown(function() {
var pos = this.getPos();
if (this === node) {
result = i;
return this;
}
else if (pos && pos.sl <= cursorPos.row && pos.sc <= cursorPos.column) {
if (pos.sl === cursorPos.row && pos.ec === cursorPos.column - 1 && line[pos.ec] === ")")
return result = -1;
result = i;
}
});
}
return this;
}
);
return result;
};
function getCallNode(currentNode, cursorPos) {
var result;
var previous;
currentNode.traverseUp(
'Call(e, args)', 'New(e, args)', function(b, node) {
if (b.e === previous)
return;
result = node;
return node;
},
'Function(x, args, body)', 'Arrow(args, body)', function(b, node) {
// Bail for anything inside a function. The function itself is ok.
if (node !== currentNode)
return node;
previous = node;
},
'PropertyInit(x, _)', 'Method(x, body)', function(b, node) {
// Bail
return node;
},
function(node) {
previous = node;
}
);
return result;
}
function getIcon(property, priority) {
if (property.guess || !property.type || property.type === "fn()?") {
// These were found in calls or property accesses and are uncertain
return property.type ? "method2" : "property2";
}
else if (property.type.match(/^fn\(/)) {
return priority ? "method" : "method2";
}
else {
return priority ? "property" : "property2";
}
}
function addTernFile(path, value) {
if (lastAddPath === path && lastAddValue === value)
return;
lastAddPath = path;
lastAddValue = value;
setJSXMode(path);
ternWorker.addFile(path, value);
}
function dirname(path) {
return path.replace(/[\/\\][^\/\\]*$/, "");
}
/**
* Parse tern type strings.
* (Would have been useful if tern exposed type objects, but this works.)
*/
function getSignature(property) {
if (!property.type || !property.type.match(/^fn\(/))
return { parameters: []};
var sig = property.type;
var parameters = [{ name: "", type: "" }];
var parameterIndex = 0;
var returnType = "";
var depth = 0;
var inType = false;
var inReturn = false;
for (var i = "fn(".length; i < sig.length; i++) {
switch (sig[i]) {
case "(": case "{":
depth++; break;
case ")": case "}":
depth--; break;
case ":":
inType = true; break;
case ",":
if (depth)
break;
inType = false;
parameters.push({ name: "", type: "" });
parameterIndex++;
break;
case " ":
break;
case "-": // ->
if (depth >= 0)
break;
i++;
depth++;
inType = false;
inReturn = true;
break;
case "?":
if (!depth && inType && parameters[parameterIndex].type)
parameters[parameterIndex].name = "[" + parameters[parameterIndex].name + "]";
break;
default:
if (sig[i] === "]")
depth--;
if (!depth && inReturn)
returnType += sig[i];
else if (!depth && !inType)
parameters[parameterIndex].name += sig[i];
else if (!depth && inType)
parameters[parameterIndex].type += sig[i];
if (sig[i] === "[")
depth++;
}
}
parameters.forEach(function(p) {
if (p.type === "?")
delete p.type;
if (p.type === "[]")
p.type = "Array";
if (p.type)
p.type = p.type.replace(/\.prototype$/, "").replace(/.*\./, "");
});
if (parameters[0].name === "")
parameters.shift();
return {
parameters: parameters,
returnType: returnType && returnType.replace(/\.prototype$/, "").replace(/.*\./, "")
};
}
handler.$request = function(query, callback) {
query.file = this.path;
setJSXMode(this.path);
if (query.pos)
query.end = query.start = {
line: query.pos.row || query.pos.sl || 0,
ch: query.pos.column || query.pos.sc || 0
};
query.lineCharPositions = true;
try {
ternWorker.request(
{
query: query,
},
done
);
}
catch (err) {
if (isDone) throw err;
return done(err);
}
var isDone;
function done(err, result) {
isDone = true;
callback(err, result);
}
};
handler.$flush = function(callback) {
try {
ternWorker.flush(done);
}
catch (err) {
if (isDone) throw err;
return done(err);
}
var isDone;
function done(err, result) {
isDone = true;
callback(err, result);
}
};
/**
* @param {String} name
* @param {Object} def
* @param {Boolean} enabled
* @param {Object} [options]
* @param {Boolean} [options.firstClass]
* Treat as if these were built-in types,
* showing nicer icons and hiding the library name.
*/
function setDefEnabled(name, def, enabled, options) {
if (options && options.firstClass)
firstClassDefs.push(name);
var i;
if (!enabled) {
ternWorker.defs = ternWorker.defs.filter(function(d) {
return d["!name"] !== name;
});
ternWorker.reset();
ternServerOptions.defs = ternWorker.defs;
return;
}
var defs = def instanceof Array ? def : [def];
var downloaded = 0;
defs.forEach(function(def) {
if (typeof def !== "string") {
ternWorker.defs.push(def);
return checkDone();
}
completeUtil.fetchText(def, function(err, result) {
if (err) console.error(err);
try {
result = JSON.parse(result);
}
catch (err) {
console.error(err);
result = null;
}
ternWorker.defs.push(result);
checkDone();
});
});
function checkDone() {
if (++downloaded < defs.length)
return;
ternServerOptions.defs = ternWorker.defs;
ternWorker.reset();
}
}
function setJSXMode(path) {
acornHelper.setLanguage(/\.jsx$/.test(path) ? "jsx" : null);
}
});