c9-core/plugins/c9.ide.language.javascript..../infer_completer.js

410 wiersze
17 KiB
JavaScript

define(function(require, exports, module) {
var baseLanguageHandler = require('plugins/c9.ide.language/base_handler');
var infer = require('./infer');
var path = require('./path');
var KIND_DEFAULT = require('plugins/c9.ide.language.javascript/scope_analyzer').KIND_DEFAULT;
var KIND_PACKAGE = require('plugins/c9.ide.language.javascript/scope_analyzer').KIND_PACKAGE;
var KIND_EVENT = require('plugins/c9.ide.language.javascript/scope_analyzer').KIND_EVENT;
var PROPER = require('plugins/c9.ide.language.javascript/scope_analyzer').PROPER;
var EXPAND_STRING = 1;
var EXPAND_REQUIRE = 2;
var EXPAND_REQUIRE_LIMIT = 5;
var REQUIRE_PROPOSALS_MAX = 80;
var REQUIRE_ID_REGEX = /(?!["'])./;
var FunctionValue = require('./values').FunctionValue;
var completeUtil = require("plugins/c9.ide.language/complete_util");
var traverse = require("treehugger/traverse");
var args = require("./infer_arguments");
var astUpdater = require("./ast_updater");
// Completion priority levels
// Should be used sparingly, since they disrupt the sorting order
var PRIORITY_INFER_LOW = 3;
var PRIORITY_INFER = 4;
var PRIORITY_INFER_TERN = 5;
var PRIORITY_INFER_HIGH = 6;
var completer = module.exports = Object.create(baseLanguageHandler);
var extraModuleCompletions;
completer.handlesLanguage = function(language) {
// Note that we don't really support jsx here,
// but rather tolerate it using error recovery...
return language === "javascript" || language === "jsx";
};
completer.getIdentifierRegex = function() {
// Allow slashes for package names
return (/[a-zA-Z_0-9\$\/]/);
};
completer.getCompletionRegex = function() {
return (/^[\.]$/);
};
completer.getCacheCompletionRegex = function() {
// Match strings that can be an expression or its prefix
return new RegExp(
// 'if/while/for ('
"(\\b(if|while|for|switch)\\s*\\("
// other identifiers and keywords without (
+ "|\\b\\w+\\s+"
// equality operators, operators such as + and -,
// and opening brackets { and [
+ "|(===?|!==?|[-+]=|[-+*%<>?!|&{[])"
// spaces
+ "|\\s)+"
);
};
completer.getMaxFileSizeSupported = function() {
// .25 of current base_handler default
return .25 * 10 * 1000 * 80;
};
completer.setExtraModules = function(extraModules) {
extraModuleCompletions = extraModules;
};
function valueToMatch(container, v, name, isPackage, isContextual) {
// Node.js and the default behavior of require.js is not adding the .js extension
if (isPackage)
name = name.replace(/\.js$/, "");
if ((v instanceof FunctionValue || v.properties._return) && !isPackage) {
var showArgs = args.extractArgumentNames(v, true);
var insertArgs = "opt" in showArgs ? args.extractArgumentNames(v, false) : showArgs;
return {
id: name,
guid: v.guid + "[0" + name + "]",
name: name + "(" + showArgs.argNames.join(", ") + ")",
replaceText: name + (insertArgs.argNames.length === 0 && v.guid && v.guid.indexOf("es5:") !== 0 ? "()" : "(^^)"),
icon: "method",
priority: PRIORITY_INFER,
inferredNames: showArgs.inferredNames,
doc: v.doc,
docUrl: v.docUrl,
isFunction: true,
type: v.properties._return && getGuid(v.properties._return.values[0]),
isContextual: isContextual
};
}
else {
var isHighConfidence =
container && container.properties && container.properties["_" + name]
&& container.properties["_" + name].confidence >= 1;
return {
id: name,
guid: container ? container.guid + "/" + name : v.guid + "[0" + name + "]",
name: name,
replaceText: name,
doc: v.doc,
docUrl: v.docUrl,
icon: "property",
priority: name === "__proto__" ? PRIORITY_INFER_LOW : PRIORITY_INFER,
type: !isPackage && getGuid(v.properties.___proto__ ? v.properties.___proto__.values[0] : v.guid),
isContextual: isHighConfidence
};
}
}
function getGuid(valueOrGuid) {
if (!valueOrGuid)
return;
var result = valueOrGuid.guid || valueOrGuid;
return result.substr && result.substr(-11) !== "/implReturn" ? result : undefined;
}
completer.predictNextCompletion = function(doc, fullAst, pos, options, callback) {
if (!options.matches.length) {
// Normally we wouldn't complete here, maybe we can complete for the next char?
// Let's do so unless it looks like the next char may be a newline or equals sign
if (options.line[pos.column - 1] && /(?![{;})\]\s"'\+\-\*])./.test(options.line[pos.column - 1]))
return callback(null, { predicted: "" });
}
var predicted = options.matches.filter(function(m) {
return m.priority >= PRIORITY_INFER;
});
if (predicted.length !== 1 || predicted[0].icon === "method")
return callback();
callback(null, {
predicted: predicted[0].replaceText + ".",
showEarly: predicted[0].icon === "property" && !/\./.test(options.line)
});
};
completer.complete = function(doc, fullAst, pos, options, callback) {
if (!options.node)
return callback();
var line = options.line;
var identifier = options.identifierPrefix;
var basePath = path.getBasePath(completer.path, completer.workspaceDir);
var filePath = path.canonicalizePath(completer.path, basePath);
if (fullAst.parent === undefined) {
traverse.addParentPointers(fullAst);
fullAst.parent = null;
}
astUpdater.updateOrReanalyze(doc, fullAst, filePath, basePath, pos, function(fullAst, currentNode) {
var completions = {};
var duplicates = {};
currentNode.rewrite(
'PropAccess(e, x)', function(b) {
var allIdentifiers = [];
var values = infer.inferValues(b.e);
values.forEach(function(v) {
var propNames = v.getPropertyNames();
for (var i = 0; i < propNames.length; i++) {
if (propNames[i] !== b.x.value || v.isProperDeclaration(propNames[i]))
allIdentifiers.push(propNames[i]);
}
});
var matches = completeUtil.findCompletions(identifier, allIdentifiers);
for (var i = 0; i < matches.length; i++) {
values.forEach(function(v) {
v.get(matches[i]).forEach(function(propVal) {
var match = valueToMatch(v, propVal, matches[i], false, true);
// Only override completion if argument names were _not_ inferred, or if no better match is known
var duplicate = duplicates["_" + match.id];
if (duplicate && duplicate.inferredNames)
delete completions["_" + duplicate.guid];
if (duplicate && match.inferredNames)
return;
duplicates["_" + match.id] = completions["_" + match.guid] = match;
});
});
}
return this;
},
// Don't complete definitions
'FArg(_)', 'Function(_,_,_)', 'VarDeclInit(_,_)', 'VarDecl(_,_)',
'ConstDeclInit(_,_)', 'ConstDecl(_,_)', function() { return this; },
'_', function() {
var me = this;
if (this.traverseUp(
"Call(Var(\"require\"), args)",
function(b) {
if (b.args[0] !== me && this !== me)
return;
var scope = this[0].getAnnotation("scope");
var expand = b.args[0] && b.args[0].cons === "String" ? null : EXPAND_STRING;
identifier = completeUtil.retrievePrecedingIdentifier(line, pos.column, REQUIRE_ID_REGEX);
var useBasePath = path.isRelativePath(identifier) || path.isAbsolutePath(identifier) ? basePath : null;
completer.proposeRequire(identifier, expand, scope, completions, useBasePath);
}))
return this;
},
'ERROR()', 'PropertyInit(x,e)', 'ObjectInit(ps)', function(b, node) {
if (b.ps) {
completer.proposeObjectProperty(node, identifier, completions);
}
else if (!b.x) {
if (currentNode.parent.cons !== "PropertyInit")
return; // Fallthrough
currentNode = currentNode.parent;
b.x = currentNode[0];
b.e = currentNode[1];
}
// get parent parent like in ObjectInit([PropertyInit("b",ERROR())])
var objectInit = currentNode.parent.parent;
if (!objectInit.parent || !objectInit.parent.parent || objectInit.parent.parent.cons !== "Call")
return node;
completer.proposeObjectProperty(objectInit, identifier, completions);
return node;
},
'Call(_, _)', function(b) {
if ("function".indexOf(identifier) === 0)
completer.proposeClosure(this, doc, pos, completions);
// Fallthrough to next rule
},
'Var(_)', function(b) {
if (this.parent.parent && this.parent.parent.isMatch('Call(_, _)') && "function".indexOf(identifier) === 0)
completer.proposeClosure(this.parent.parent, doc, pos, completions);
// Fallthrough to next rule
},
'Var(_)', function(b) {
this.parent.rewrite('VarDeclInit(x, _)', 'ConstDeclInit(x, _)', function(b) {
if ("require".indexOf(identifier) !== 0)
return;
var scope = this.getAnnotation("scope");
// Propose relative and non-relative paths
completer.proposeRequire(b.x.value, EXPAND_REQUIRE, scope, completions);
completer.proposeRequire(b.x.value, EXPAND_REQUIRE, scope, completions, basePath);
});
// Fallthrough to next rule
},
// Else, let's assume it's a variable
function() {
var scope;
this.traverseUp(function() {
if (!scope) scope = this.getAnnotation("scope");
if (this.rewrite("String(_)")) return this;
});
if (!scope)
return;
var variableNames = scope.getVariableNames();
if (this.cons === 'Var') { // Delete current var from proposals if not properly declared anywhere
var varName = this[0].value;
if (variableNames.indexOf(varName) !== -1 && (!scope.get(varName) || !scope.get(varName).isProperDeclaration()))
variableNames.splice(variableNames.indexOf(varName), 1);
}
var matches = completeUtil.findCompletions(identifier, variableNames);
for (var i = 0; i < matches.length; i++) {
var v = scope.get(matches[i]);
if (!v)
continue;
if (!v.values.length && v.properDeclarationConfidence >= PROPER && currentNode.cons === "Var") {
completions[matches[i]] = {
id: matches[i],
name: matches[i],
replaceText: matches[i],
icon: "property",
priority: PRIORITY_INFER_TERN
};
}
v.values.forEach(function(propVal) {
var match = valueToMatch(null, propVal, matches[i]);
if (!match.name)
return;
// Only override completion if argument names were _not_ inferred, or if no better match is known
var duplicate = duplicates["_" + match.id];
if (duplicate && duplicate.inferredNames)
delete completions["_" + duplicate.guid];
if (duplicate && match.inferredNames)
return;
duplicates["_" + match.id] = completions["_" + match.guid] = match;
});
}
}
);
// Find completions equal to the current prefix
var completionsArray = [];
for (var id in completions) {
completionsArray.push(completions[id]);
}
callback(completionsArray);
});
};
/**
* @param basePath If specified, the base path to use for relative paths.
* Enables listing relative paths.
*/
completer.proposeRequire = function(identifier, expand, scope, completions, basePath) {
var names = scope.getNamesByKind(KIND_PACKAGE);
if (basePath || basePath === "")
identifier = path.canonicalizePath(identifier, basePath).replace(/^\.$/, "");
if (expand === EXPAND_REQUIRE && extraModuleCompletions)
names = names.concat(Object.keys(extraModuleCompletions));
var matches = expand === EXPAND_REQUIRE
? filterRequireSubstring(identifier, names)
: completeUtil.findCompletions(identifier === "/" ? "" : identifier, names);
if (basePath || basePath === "")
matches = matches.filter(function(v) { return v.match(/\.js$/) && !v.match(/(\/|^)node_modules\//); });
else
matches = matches.filter(function(v) { return !v.match(/\.js$/); });
if (expand === EXPAND_REQUIRE && matches.length > EXPAND_REQUIRE_LIMIT)
return;
matches = matches.slice(0, REQUIRE_PROPOSALS_MAX);
for (var i = 0; i < matches.length; i++) {
var match = matches[i];
var v = scope.get(match, KIND_PACKAGE);
if (!v && expand === EXPAND_REQUIRE) {
return completions["_" + match] = {
id: match,
icon: "package",
name: 'require("' + match + '")',
replaceText: 'require("' + match + '")',
doc: "Origin: node<br/>"
+ (extraModuleCompletions[match].doc || ""),
priority: PRIORITY_INFER_HIGH
};
}
v.values.forEach(function(propVal) {
var match = valueToMatch(null, propVal, matches[i], true, expand);
match.icon = "package";
if (identifier.match(/^\//))
match.replaceText = match.name = "/" + match.replaceText;
else if (basePath || basePath === "")
match.replaceText = match.name = path.uncanonicalizePath(match.replaceText, basePath);
completions["_" + match.guid] = match;
if (expand === EXPAND_REQUIRE) {
match.replaceText = 'require("' + match.replaceText + '")';
match.name = 'require("' + match.name + '")';
}
if (expand === EXPAND_STRING)
match.replaceText = '"' + match.replaceText + '"';
if (expand !== EXPAND_REQUIRE)
match.identifierRegex = REQUIRE_ID_REGEX;
});
}
};
completer.proposeClosure = function(node, doc, pos, completions) {
node.rewrite('Call(f, args)', function(b) {
var argIndex = args.getArgIndex(this, doc, pos);
var id = 0;
infer.inferValues(b.f).forEach(function(v) {
var argNames = args.extractArgumentNames(v, false);
var code = argNames.argValueCodes[argIndex];
if (!code)
return;
var codeName = code.split(/\n/)[0] + "}";
var guid = v.guid + "-argfun" + (id++);
completions[guid] = {
id: codeName,
guid: guid,
name: codeName,
replaceText: code,
doc: v.fargs && v.fargs.doc,
docUrl: v.fargs && v.fargs.docUrl,
icon: "method",
priority: PRIORITY_INFER_HIGH
};
});
});
};
/**
* Complete properties for an Object init in e.g.
* Call(PropAccess(Var("http"),"example"),[ObjectInit([PropertyInit("b",ERROR())])])
*/
completer.proposeObjectProperty = function(objectInit, identifier, completions) {
var listIndex;
for (var i = 0; i < objectInit.parent.length; i++)
if (objectInit.parent[i] === objectInit) listIndex = i;
var call = objectInit.parent.parent;
infer.inferValues(call[0]).forEach(function(v) {
if (!v.fargs || !v.fargs[listIndex] || !v.fargs[listIndex].properties)
return;
v.fargs[listIndex].properties.forEach(function(property) {
completions["_$p$" + property.id] = {
id: property.id,
name: property.id,
replaceText: property.id,
doc: property.doc,
docUrl: property.docUrl,
icon: "property",
priority: PRIORITY_INFER
};
});
});
};
function filterRequireSubstring(name, names) {
var nameClean = name.replace(/[^A-Za-z0-9_-]/g, ".");
var nameRegex = new RegExp("^" + nameClean + "\\b|\\b" + nameClean + "$");
return names.filter(function(n) {
return nameRegex.test(n);
});
}
});