kopia lustrzana https://github.com/c9/core
369 wiersze
13 KiB
JavaScript
369 wiersze
13 KiB
JavaScript
(function(mod) {
|
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
return mod(require("../lib/infer"), require("../lib/tern"), require("../lib/signal"), require)
|
|
if (typeof define == "function" && define.amd) // AMD
|
|
return define(["../lib/infer", "../lib/tern", "../lib/signal"], mod)
|
|
mod(tern, tern, tern.signal)
|
|
})(function(infer, tern, signal, require) {
|
|
"use strict"
|
|
|
|
function Modules(server, options) {
|
|
this.server = server
|
|
this.options = options || {}
|
|
this.modules = Object.create(null)
|
|
this.nonRelative = Object.create(null)
|
|
this.knownModules = Object.create(null)
|
|
this.resolvers = []
|
|
this.modNameTests = []
|
|
this.importTests = []
|
|
}
|
|
|
|
Modules.prototype = signal.mixin({
|
|
buildWrappingScope: function(parent, origin, node) {
|
|
var scope = new infer.Scope(parent, node)
|
|
scope.origin = origin
|
|
this.signal("wrapScope", scope)
|
|
return scope
|
|
},
|
|
|
|
maybeOverride: function(name) {
|
|
if (!this.options.modules || !this.options.modules.hasOwnProperty(name))
|
|
return false
|
|
if (this.modules[name]) return this.modules[name]
|
|
|
|
var override = this.options.modules[name]
|
|
if (typeof(override) == "string" && override.charAt(0) == "=")
|
|
return infer.def.parsePath(override.slice(1))
|
|
|
|
var scope = this.buildWrappingScope(infer.cx().topScope, name)
|
|
infer.def.load(override, scope)
|
|
return this.modules[name] = scope.exports
|
|
},
|
|
|
|
resolveModule: function(name, parentFile) {
|
|
var over = this.maybeOverride(name)
|
|
if (over) return over
|
|
var known = this.knownModules[name]
|
|
if (known) return known
|
|
if (this.options.dontLoad == true ||
|
|
this.options.dontLoad && new RegExp(this.options.dontLoad).test(name) ||
|
|
this.options.load && !new RegExp(this.options.load).test(name))
|
|
return infer.ANull
|
|
|
|
var resolved, relative = isRelative(name)
|
|
for (var i = 0; !resolved && i < this.resolvers.length; i++)
|
|
resolved = this.resolvers[i](name, parentFile)
|
|
if (!resolved) resolved = defaultResolver(name, parentFile)
|
|
if (!resolved) return infer.ANull
|
|
if (typeof resolved != "string") {
|
|
if (!relative) this.nonRelative[name] = true
|
|
return resolved
|
|
}
|
|
|
|
var known = this.modules[resolved]
|
|
if (known) return known
|
|
|
|
if (/\.js$|(?:^\/)[^\.]+$/.test(resolved))
|
|
this.server.addFile(resolved, null, parentFile)
|
|
if (!relative) this.nonRelative[name] = resolved
|
|
return this.modules[resolved] = new infer.AVal
|
|
},
|
|
|
|
findIn: function(array, node) {
|
|
for (var i = 0; i < array.length; i++) {
|
|
var name = array[i](node)
|
|
if (name != null) return name
|
|
}
|
|
},
|
|
|
|
isModName: function(node) { return this.findIn(this.modNameTests, node) },
|
|
isImport: function(node) { return this.findIn(this.importTests, node) },
|
|
|
|
get: function(name) {
|
|
return this.modules[name] || (this.modules[name] = new infer.AVal)
|
|
},
|
|
|
|
completeModuleName: function(completions, query, word) {
|
|
function fromObj(obj, useVal) {
|
|
for (var name in obj)
|
|
if (filter(word, name, query))
|
|
tern.addCompletion(query, completions, name, useVal && obj[name])
|
|
}
|
|
|
|
fromObj(this.knownModules, true)
|
|
if (this.options.modules) fromObj(this.options.modules, false)
|
|
|
|
var pathsSeen = Object.create(null)
|
|
for (var prop in this.nonRelative) {
|
|
var val = this.nonRelative[prop]
|
|
if (val == true || prop.indexOf("/") == -1) {
|
|
if (filter(word, prop, query)) tern.addCompletion(query, completions, prop)
|
|
} else if (prop.indexOf(word) == 0 && word.indexOf("/") > -1) {
|
|
var afterSlash = /.*?\/(.*)/.exec(prop)[1]
|
|
var found = val.indexOf(afterSlash)
|
|
if (found > -1) {
|
|
var dir = val.slice(0, found) + (/.*?\/(.*\/)?/.exec(word)[1] || "")
|
|
if (dir in pathsSeen) continue
|
|
pathsSeen[dir] = true
|
|
this.completeFileName(completions, query, null, word, dir)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
completeFileName: function(completions, query, parentFile, word, _dir) {
|
|
var path = parentFile ? resolvePath(dirName(parentFile), word) : baseName(word)
|
|
for (var prop in this.modules) {
|
|
if (prop != parentFile && filter(path, prop, query)) {
|
|
if (/\.js$/.test(prop)) prop = prop.slice(0, prop.length - 3)
|
|
var added = prop.slice(path.length)
|
|
tern.addCompletion(query, completions, word + added, this.modules[prop])
|
|
}
|
|
}
|
|
},
|
|
|
|
getModType: function(node) {
|
|
var modName = this.isModName(node), imp, prop
|
|
if (modName == null && (imp = this.isImport(node))) {
|
|
modName = imp.name
|
|
prop = imp.prop
|
|
}
|
|
if (modName == null) return
|
|
|
|
var modType = this.resolveModule(modName, node.sourceFile.name)
|
|
return (prop ? modType.getProp(prop) : modType).getType()
|
|
}
|
|
})
|
|
|
|
function resolvePath(parent, sub) {
|
|
if (/^https?:|^\//.test(sub)) return sub
|
|
if (parent && !/\/$/.test(parent)) parent = parent + "/"
|
|
var m
|
|
while (m = /^\.(\.)?\//.exec(sub)) {
|
|
if (m[1] && parent.length > 1) {
|
|
var lastSlash = parent.lastIndexOf("/", parent.length - 2)
|
|
parent = lastSlash == -1 ? "" : parent.slice(0, lastSlash + 1)
|
|
}
|
|
sub = sub.slice(m[0].length)
|
|
}
|
|
return parent + sub
|
|
}
|
|
|
|
function dirName(path) {
|
|
var lastSlash = path.lastIndexOf("/")
|
|
if (lastSlash == -1) return ""
|
|
return path.slice(0, lastSlash + 1)
|
|
}
|
|
function baseName(path) {
|
|
var lastSlash = path.lastIndexOf("/")
|
|
if (lastSlash == -1) return path
|
|
else return path.slice(lastSlash + 1)
|
|
}
|
|
|
|
function defaultResolver(name, parentFile) {
|
|
if (!/^\.\.?\//.test(name)) return
|
|
var path = resolvePath(dirName(parentFile), name)
|
|
var server = infer.cx().parent
|
|
if (server.findFile(path)) return path
|
|
if (server.findFile(path + ".js")) return path + ".js"
|
|
}
|
|
|
|
// Under node, replace completeFileName with a version that actually
|
|
// queries the file system
|
|
if (require) (function() {
|
|
var fs = require("fs"), path = require("path")
|
|
|
|
Modules.prototype.completeFileName = function(completions, query, parentFile, word, dir) {
|
|
var pDir = this.server.projectDir
|
|
var endSlash = /\/$/.test(word)
|
|
if (parentFile) {
|
|
var pt = path.resolve(pDir, path.dirname(parentFile), word)
|
|
dir = endSlash ? pt : path.dirname(pt)
|
|
}
|
|
var base = endSlash ? word : path.dirname(word) + "/"
|
|
var filePart = endSlash ? "" : path.basename(word)
|
|
|
|
var me = this
|
|
fs.readdirSync(dir).forEach(function(file) {
|
|
if (/^\./.test(file)) return
|
|
if (filter(filePart, file, query)) {
|
|
var projectPath = me.server.normalizeFilename(path.relative(pDir, path.resolve(dir, file)))
|
|
if (projectPath == parentFile) return
|
|
var value = me.modules[projectPath]
|
|
if (/\.js$/.test(file)) file = file.slice(0, file.length - 3)
|
|
tern.addCompletion(query, completions, base + file, value)
|
|
}
|
|
})
|
|
}
|
|
}())
|
|
|
|
function isRelative(path) {
|
|
return /^\.\.?\//.test(path)
|
|
}
|
|
|
|
function filter(word, string, query) {
|
|
return query.filter === false || !word ||
|
|
(query.caseInsensitive ? string.toLowerCase() : string).indexOf(word) == 0
|
|
}
|
|
|
|
function preCondenseReach(state) {
|
|
var mods = infer.cx().parent.mod.modules.modules
|
|
var node = state.roots["!modules"] = new infer.Obj(null)
|
|
for (var name in mods) {
|
|
var mod = mods[name]
|
|
var id = mod.origin || name
|
|
var prop = node.defProp(id.replace(/\./g, "`"))
|
|
prop.origin = mod.origin
|
|
mod.propagate(prop)
|
|
}
|
|
}
|
|
|
|
function postLoadDef(data) {
|
|
var cx = infer.cx(), me = cx.parent.mod.modules
|
|
var mods = cx.definitions[data["!name"]]["!modules"]
|
|
if (mods) for (var name in mods.props) {
|
|
var origin = name.replace(/`/g, ".")
|
|
var mod = me.get(origin)
|
|
mod.origin = origin
|
|
mods.props[name].propagate(mod)
|
|
}
|
|
var known = cx.definitions[data["!name"]]["!known_modules"]
|
|
if (known) for (var name in known.props)
|
|
me.knownModules[name.replace(/`/g, ".")] = known.props[name]
|
|
}
|
|
|
|
function findTypeAt(_file, _pos, expr, type) {
|
|
if (!expr) return type
|
|
var me = infer.cx().parent.mod.modules
|
|
var modType = me.getModType(expr.node)
|
|
if (!modType) return type
|
|
|
|
// The `type` is a value shared for all string literals.
|
|
// We must create a copy before modifying `origin` and `originNode`.
|
|
// Otherwise all string literals would point to the last jump location
|
|
type = Object.create(type)
|
|
type.origin = modType.origin
|
|
type.originNode = modType.originNode
|
|
if (modType.doc) type.doc = modType.doc
|
|
if (modType.url) type.url = modType.url
|
|
return type
|
|
}
|
|
|
|
// Complete previously seen module names when completing in strings passed to require
|
|
function findCompletions(file, query) {
|
|
var wordEnd = tern.resolvePos(file, query.end)
|
|
var expr = infer.findExpressionAround(file.ast, null, wordEnd, file.scope)
|
|
if (!expr) return null
|
|
var me = infer.cx().parent.mod.modules
|
|
|
|
if (me.isModName(expr.node) != null)
|
|
return findModuleCompletions(me, file, query, expr.node, wordEnd)
|
|
|
|
var imp = me.isImport(expr.node)
|
|
if (imp && imp.name && imp.prop != null)
|
|
return findImportCompletions(me, file, query, expr.node, imp, wordEnd)
|
|
}
|
|
|
|
function findImportCompletions(me, file, query, node, imp, wordEnd) {
|
|
var completions = []
|
|
var word = node.name.slice(0, wordEnd - node.start)
|
|
if (query.caseInsensitive) word = word.toLowerCase()
|
|
|
|
var modType = me.resolveModule(imp.name, node.sourceFile.name).getType()
|
|
if (!modType) return null
|
|
infer.forAllPropertiesOf(modType, function(prop, obj, depth) {
|
|
if (obj == infer.cx().protos.Object) return
|
|
if (query.filter !== false && word &&
|
|
(query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return
|
|
tern.addCompletion(query, completions, prop, obj && obj.props[prop], depth)
|
|
})
|
|
return {
|
|
start: tern.outputPos(query, file, node.start),
|
|
end: tern.outputPos(query, file, wordEnd),
|
|
completions: completions,
|
|
isSpecifier: true
|
|
}
|
|
}
|
|
|
|
function findModuleCompletions(me, file, query, argNode, wordEnd) {
|
|
if (argNode.type != "Literal" || typeof argNode.value != "string" ||
|
|
argNode.start > wordEnd || argNode.end < wordEnd) return
|
|
|
|
var word = argNode.raw.slice(1, wordEnd - argNode.start), quote = argNode.raw.charAt(0)
|
|
if (word && word.charAt(word.length - 1) == quote)
|
|
word = word.slice(0, word.length - 1)
|
|
if (query.caseInsensitive) word = word.toLowerCase()
|
|
|
|
var completions = []
|
|
if (isRelative(word)) me.completeFileName(completions, query, file.name, word)
|
|
else me.completeModuleName(completions, query, word)
|
|
|
|
if (argNode.end == wordEnd + 1 && file.text.charAt(wordEnd) == quote)
|
|
++wordEnd
|
|
return {
|
|
start: tern.outputPos(query, file, argNode.start),
|
|
end: tern.outputPos(query, file, wordEnd),
|
|
isProperty: false,
|
|
completions: completions.map(function(rec) {
|
|
var name = typeof rec == "string" ? rec : rec.name
|
|
var string = JSON.stringify(name)
|
|
if (quote == "'") string = quote + string.slice(1, string.length -1).replace(/'/g, "\\'") + quote
|
|
if (typeof rec == "string") return string
|
|
rec.displayName = name
|
|
rec.name = string
|
|
return rec
|
|
})
|
|
}
|
|
}
|
|
|
|
tern.registerPlugin("modules", function(server, options) {
|
|
server.mod.modules = new Modules(server, options)
|
|
|
|
server.on("beforeLoad", function(file) {
|
|
file.scope = this.mod.modules.buildWrappingScope(file.scope, file.name, file.ast)
|
|
})
|
|
|
|
server.on("afterLoad", function(file) {
|
|
var mod = this.mod.modules.get(file.name)
|
|
mod.origin = file.name
|
|
this.mod.modules.signal("getExports", file, mod)
|
|
})
|
|
|
|
server.on("reset", function() {
|
|
this.mod.modules.modules = Object.create(null)
|
|
})
|
|
|
|
server.on("preCondenseReach", preCondenseReach)
|
|
server.on("postLoadDef", postLoadDef)
|
|
server.on("typeAt", findTypeAt)
|
|
server.on("completion", findCompletions)
|
|
})
|
|
|
|
tern.defineQueryType("exports", {
|
|
takesFile: true,
|
|
run: function(server, query, file) {
|
|
function describe(aval) {
|
|
var target = {}, type = aval.getType(false)
|
|
target.type = infer.toString(type, 3)
|
|
var doc = aval.doc || (type && type.doc), url = aval.url || (type && type.url)
|
|
if (doc) target.doc = doc
|
|
if (url) target.url = url
|
|
var span = tern.getSpan(aval) || (type && tern.getSpan(type))
|
|
if (span) tern.storeSpan(server, query, span, target)
|
|
return target
|
|
}
|
|
|
|
var mod = server.mod.modules, known = mod && mod.modules[file.name]
|
|
if (!known) return {}
|
|
var resp = describe(known);
|
|
var type = known.getType(false)
|
|
if (type instanceof infer.Obj) {
|
|
var props = resp.props = {}
|
|
for (var prop in type.props)
|
|
props[prop] = describe(type.props[prop])
|
|
}
|
|
return resp
|
|
}
|
|
})
|
|
})
|