javascript-typescript-langserver
Version:
Implementation of the Language Server Protocol for JavaScript and TypeScript
811 lines • 33.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const iterare_1 = require("iterare");
const lodash_1 = require("lodash");
const opentracing_1 = require("opentracing");
const path = require("path");
const rxjs_1 = require("rxjs");
const ts = require("typescript");
const logging_1 = require("./logging");
const plugins_1 = require("./plugins");
const tracing_1 = require("./tracing");
const util_1 = require("./util");
const LAST_FORWARD_OR_BACKWARD_SLASH = /[\\\/][^\\\/]*$/;
/**
* Implementaton of LanguageServiceHost that works with in-memory file system.
* It takes file content from local cache and provides it to TS compiler on demand
*
* @implements ts.LanguageServiceHost
*/
class InMemoryLanguageServiceHost {
constructor(rootPath, options, fs, versions, logger = new logging_1.NoopLogger()) {
this.logger = logger;
this.rootPath = rootPath;
this.options = options;
this.fs = fs;
this.versions = versions;
this.projectVersion = 1;
this.filePaths = [];
}
/**
* TypeScript uses this method (when present) to compare project's version
* with the last known one to decide if internal data should be synchronized
*/
getProjectVersion() {
return '' + this.projectVersion;
}
getNewLine() {
// Although this is optional, language service was sending edits with carriage returns if not specified.
// TODO: combine with the FormatOptions defaults.
return '\n';
}
/**
* Incrementing current project version, telling TS compiler to invalidate internal data
*/
incProjectVersion() {
this.projectVersion++;
}
getCompilationSettings() {
return this.options;
}
getScriptFileNames() {
return this.filePaths;
}
/**
* Adds a file and increments project version, used in conjunction with getProjectVersion()
* which may be called by TypeScript to check if internal data is up to date
*
* @param filePath relative file path
*/
addFile(filePath) {
this.filePaths.push(filePath);
this.incProjectVersion();
}
/**
* @param fileName absolute file path
*/
getScriptVersion(filePath) {
const uri = util_1.path2uri(filePath);
let version = this.versions.get(uri);
if (!version) {
version = 1;
this.versions.set(uri, version);
}
return '' + version;
}
/**
* @param filePath absolute file path
*/
getScriptSnapshot(filePath) {
const exists = this.fs.fileExists(filePath);
if (!exists) {
return undefined;
}
return ts.ScriptSnapshot.fromString(this.fs.readFile(filePath));
}
getCurrentDirectory() {
return this.rootPath;
}
getDefaultLibFileName(options) {
return util_1.toUnixPath(ts.getDefaultLibFilePath(options));
}
trace(message) {
// empty
}
log(message) {
// empty
}
error(message) {
this.logger.error(message);
}
readFile(path, encoding) {
return this.fs.readFile(path);
}
fileExists(path) {
return this.fs.fileExists(path);
}
}
exports.InMemoryLanguageServiceHost = InMemoryLanguageServiceHost;
/**
* ProjectConfiguration instances track the compiler configuration (as
* defined by {tj}sconfig.json if it exists) and state for a single
* TypeScript project. It represents the world of the view as
* presented to the compiler.
*
* For efficiency, a ProjectConfiguration instance may hide some files
* from the compiler, preventing them from being parsed and
* type-checked. Depending on the use, the caller should call one of
* the ensure* methods to ensure that the appropriate files have been
* made available to the compiler before calling any other methods on
* the ProjectConfiguration or its public members. By default, no
* files are parsed.
*
* Windows file paths are converted to UNIX-style forward slashes
* when compared with Typescript configuration (isGlobalTSFile,
* expectedFilePaths and typeRoots)
*/
class ProjectConfiguration {
/**
* @param fs file system to use
* @param documentRegistry Shared DocumentRegistry that manages SourceFile objects
* @param rootFilePath root file path, absolute
* @param configFilePath configuration file path, absolute
* @param configContent optional configuration content to use instead of reading configuration file)
*/
constructor(fs, documentRegistry, rootFilePath, versions, configFilePath, configContent, traceModuleResolution, pluginSettings, logger = new logging_1.NoopLogger()) {
this.documentRegistry = documentRegistry;
this.pluginSettings = pluginSettings;
this.logger = logger;
/**
* List of files that project consist of (based on tsconfig includes/excludes and wildcards).
* Each item is a relative UNIX-like file path
*/
this.expectedFilePaths = new Set();
this.initialized = false;
this.ensuredAllFiles = false;
this.ensuredBasicFiles = false;
this.fs = fs;
this.configFilePath = configFilePath;
this.configContent = configContent;
this.versions = versions;
this.traceModuleResolution = traceModuleResolution || false;
this.rootFilePath = rootFilePath;
}
/**
* reset resets a ProjectConfiguration to its state immediately
* after construction. It should be called whenever the underlying
* local filesystem (fs) has changed, and so the
* ProjectConfiguration can no longer assume its state reflects
* that of the underlying files.
*/
reset() {
this.initialized = false;
this.ensuredBasicFiles = false;
this.ensuredAllFiles = false;
this.service = undefined;
this.host = undefined;
this.expectedFilePaths = new Set();
}
/**
* @return language service object
*/
getService() {
if (!this.service) {
throw new Error('project is uninitialized');
}
return this.service;
}
/**
* Tells TS service to recompile program (if needed) based on current list of files and compilation options.
* TS service relies on information provided by language servide host to see if there were any changes in
* the whole project or in some files
*
* @return program object (cached result of parsing and typechecking done by TS service)
*/
getProgram(childOf = new opentracing_1.Span()) {
return tracing_1.traceSync('Get program', childOf, span => this.getService().getProgram());
}
/**
* @return language service host that TS service uses to read the data
*/
getHost() {
if (!this.host) {
throw new Error('project is uninitialized');
}
return this.host;
}
/**
* Initializes (sub)project by parsing configuration and making proper internal objects
*/
init(span = new opentracing_1.Span()) {
if (this.initialized) {
return;
}
let configObject;
if (!this.configContent) {
const jsonConfig = ts.parseConfigFileTextToJson(this.configFilePath, this.fs.readFile(this.configFilePath));
if (jsonConfig.error) {
this.logger.error('Cannot parse ' + this.configFilePath + ': ' + jsonConfig.error.messageText);
throw new Error('Cannot parse ' + this.configFilePath + ': ' + jsonConfig.error.messageText);
}
configObject = jsonConfig.config;
}
else {
configObject = this.configContent;
}
let dir = util_1.toUnixPath(this.configFilePath);
const pos = dir.lastIndexOf('/');
if (pos <= 0) {
dir = '';
}
else {
dir = dir.substring(0, pos);
}
const base = dir || this.fs.path;
const configParseResult = ts.parseJsonConfigFileContent(configObject, this.fs, base);
this.expectedFilePaths = new Set(configParseResult.fileNames);
const options = configParseResult.options;
const pathResolver = /^[a-z]:\//i.test(base) ? path.win32 : path.posix;
this.typeRoots = options.typeRoots
? options.typeRoots.map((r) => util_1.toUnixPath(pathResolver.resolve(this.rootFilePath, r)))
: [];
if (/(^|\/)jsconfig\.json$/.test(this.configFilePath)) {
options.allowJs = true;
}
if (this.traceModuleResolution) {
options.traceResolution = true;
}
this.host = new InMemoryLanguageServiceHost(this.fs.path, options, this.fs, this.versions, this.logger);
this.service = ts.createLanguageService(this.host, this.documentRegistry);
const pluginLoader = new plugins_1.PluginLoader(this.rootFilePath, this.fs, this.pluginSettings, this.logger);
pluginLoader.loadPlugins(options, (factory, config) => this.wrapService(factory, config));
this.initialized = true;
}
/**
* Replaces the LanguageService with an instance wrapped by the plugin
* @param pluginModuleFactory function to create the module
* @param configEntry extra settings from tsconfig to pass to the plugin module
*/
wrapService(pluginModuleFactory, configEntry) {
try {
if (typeof pluginModuleFactory !== 'function') {
this.logger.info(`Skipped loading plugin ${configEntry.name} because it didn't expose a proper factory function`);
return;
}
const info = {
config: configEntry,
project: {
// TODO: may need more support
getCurrentDirectory: () => this.getHost().getCurrentDirectory(),
projectService: { logger: this.logger },
},
languageService: this.getService(),
languageServiceHost: this.getHost(),
serverHost: {},
};
const pluginModule = pluginModuleFactory({ typescript: ts });
this.service = pluginModule.create(info);
}
catch (e) {
this.logger.error(`Plugin activation failed: ${e}`);
}
}
/**
* Ensures we are ready to process files from a given sub-project
*/
ensureConfigFile(span = new opentracing_1.Span()) {
this.init(span);
}
/**
* Determines if a fileName is a declaration file within expected files or type roots
* @param fileName A Unix-like absolute file path.
*/
isExpectedDeclarationFile(fileName) {
return (util_1.isDeclarationFile(fileName) &&
(this.expectedFilePaths.has(fileName) || this.typeRoots.some(root => fileName.startsWith(root))));
}
/**
* Ensures we added basic files (global TS files, dependencies, declarations)
*/
ensureBasicFiles(span = new opentracing_1.Span()) {
if (this.ensuredBasicFiles) {
return;
}
this.init(span);
const program = this.getProgram(span);
if (!program) {
return;
}
// Add all global declaration files from the workspace and all declarations from the project
for (const uri of this.fs.uris()) {
const fileName = util_1.uri2path(uri);
const unixPath = util_1.toUnixPath(fileName);
if (util_1.isGlobalTSFile(unixPath) || this.isExpectedDeclarationFile(unixPath)) {
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
this.getHost().addFile(fileName);
}
}
}
this.ensuredBasicFiles = true;
}
/**
* Ensures a single file is available to the LanguageServiceHost
* @param filePath
*/
ensureSourceFile(filePath, span = new opentracing_1.Span()) {
const program = this.getProgram(span);
if (!program) {
return;
}
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
this.getHost().addFile(filePath);
}
}
/**
* Ensures we added all project's source file (as were defined in tsconfig.json)
*/
ensureAllFiles(span = new opentracing_1.Span()) {
if (this.ensuredAllFiles) {
return;
}
this.init(span);
if (this.getHost().complete) {
return;
}
const program = this.getProgram(span);
if (!program) {
return;
}
for (const fileName of this.expectedFilePaths) {
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
this.getHost().addFile(fileName);
}
}
this.getHost().complete = true;
this.ensuredAllFiles = true;
}
}
exports.ProjectConfiguration = ProjectConfiguration;
/**
* ProjectManager translates VFS files to one or many projects denoted by [tj]config.json.
* It uses either local or remote file system to fetch directory tree and files from and then
* makes one or more LanguageService objects. By default all LanguageService objects contain no files,
* they are added on demand - current file for hover or definition, project's files for references and
* all files from all projects for workspace symbols.
*
* ProjectManager preserves Windows paths until passed to ProjectConfiguration or TS APIs.
*/
class ProjectManager {
/**
* @param rootPath root path as passed to `initialize`
* @param inMemoryFileSystem File system that keeps structure and contents in memory
* @param strict indicates if we are working in strict mode (VFS) or with a local file system
* @param traceModuleResolution allows to enable module resolution tracing (done by TS compiler)
*/
constructor(rootPath, inMemoryFileSystem, updater, traceModuleResolution, pluginSettings, logger = new logging_1.NoopLogger()) {
this.logger = logger;
/**
* (Workspace subtree (folder) -> TS or JS configuration) mapping.
* Configuration settings for a source file A are located in the closest parent folder of A.
* Map keys are relative (to workspace root) paths
*/
this.configs = {
js: new Map(),
ts: new Map(),
};
/**
* A URI Map from file to files referenced by the file, so files only need to be pre-processed once
*/
this.referencedFiles = new Map();
/**
* Tracks all Subscriptions that are done in the lifetime of this object to dispose on `dispose()`
*/
this.subscriptions = new rxjs_1.Subscription();
this.rootPath = rootPath;
this.updater = updater;
this.inMemoryFs = inMemoryFileSystem;
this.versions = new Map();
this.pluginSettings = pluginSettings;
this.traceModuleResolution = traceModuleResolution || false;
// Share DocumentRegistry between all ProjectConfigurations
const documentRegistry = ts.createDocumentRegistry();
// Create catch-all fallback configs in case there are no tsconfig.json files
// They are removed once at least one tsconfig.json is found
const trimmedRootPath = this.rootPath.replace(/[\\\/]+$/, '');
const fallbackConfigs = {};
for (const configType of ['js', 'ts']) {
const configs = this.configs[configType];
const tsConfig = {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
allowNonTsExtensions: false,
allowJs: configType === 'js',
},
include: { js: ['**/*.js', '**/*.jsx'], ts: ['**/*.ts', '**/*.tsx'] }[configType],
};
const config = new ProjectConfiguration(this.inMemoryFs, documentRegistry, trimmedRootPath, this.versions, '', tsConfig, this.traceModuleResolution, this.pluginSettings, this.logger);
configs.set(trimmedRootPath, config);
fallbackConfigs[configType] = config;
}
// Whenever a file with content is added to the InMemoryFileSystem, check if it's a tsconfig.json and add a new ProjectConfiguration
this.subscriptions.add(rxjs_1.Observable.fromEvent(inMemoryFileSystem, 'add', (k, v) => [k, v])
.filter(([uri, content]) => !!content && /\/[tj]sconfig\.json/.test(uri) && !uri.includes('/node_modules/'))
.subscribe(([uri, content]) => {
const filePath = util_1.uri2path(uri);
const pos = filePath.search(LAST_FORWARD_OR_BACKWARD_SLASH);
const dir = pos <= 0 ? '' : filePath.substring(0, pos);
const configType = this.getConfigurationType(filePath);
const configs = this.configs[configType];
configs.set(dir, new ProjectConfiguration(this.inMemoryFs, documentRegistry, dir, this.versions, filePath, undefined, this.traceModuleResolution, this.pluginSettings, this.logger));
// Remove catch-all config (if exists)
if (configs.get(trimmedRootPath) === fallbackConfigs[configType]) {
configs.delete(trimmedRootPath);
}
}));
}
/**
* Disposes the object (removes all registered listeners)
*/
dispose() {
this.subscriptions.unsubscribe();
}
/**
* @return root path (as passed to `initialize`)
*/
getRemoteRoot() {
return this.rootPath;
}
/**
* @return local side of file content provider which keeps cached copies of fethed files
*/
getFs() {
return this.inMemoryFs;
}
/**
* @param filePath file path (both absolute or relative file paths are accepted)
* @return true if there is a fetched file with a given path
*/
hasFile(filePath) {
return this.inMemoryFs.fileExists(filePath);
}
/**
* @return all sub-projects we have identified for a given workspace.
* Sub-project is mainly a folder which contains tsconfig.json, jsconfig.json, package.json,
* or a root folder which serves as a fallback
*/
configurations() {
return iterare_1.default(this.configs.js.values()).concat(this.configs.ts.values());
}
/**
* Ensures that the module structure of the project exists in memory.
* TypeScript/JavaScript module structure is determined by [jt]sconfig.json,
* filesystem layout, global*.d.ts and package.json files.
* Then creates new ProjectConfigurations, resets existing and invalidates file references.
*/
ensureModuleStructure(childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Ensure module structure', childOf, span => {
if (!this.ensuredModuleStructure) {
this.ensuredModuleStructure = this.updater
.ensureStructure()
// Ensure content of all all global .d.ts, [tj]sconfig.json, package.json files
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFs.uris())))
.filter(uri => util_1.isGlobalTSFile(uri) || util_1.isConfigFile(uri) || util_1.isPackageJsonFile(uri))
.mergeMap(uri => this.updater.ensure(uri))
.do(lodash_1.noop, err => {
this.ensuredModuleStructure = undefined;
}, () => {
// Reset all compilation state
// TODO ze incremental compilation instead
for (const config of this.configurations()) {
config.reset();
}
// Require re-processing of file references
this.invalidateReferencedFiles();
})
.publishReplay()
.refCount();
}
return this.ensuredModuleStructure;
});
}
/**
* Invalidates caches for `ensureModuleStructure`, `ensureAllFiles` and `insureOwnFiles`
*/
invalidateModuleStructure() {
this.ensuredModuleStructure = undefined;
this.ensuredConfigDependencies = undefined;
this.ensuredAllFiles = undefined;
this.ensuredOwnFiles = undefined;
}
/**
* Ensures all files not in node_modules were fetched.
* This includes all js/ts files, tsconfig files and package.json files.
* Invalidates project configurations after execution
*/
ensureOwnFiles(childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Ensure own files', childOf, span => {
if (!this.ensuredOwnFiles) {
this.ensuredOwnFiles = this.updater
.ensureStructure(span)
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFs.uris())))
.filter(uri => (!uri.includes('/node_modules/') && util_1.isJSTSFile(uri)) ||
util_1.isConfigFile(uri) ||
util_1.isPackageJsonFile(uri))
.mergeMap(uri => this.updater.ensure(uri))
.do(lodash_1.noop, err => {
this.ensuredOwnFiles = undefined;
})
.publishReplay()
.refCount();
}
return this.ensuredOwnFiles;
});
}
/**
* Ensures all files were fetched from the remote file system.
* Invalidates project configurations after execution
*/
ensureAllFiles(childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Ensure all files', childOf, span => {
if (!this.ensuredAllFiles) {
this.ensuredAllFiles = this.updater
.ensureStructure(span)
.concat(rxjs_1.Observable.defer(() => util_1.observableFromIterable(this.inMemoryFs.uris())))
.filter(uri => util_1.isJSTSFile(uri) || util_1.isConfigFile(uri) || util_1.isPackageJsonFile(uri))
.mergeMap(uri => this.updater.ensure(uri))
.do(lodash_1.noop, err => {
this.ensuredAllFiles = undefined;
})
.publishReplay()
.refCount();
}
return this.ensuredAllFiles;
});
}
/**
* Recursively collects file(s) dependencies up to given level.
* Dependencies are extracted by TS compiler from import and reference statements
*
* Dependencies include:
* - all the configuration files
* - files referenced by the given file
* - files included by the given file
*
* The return values of this method are not cached, but those of the file fetching and file processing are.
*
* @param uri File to process
* @param maxDepth Stop collecting when reached given recursion level
* @param ignore Tracks visited files to prevent cycles
* @param childOf OpenTracing parent span for tracing
* @return Observable of file URIs ensured
*/
ensureReferencedFiles(uri, maxDepth = 30, ignore = new Set(), childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Ensure referenced files', childOf, span => {
span.addTags({ uri, maxDepth });
ignore.add(uri);
return (this.ensureModuleStructure(span)
.concat(rxjs_1.Observable.defer(() => this.ensureConfigDependencies()))
// If max depth was reached, don't go any further
.concat(rxjs_1.Observable.defer(() => (maxDepth === 0 ? rxjs_1.Observable.empty() : this.resolveReferencedFiles(uri))))
// Prevent cycles
.filter(referencedUri => !ignore.has(referencedUri))
// Call method recursively with one less dep level
.mergeMap(referencedUri => this.ensureReferencedFiles(referencedUri, maxDepth - 1, ignore)
// Continue even if an import wasn't found
.catch(err => {
this.logger.error(`Error resolving file references for ${uri}:`, err);
return [];
})));
});
}
/**
* Determines if a tsconfig/jsconfig needs additional declaration files loaded.
* @param filePath A UNIX-like absolute file path
*/
isConfigDependency(filePath) {
for (const config of this.configurations()) {
config.ensureConfigFile();
if (config.isExpectedDeclarationFile(filePath)) {
return true;
}
}
return false;
}
/**
* Loads files determined by tsconfig to be needed into the file system
*/
ensureConfigDependencies(childOf = new opentracing_1.Span()) {
return tracing_1.traceObservable('Ensure config dependencies', childOf, span => {
if (!this.ensuredConfigDependencies) {
this.ensuredConfigDependencies = util_1.observableFromIterable(this.inMemoryFs.uris())
.filter(uri => this.isConfigDependency(util_1.toUnixPath(util_1.uri2path(uri))))
.mergeMap(uri => this.updater.ensure(uri))
.do(lodash_1.noop, err => {
this.ensuredConfigDependencies = undefined;
})
.publishReplay()
.refCount();
}
return this.ensuredConfigDependencies;
});
}
/**
* Invalidates a cache entry for `resolveReferencedFiles` (e.g. because the file changed)
*
* @param uri The URI that referenced files should be invalidated for. If not given, all entries are invalidated
*/
invalidateReferencedFiles(uri) {
if (uri) {
this.referencedFiles.delete(uri);
}
else {
this.referencedFiles.clear();
}
}
/**
* Returns the files that are referenced from a given file.
* If the file has already been processed, returns a cached value.
*
* @param uri URI of the file to process
* @return URIs of files referenced by the file
*/
resolveReferencedFiles(uri, span = new opentracing_1.Span()) {
let observable = this.referencedFiles.get(uri);
if (observable) {
return observable;
}
observable = this.updater
.ensure(uri)
.concat(rxjs_1.Observable.defer(() => {
const referencingFilePath = util_1.uri2path(uri);
const config = this.getConfiguration(referencingFilePath);
config.ensureBasicFiles(span);
const contents = this.inMemoryFs.getContent(uri);
const info = ts.preProcessFile(contents, true, true);
const compilerOpt = config.getHost().getCompilationSettings();
const pathResolver = referencingFilePath.includes('\\') ? path.win32 : path.posix;
// Iterate imported files
return rxjs_1.Observable.merge(
// References with `import`
rxjs_1.Observable.from(info.importedFiles)
.map(importedFile => ts.resolveModuleName(importedFile.fileName, util_1.toUnixPath(referencingFilePath), compilerOpt, this.inMemoryFs))
// false means we didn't find a file defining the module. It could still
// exist as an ambient module, which is why we fetch global*.d.ts files.
.filter(resolved => !!(resolved && resolved.resolvedModule))
.map(resolved => resolved.resolvedModule.resolvedFileName),
// References with `<reference path="..."/>`
rxjs_1.Observable.from(info.referencedFiles)
// Resolve triple slash references relative to current file instead of using
// module resolution host because it behaves differently in "nodejs" mode
.map(referencedFile => pathResolver.resolve(this.rootPath, pathResolver.dirname(referencingFilePath), util_1.toUnixPath(referencedFile.fileName))),
// References with `<reference types="..."/>`
rxjs_1.Observable.from(info.typeReferenceDirectives)
.map(typeReferenceDirective => ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, referencingFilePath, compilerOpt, this.inMemoryFs))
.filter(resolved => !!(resolved &&
resolved.resolvedTypeReferenceDirective &&
resolved.resolvedTypeReferenceDirective.resolvedFileName))
.map(resolved => resolved.resolvedTypeReferenceDirective.resolvedFileName));
}))
// Use same scheme, slashes, host for referenced URI as input file
.map(util_1.path2uri)
// Don't cache errors
.do(lodash_1.noop, err => {
this.referencedFiles.delete(uri);
})
// Make sure all subscribers get the same values
.publishReplay()
.refCount();
this.referencedFiles.set(uri, observable);
return observable;
}
/**
* @param filePath source file path, absolute
* @return project configuration for a given source file. Climbs directory tree up to workspace root if needed
*/
getConfiguration(filePath, configType = this.getConfigurationType(filePath)) {
const config = this.getConfigurationIfExists(filePath, configType);
if (!config) {
throw new Error(`TypeScript config file for ${filePath} not found`);
}
return config;
}
/**
* @param filePath source file path, absolute
* @return closest configuration for a given file path or undefined if there is no such configuration
*/
getConfigurationIfExists(filePath, configType = this.getConfigurationType(filePath)) {
let dir = filePath;
let config;
const configs = this.configs[configType];
if (!configs) {
return undefined;
}
const rootPath = this.rootPath.replace(/[\\\/]+$/, '');
while (dir && dir !== rootPath) {
config = configs.get(dir);
if (config) {
return config;
}
const pos = dir.search(LAST_FORWARD_OR_BACKWARD_SLASH);
if (pos <= 0) {
dir = '';
}
else {
dir = dir.substring(0, pos);
}
}
return configs.get(rootPath);
}
/**
* Returns the ProjectConfiguration a file belongs to
*/
getParentConfiguration(uri, configType) {
return this.getConfigurationIfExists(util_1.uri2path(uri), configType);
}
/**
* Returns all ProjectConfigurations contained in the given directory or one of its childrens
*
* @param uri URI of a directory
*/
getChildConfigurations(uri) {
const pathPrefix = util_1.uri2path(uri);
return iterare_1.default(this.configs.ts)
.concat(this.configs.js)
.filter(([folderPath, config]) => folderPath.startsWith(pathPrefix))
.map(([folderPath, config]) => config);
}
/**
* Called when file was opened by client. Current implementation
* does not differenciates open and change events
* @param uri file's URI
* @param text file's content
*/
didOpen(uri, text) {
this.didChange(uri, text);
}
/**
* Called when file was closed by client. Current implementation invalidates compiled version
* @param uri file's URI
*/
didClose(uri, span = new opentracing_1.Span()) {
const filePath = util_1.uri2path(uri);
this.inMemoryFs.didClose(uri);
let version = this.versions.get(uri) || 0;
this.versions.set(uri, ++version);
const config = this.getConfigurationIfExists(filePath);
if (!config) {
return;
}
config.ensureConfigFile(span);
config.getHost().incProjectVersion();
}
/**
* Called when file was changed by client. Current implementation invalidates compiled version
* @param uri file's URI
* @param text file's content
*/
didChange(uri, text, span = new opentracing_1.Span()) {
const filePath = util_1.uri2path(uri);
this.inMemoryFs.didChange(uri, text);
let version = this.versions.get(uri) || 0;
this.versions.set(uri, ++version);
const config = this.getConfigurationIfExists(filePath);
if (!config) {
return;
}
config.ensureConfigFile(span);
config.ensureSourceFile(filePath);
config.getHost().incProjectVersion();
}
/**
* Called when file was saved by client
* @param uri file's URI
*/
didSave(uri) {
this.inMemoryFs.didSave(uri);
}
/**
* @param filePath path to source (or config) file
* @return configuration type to use for a given file
*/
getConfigurationType(filePath) {
const unixPath = util_1.toUnixPath(filePath);
const name = path.posix.basename(unixPath);
if (name === 'tsconfig.json') {
return 'ts';
}
else if (name === 'jsconfig.json') {
return 'js';
}
const extension = path.posix.extname(unixPath);
if (extension === '.js' || extension === '.jsx') {
return 'js';
}
return 'ts';
}
}
exports.ProjectManager = ProjectManager;
//# sourceMappingURL=project-manager.js.map
;