UNPKG

javascript-typescript-langserver

Version:

Implementation of the Language Server Protocol for JavaScript and TypeScript

811 lines 33.8 kB
"use strict"; 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