From 2ba27a58fb7e4b3864836d3a424a6d1f4f5d8478 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:03:19 -0700 Subject: [PATCH] Create composite host/fshost. This removes fshost usage in most places except for construction of objects. --- src/harness/harnessLanguageService.ts | 4 +- src/server/editorServices.ts | 139 +++++++++++++----- src/server/packageJsonCache.ts | 2 +- src/server/project.ts | 46 +++--- src/server/scriptInfo.ts | 11 +- src/server/session.ts | 8 +- src/server/types.ts | 12 +- .../unittests/tsserver/externalProjects.ts | 2 +- .../unittests/tsserver/webServer.ts | 6 +- src/tsserver/server.ts | 18 +-- src/tsserver/webServer.ts | 2 +- src/webServer/webServer.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 19 ++- 13 files changed, 167 insertions(+), 104 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 01b74f56973..0b8d0cedea1 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -918,7 +918,7 @@ namespace Harness.LanguageService { const proxy = makeDefaultProxy(info); proxy.getSemanticDiagnostics = filename => { const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.fsHost.useCaseSensitiveFileNames)))!; + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; prev.push({ category: ts.DiagnosticCategory.Warning, file: sourceFile, @@ -945,7 +945,7 @@ namespace Harness.LanguageService { const proxy = makeDefaultProxy(info); proxy.getSemanticDiagnostics = filename => { const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.fsHost.useCaseSensitiveFileNames)))!; + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; prev.push({ category: ts.DiagnosticCategory.Error, file: sourceFile, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 63841247920..5fbfe4aa318 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -358,6 +358,67 @@ namespace ts.server { } } + /*@internal*/ + export function getFileSystemHost(host: ServerHost, fshost: FileServerHost | undefined): ServerHost { + return !fshost ? host + : { + // FileServerHost + readFile: fshost.readFile.bind(fshost), + writeFile: fshost.writeFile.bind(fshost), + fileExists: fshost.fileExists.bind(fshost), + directoryExists: fshost.directoryExists.bind(fshost), + getFileSize: fshost.getFileSize?.bind(fshost), + getModifiedTime: fshost.getModifiedTime?.bind(fshost), + getDirectories: fshost.getDirectories.bind(fshost), + getCurrentDirectory: fshost.getCurrentDirectory.bind(fshost), + getExecutingFilePath: fshost.getExecutingFilePath.bind(fshost), + realpath: fshost.realpath?.bind(fshost), + resolvePath: fshost.resolvePath.bind(fshost), + createDirectory: fshost.createDirectory.bind(fshost), + setModifiedTime: fshost.setModifiedTime?.bind(fshost), + deleteFile: fshost.deleteFile?.bind(fshost), + readDirectory: fshost.readDirectory.bind(fshost), + watchFile: fshost.watchFile.bind(fshost), + watchDirectory: fshost.watchDirectory.bind(fshost), + useCaseSensitiveFileNames: fshost.useCaseSensitiveFileNames, + // ServerHost + setTimeout: host.setTimeout.bind(host), + clearTimeout: host.clearTimeout.bind(host), + setImmediate: host.setImmediate.bind(host), + clearImmediate: host.clearImmediate.bind(host), + gc: host.gc?.bind(host), + trace: host.trace?.bind(host), + require: host.require?.bind(host), + // Sys + args: host.args, + newLine: host.newLine, + write: host.write.bind(host), + writeOutputIsTTY: host.writeOutputIsTTY?.bind(host), + getWidthOfTerminal: host.getWidthOfTerminal?.bind(host), + createHash: host.createHash?.bind(host), + createSHA256Hash: host.createSHA256Hash?.bind(host), + getMemoryUsage: host.getMemoryUsage?.bind(host), + exit: host.exit.bind(host), + enableCPUProfiler: host.enableCPUProfiler?.bind(host), + disableCPUProfiler: host.disableCPUProfiler?.bind(host), + cpuProfilingEnabled: host.cpuProfilingEnabled?.bind(host), + getEnvironmentVariable: host.getEnvironmentVariable.bind(host), + tryEnableSourceMapsForHost: host.tryEnableSourceMapsForHost?.bind(host), + debugMode: host.debugMode, + clearScreen: host.clearScreen?.bind(host), + setBlocking: host.setBlocking?.bind(host), + base64decode: host.base64decode?.bind(host), + base64encode: host.base64encode?.bind(host), + bufferFrom: host.bufferFrom?.bind(host), + defaultWatchFileKind: host.defaultWatchFileKind?.bind(host), + now: host.now?.bind(host), + disableUseFileVersionAsSignature: host.disableUseFileVersionAsSignature, + storeFilesChangingSignatureDuringEmit: host.storeFilesChangingSignatureDuringEmit, + // VirtualFS + ensureFileOrFolder: fshost.ensureFileOrFolder.bind(fshost), + } as any; + } + const noopConfigFileWatcher: FileWatcher = { close: noop }; /*@internal*/ @@ -761,7 +822,6 @@ namespace ts.server { readonly toCanonicalFileName: (f: string) => string; public readonly host: ServerHost; - public readonly fshost: FileServerHost; public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; @@ -807,8 +867,7 @@ namespace ts.server { private performanceEventHandler?: PerformanceEventHandler; constructor(opts: ProjectServiceOptions) { - this.host = opts.host; - this.fshost = opts.fshost || this.host; + this.host = getFileSystemHost(opts.host, opts.fshost); this.logger = opts.logger; this.cancellationToken = opts.cancellationToken; this.useSingleInferredProject = opts.useSingleInferredProject; @@ -836,11 +895,11 @@ namespace ts.server { this.syntaxOnly = false; } - if (this.fshost.realpath) { + if (this.host.realpath) { this.realpathToScriptInfos = createMultiMap(); } - this.currentDirectory = toNormalizedPath(this.fshost.getCurrentDirectory()); - this.toCanonicalFileName = createGetCanonicalFileName(this.fshost.useCaseSensitiveFileNames); + this.currentDirectory = toNormalizedPath(this.host.getCurrentDirectory()); + this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); this.globalCacheLocationDirectoryPath = this.typingsInstaller.globalTypingsCacheLocation ? ensureTrailingDirectorySeparator(this.toPath(this.typingsInstaller.globalTypingsCacheLocation)) : undefined; @@ -864,7 +923,7 @@ namespace ts.server { extraFileExtensions: [], }; - this.documentRegistry = createDocumentRegistryInternal(this.fshost.useCaseSensitiveFileNames, this.currentDirectory, this); + this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this); const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; @@ -874,7 +933,7 @@ namespace ts.server { watchFile: returnNoopFileWatcher, watchDirectory: returnNoopFileWatcher, } : - getWatchFactory(this.fshost, watchLogLevel, log, getDetailWatchInfo); + getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -883,12 +942,12 @@ namespace ts.server { /*@internal*/ getExecutingFilePath() { - return this.getNormalizedAbsolutePath(this.fshost.getExecutingFilePath()); + return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath()); } /*@internal*/ getNormalizedAbsolutePath(fileName: string) { - return getNormalizedAbsolutePath(fileName, this.fshost.getCurrentDirectory()); + return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory()); } /*@internal*/ @@ -927,7 +986,7 @@ namespace ts.server { private loadTypesMap() { try { - const fileContent = this.fshost.readFile(this.typesMapLocation!); // TODO: GH#18217 + const fileContent = this.host.readFile(this.typesMapLocation!); // TODO: GH#18217 if (fileContent === undefined) { this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`); return; @@ -1309,7 +1368,7 @@ namespace ts.server { const fileOrDirectoryPath = this.toPath(fileOrDirectory); const fsResult = config.cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) && - (fsResult && fsResult.fileExists || !fsResult && this.fshost.fileExists(fileOrDirectoryPath)) + (fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath)) ) { this.logger.info(`Config: ${configFileName} Detected new package.json: ${fileOrDirectory}`); this.onAddPackageJson(fileOrDirectoryPath); @@ -1325,7 +1384,7 @@ namespace ts.server { currentDirectory: this.currentDirectory, options: config.parsedCommandLine!.options, program: configuredProjectForConfig?.getCurrentProgram() || config.parsedCommandLine!.fileNames, - useCaseSensitiveFileNames: this.fshost.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames, writeLog: s => this.logger.info(s), toPath: s => this.toPath(s) })) return; @@ -1554,7 +1613,7 @@ namespace ts.server { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - const fileExists = info.isDynamic ? false : this.fshost.fileExists(info.fileName); + const fileExists = info.isDynamic ? false : this.host.fileExists(info.fileName); info.close(fileExists); this.stopWatchingConfigFilesForClosedScriptInfo(info); @@ -1656,7 +1715,7 @@ namespace ts.server { // Or the whole chain of config files for the roots of the inferred projects // Cache the host value of file exists and add the info to map of open files impacted by this config file - const exists = this.fshost.fileExists(configFileName); + const exists = this.host.fileExists(configFileName); let openFilesImpactedByConfigFile: ESMap | undefined; if (isOpenScriptInfo(info)) { (openFilesImpactedByConfigFile ||= new Map()).set(info.path, false); @@ -1786,7 +1845,7 @@ namespace ts.server { let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (!configFileExistenceInfo) { // Create the cache - configFileExistenceInfo = { exists: this.fshost.fileExists(configFileName) }; + configFileExistenceInfo = { exists: this.host.fileExists(configFileName) }; this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); } @@ -1843,7 +1902,7 @@ namespace ts.server { if (scriptInfo.isDynamic) return undefined; let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); - const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.fshost.useCaseSensitiveFileNames); + const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); // If projectRootPath doesn't contain info.path, then do normal search for config file const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); @@ -1958,7 +2017,7 @@ namespace ts.server { /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */ private getFilenameForExceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions | undefined, fileNames: T[], propertyReader: FilePropertyReader): string | undefined { - if (options && options.disableSizeLimit || !this.fshost.getFileSize) { + if (options && options.disableSizeLimit || !this.host.getFileSize) { return; } @@ -1974,12 +2033,12 @@ namespace ts.server { continue; } - totalNonTsFileSize += this.fshost.getFileSize(fileName); + totalNonTsFileSize += this.host.getFileSize(fileName); if (totalNonTsFileSize > maxProgramSizeForNonTsFiles || totalNonTsFileSize > availableSpace) { const top5LargestFiles = fileNames.map(f => propertyReader.getFileName(f)) .filter(name => !hasTSFileExtension(name)) - .map(name => ({ name, size: this.fshost.getFileSize!(name) })) + .map(name => ({ name, size: this.host.getFileSize!(name) })) .sort((a, b) => b.size - a.size) .slice(0, 5); this.logger.info(`Non TS file size exceeded limit (${totalNonTsFileSize}). Largest files: ${top5LargestFiles.map(file => `${file.name}:${file.size}`).join(", ")}`); @@ -2082,7 +2141,7 @@ namespace ts.server { } if (!configFileExistenceInfo.config) { configFileExistenceInfo.config = { - cachedDirectoryStructureHost: createCachedDirectoryStructureHost(this.fshost, this.fshost.getCurrentDirectory(), this.fshost.useCaseSensitiveFileNames)!, + cachedDirectoryStructureHost: createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!, projects: new Map(), reloadLevel: ConfigFileProgramReloadLevel.Full }; @@ -2183,10 +2242,10 @@ namespace ts.server { // Parse the config file and ensure its cached const cachedDirectoryStructureHost = configFileExistenceInfo.config?.cachedDirectoryStructureHost || - createCachedDirectoryStructureHost(this.fshost, this.fshost.getCurrentDirectory(), this.fshost.useCaseSensitiveFileNames)!; + createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; // Read updated contents from disk - const configFileContent = tryReadFile(configFilename, fileName => this.fshost.readFile(fileName)); + const configFileContent = tryReadFile(configFilename, fileName => this.host.readFile(fileName)); const configFile = parseJsonText(configFilename, isString(configFileContent) ? configFileContent : "") as TsConfigSourceFile; const configFileErrors = configFile.parseDiagnostics as Diagnostic[]; if (!isString(configFileContent)) configFileErrors.push(configFileContent); @@ -2485,7 +2544,7 @@ namespace ts.server { // ignore single inferred projects (handled elsewhere) if (!project.projectRootPath) continue; // ignore inferred projects that don't contain the root's path - if (!containsPath(project.projectRootPath, info.path, this.fshost.getCurrentDirectory(), !this.fshost.useCaseSensitiveFileNames)) continue; + if (!containsPath(project.projectRootPath, info.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue; // ignore inferred projects that are higher up in the project root. // TODO(rbuckton): Should we add the file as a root to these as well? if (bestMatch && bestMatch.projectRootPath!.length > project.projectRootPath.length) continue; @@ -2630,7 +2689,7 @@ namespace ts.server { (!this.globalCacheLocationDirectoryPath || !startsWith(info.path, this.globalCacheLocationDirectoryPath))) { const indexOfNodeModules = info.path.indexOf("/node_modules/"); - if (!this.fshost.getModifiedTime || indexOfNodeModules === -1) { + if (!this.host.getModifiedTime || indexOfNodeModules === -1) { info.fileWatcher = this.watchFactory.watchFile( info.fileName, (_fileName, eventKind) => this.onSourceFileChanged(info, eventKind), @@ -2728,7 +2787,7 @@ namespace ts.server { } private getModifiedTime(info: ScriptInfo) { - return (this.fshost.getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); + return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); } private refreshScriptInfo(info: ScriptInfo) { @@ -2791,10 +2850,10 @@ namespace ts.server { Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`); Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); // If the file is not opened by client and the file doesnot exist on the disk, return - if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.fshost).fileExists(fileName)) { + if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { return; } - info = new ScriptInfo(this.host, this.fshost, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 + info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 this.filenameToScriptInfo.set(info.path, info); this.filenameToScriptInfoVersion.delete(info.path); if (!openedByClient) { @@ -2832,7 +2891,7 @@ namespace ts.server { /*@internal*/ getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host - const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.fshost); + const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host); if (!declarationInfo) { if (sourceFileName) { // Project contains source file and it generates the generated file name @@ -2869,7 +2928,7 @@ namespace ts.server { let mapFileNameFromDeclarationInfo: string | undefined; let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => { - const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.fshost); + const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host); if (!mapInfo) { mapFileNameFromDeclarationInfo = mapFileNameFromDts; return undefined; @@ -2948,7 +3007,7 @@ namespace ts.server { } // Need to look for other files. - const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.fshost); + const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host); if (!info) return undefined; // Attach as source @@ -3069,7 +3128,7 @@ namespace ts.server { if (this.openFiles.has(info.path)) return; // Skip open files if (!info.fileWatcher) return; // not watched file // Handle as if file is changed or deleted - this.onSourceFileChanged(info, this.fshost.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); + this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); }); // Cancel all project updates since we will be updating them now this.pendingProjectUpdates.forEach((_project, projectName) => { @@ -3242,7 +3301,7 @@ namespace ts.server { const { fileName } = originalLocation; const scriptInfo = this.getScriptInfo(fileName); - if (!scriptInfo && !this.fshost.fileExists(fileName)) return undefined; + if (!scriptInfo && !this.host.fileExists(fileName)) return undefined; const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; const configFileName = this.getConfigFileNameForFile(originalFileInfo); @@ -3314,7 +3373,7 @@ namespace ts.server { /** @internal */ fileExists(fileName: NormalizedPath): boolean { - return !!this.getScriptInfoForNormalizedPath(fileName) || this.fshost.fileExists(fileName); + return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName); } private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined { @@ -3696,15 +3755,17 @@ namespace ts.server { /* @internal */ updateFileSystem(updatedFiles: protocol.FileSystemRequestArgs[] | undefined, deletedFiles?: string[]) { - Debug.assert(this.fshost instanceof VirtualFS.VirtualServerHost); + const fshost = this.host as unknown as FileServerHost; if (updatedFiles) { + Debug.assert(fshost.ensureFileOrFolder) for (const { file, fileContent } of updatedFiles) { - this.fshost.ensureFileOrFolder({ path: file, content: fileContent }); + fshost.ensureFileOrFolder({ path: file, content: fileContent }); } } if (deletedFiles) { + Debug.assert(fshost.deleteFile) for (const file of deletedFiles) { - this.fshost.deleteFile(file, /*deleteEmptyParentFolders*/ true); + fshost.deleteFile(file, /*deleteEmptyParentFolders*/ true); } } } @@ -3970,7 +4031,7 @@ namespace ts.server { for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); if (getBaseConfigFileName(normalized)) { - if (this.serverMode === LanguageServiceMode.Semantic && this.fshost.fileExists(normalized)) { + if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } } @@ -4125,7 +4186,7 @@ namespace ts.server { case Ternary.True: return directory; case Ternary.False: return undefined; case Ternary.Maybe: - return this.fshost.fileExists(combinePaths(directory, "package.json")) + return this.host.fileExists(combinePaths(directory, "package.json")) ? directory : undefined; } diff --git a/src/server/packageJsonCache.ts b/src/server/packageJsonCache.ts index 06a71da2041..3aa6129430a 100644 --- a/src/server/packageJsonCache.ts +++ b/src/server/packageJsonCache.ts @@ -42,7 +42,7 @@ namespace ts.server { }; function addOrUpdate(fileName: Path) { - const packageJsonInfo = Debug.checkDefined(createPackageJsonInfo(fileName, host.fshost)); + const packageJsonInfo = Debug.checkDefined(createPackageJsonInfo(fileName, host.host)); packageJsons.set(fileName, packageJsonInfo); directoriesWithoutPackageJson.delete(getDirectoryPath(fileName)); } diff --git a/src/server/project.ts b/src/server/project.ts index 91220bde33c..f6f048ca673 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -85,7 +85,6 @@ namespace ts.server { languageService: LanguageService; languageServiceHost: LanguageServiceHost; serverHost: ServerHost; - fsHost: FileServerHost; session?: Session; config: any; } @@ -234,8 +233,8 @@ namespace ts.server { return hasOneOrMoreJsAndNoTsFiles(this); } - public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, fshost: FileServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined { - const resolvedPath = normalizeSlashes(fshost.resolvePath(combinePaths(initialDir, "node_modules"))); + public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined { + const resolvedPath = normalizeSlashes(host.resolvePath(combinePaths(initialDir, "node_modules"))); log(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`); const result = host.require!(resolvedPath, moduleName); // TODO: GH#18217 if (result.error) { @@ -323,7 +322,7 @@ namespace ts.server { else if (host.trace) { this.trace = s => host.trace!(s); } - this.realpath = maybeBind(this.projectService.fshost, this.projectService.fshost.realpath); + this.realpath = maybeBind(this.projectService.host, this.projectService.host.realpath); // Use the current directory as resolution root only if the project created using current directory string this.resolutionCache = createResolutionCache( @@ -455,7 +454,7 @@ namespace ts.server { } useCaseSensitiveFileNames() { - return this.projectService.fshost.useCaseSensitiveFileNames; + return this.projectService.host.useCaseSensitiveFileNames; } readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { @@ -463,11 +462,11 @@ namespace ts.server { } readFile(fileName: string): string | undefined { - return this.projectService.fshost.readFile(fileName); + return this.projectService.host.readFile(fileName); } writeFile(fileName: string, content: string): void { - return this.projectService.fshost.writeFile(fileName, content); + return this.projectService.host.writeFile(fileName, content); } fileExists(file: string): boolean { @@ -1608,7 +1607,7 @@ namespace ts.server { (errorLogs || (errorLogs = [])).push(message); }; const resolvedModule = firstDefined(searchPaths, searchPath => - Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, this.projectService.fshost, log, logError) as PluginModuleFactory | undefined); + Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError) as PluginModuleFactory | undefined); if (resolvedModule) { const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name); if (configurationOverride) { @@ -1638,7 +1637,6 @@ namespace ts.server { languageService: this.languageService, languageServiceHost: this, serverHost: this.projectService.host, - fsHost: this.projectService.fshost, session: this.projectService.session }; @@ -1729,23 +1727,23 @@ namespace ts.server { return { fileExists: this.program.fileExists, directoryExists: this.program.directoryExists, - realpath: this.program.realpath || this.projectService.fshost.realpath?.bind(this.projectService.fshost), + realpath: this.program.realpath || this.projectService.host.realpath?.bind(this.projectService.host), getCurrentDirectory: this.getCurrentDirectory.bind(this), - readFile: this.projectService.fshost.readFile.bind(this.projectService.fshost), - getDirectories: this.projectService.fshost.getDirectories.bind(this.projectService.fshost), + readFile: this.projectService.host.readFile.bind(this.projectService.host), + getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host), trace: this.projectService.host.trace?.bind(this.projectService.host), useCaseSensitiveFileNames: this.program.useCaseSensitiveFileNames(), }; } return { - fileExists: this.projectService.fshost.fileExists.bind(this.projectService.fshost), - directoryExists: this.projectService.fshost.directoryExists.bind(this.projectService.fshost), - realpath: this.projectService.fshost.realpath?.bind(this.projectService.fshost), - getCurrentDirectory: this.projectService.fshost.getCurrentDirectory.bind(this.projectService.fshost), - readFile: this.projectService.fshost.readFile.bind(this.projectService.fshost), - getDirectories: this.projectService.fshost.getDirectories.bind(this.projectService.fshost), + fileExists: this.projectService.host.fileExists.bind(this.projectService.host), + directoryExists: this.projectService.host.directoryExists.bind(this.projectService.host), + realpath: this.projectService.host.realpath?.bind(this.projectService.host), + getCurrentDirectory: this.projectService.host.getCurrentDirectory.bind(this.projectService.host), + readFile: this.projectService.host.readFile.bind(this.projectService.host), + getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host), trace: this.projectService.host.trace?.bind(this.projectService.host), - useCaseSensitiveFileNames: this.projectService.fshost.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: this.projectService.host.useCaseSensitiveFileNames, }; } @@ -1931,7 +1929,7 @@ namespace ts.server { compilerOptions, /*compileOnSaveEnabled*/ false, watchOptions, - projectService.fshost, + projectService.host, currentDirectory); this.typeAcquisition = typeAcquisition; this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath); @@ -1998,7 +1996,7 @@ namespace ts.server { compilerOptions, /*compileOnSaveEnabled*/ false, /*watchOptions*/ undefined, - projectService.fshost, + projectService.host, /*currentDirectory*/ undefined); } @@ -2185,7 +2183,7 @@ namespace ts.server { compilerOptions, /*compileOnSaveEnabled*/ false, hostProject.getWatchOptions(), - hostProject.projectService.fshost, + hostProject.projectService.host, hostProject.currentDirectory); this.rootFileNames = initialRootNames; @@ -2363,7 +2361,7 @@ namespace ts.server { // Ensure the config file existience info is cached let configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (!configFileExistenceInfo) { - this.projectService.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo = { exists: this.projectService.fshost.fileExists(configFileName) }); + this.projectService.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo = { exists: this.projectService.host.fileExists(configFileName) }); } // Ensure we have upto date parsed command line this.projectService.ensureParsedConfigUptoDate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, this); @@ -2616,7 +2614,7 @@ namespace ts.server { compilerOptions, compileOnSaveEnabled, watchOptions, - projectService.fshost, + projectService.host, getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName))); this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 692351bfcdd..dedd30fe19b 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -46,7 +46,7 @@ namespace ts.server { */ private pendingReloadFromDisk = false; - constructor(private readonly host: FileServerHost, private readonly info: ScriptInfo, initialVersion?: ScriptInfoVersion) { + constructor(private readonly host: ServerHost, private readonly info: ScriptInfo, initialVersion?: ScriptInfoVersion) { this.version = initialVersion || { svc: 0, text: 0 }; } @@ -330,7 +330,6 @@ namespace ts.server { constructor( private readonly host: ServerHost, - private readonly fshost: FileServerHost, readonly fileName: NormalizedPath, readonly scriptKind: ScriptKind, public readonly hasMixedContent: boolean, @@ -338,7 +337,7 @@ namespace ts.server { initialVersion?: ScriptInfoVersion) { this.isDynamic = isDynamicFileName(fileName); - this.textStorage = new TextStorage(fshost, this, initialVersion); + this.textStorage = new TextStorage(host, this, initialVersion); if (hasMixedContent || this.isDynamic) { this.textStorage.reload(""); this.realpath = this.path; @@ -396,10 +395,10 @@ namespace ts.server { if (this.realpath === undefined) { // Default is just the path this.realpath = this.path; - if (this.fshost.realpath) { + if (this.host.realpath) { Debug.assert(!!this.containingProjects.length); const project = this.containingProjects[0]; - const realpath = this.fshost.realpath(this.path); + const realpath = this.host.realpath(this.path); if (realpath) { this.realpath = project.toPath(realpath); // If it is different from this.path, add to the map @@ -577,7 +576,7 @@ namespace ts.server { } saveTo(fileName: string) { - this.fshost.writeFile(fileName, getSnapshotText(this.textStorage.getSnapshot())); + this.host.writeFile(fileName, getSnapshotText(this.textStorage.getSnapshot())); } /*@internal*/ diff --git a/src/server/session.ts b/src/server/session.ts index f58e104b314..a2481d9cb2b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -782,11 +782,9 @@ namespace ts.server { private suppressDiagnosticEvents?: boolean; private eventHandler: ProjectServiceEventHandler | undefined; private readonly noGetErrOnBackgroundUpdate?: boolean; - private fshost: FileServerHost; constructor(opts: SessionOptions) { - this.host = opts.host; - this.fshost = opts.fshost || this.host; + this.host = getFileSystemHost(opts.host, opts.fshost); this.cancellationToken = opts.cancellationToken; this.typingsInstaller = opts.typingsInstaller; this.byteLength = opts.byteLength; @@ -2205,7 +2203,7 @@ namespace ts.server { return args.richResponse ? { emitSkipped: true, diagnostics: [] } : false; } const scriptInfo = project.getScriptInfo(file)!; - const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.fshost.writeFile(path, data, writeByteOrderMark)); + const { emitSkipped, diagnostics } = project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); return args.richResponse ? { emitSkipped, @@ -2905,7 +2903,7 @@ namespace ts.server { } getCanonicalFileName(fileName: string) { - const name = this.fshost.useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName); + const name = this.host.useCaseSensitiveFileNames ? fileName : toFileNameLowerCase(fileName); return normalizePath(name); } diff --git a/src/server/types.ts b/src/server/types.ts index 6fb1ec6355b..0448d0c62a7 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -17,10 +17,18 @@ declare namespace ts.server { trace?(s: string): void; require?(initialPath: string, moduleName: string): RequireResult; } + export type FileServerHost = Pick< ServerHost, | "readFile" | "writeFile" | "fileExists" | "directoryExists" | "getFileSize" | "getModifiedTime" | "getDirectories" | "getCurrentDirectory" | "getExecutingFilePath" | "realpath" | "resolvePath" - | "createDirectory" | "setModifiedTime" | "deleteFile" | "readDirectory" | "watchFile" | "watchDirectory" - | "useCaseSensitiveFileNames">; + | "createDirectory" | "setModifiedTime" | "readDirectory" | "watchFile" | "watchDirectory" + | "useCaseSensitiveFileNames"> & { + deleteFile(path: string, deleteEmptyParentFolders?: boolean): void; + ensureFileOrFolder( + fileOrDirectoryOrSymLink: { path: string } & ({ content: string, fileSize?: number } | {} | { symLink: string }), + ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, + ignoreParentWatch?: boolean + ): void + }; } diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 7a6937a20c5..3bcb5789daa 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -66,7 +66,7 @@ namespace ts.projectSystem { const proxy = Harness.LanguageService.makeDefaultProxy(info); proxy.getSemanticDiagnostics = filename => { const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: SourceFile = info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.fsHost.useCaseSensitiveFileNames)))!; + const sourceFile: SourceFile = info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; prev.push({ category: DiagnosticCategory.Warning, file: sourceFile, diff --git a/src/testRunner/unittests/tsserver/webServer.ts b/src/testRunner/unittests/tsserver/webServer.ts index 708ad4f7036..9f4a08b6b6e 100644 --- a/src/testRunner/unittests/tsserver/webServer.ts +++ b/src/testRunner/unittests/tsserver/webServer.ts @@ -1,7 +1,7 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: webServer", () => { class TestWorkerSession extends server.WorkerSession { - constructor(host: server.ServerHost, fshost: server.FileServerHost, webHost: server.HostWithWriteMessage, options: Partial, logger: server.Logger) { + constructor(host: server.ServerHost, fshost: server.FileServerHost | undefined, webHost: server.HostWithWriteMessage, options: Partial, logger: server.Logger) { super( host, fshost, @@ -37,12 +37,12 @@ namespace ts.projectSystem { writeMessage: s => messages.push(s), }; const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath()); - let fshost = webSys; let serverMode = LanguageServiceMode.PartialSemantic; + let fshost: VirtualFS.VirtualServerHost | undefined; if (isVfs) { + serverMode = LanguageServiceMode.Semantic; fshost = VirtualFS.createVirtualServerHost({ executingFilePath: "/a/lib/tsc.js" }); (fshost as VirtualFS.VirtualServerHost).ensureFileOrFolder(libFile); - serverMode = LanguageServiceMode.Semantic; } const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger(); const session = new TestWorkerSession(webSys, fshost, webHost, { serverMode }, logger); diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index 95f34b0a898..c995aa5ed00 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -28,7 +28,7 @@ namespace ts.server { cancellationToken: ServerCancellationToken; serverMode: LanguageServiceMode | undefined; unknownServerMode?: string; - startSession: (option: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fs: ServerHost) => void; + startSession: (option: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fs: FileServerHost | undefined) => void; } function start({ args, logger, cancellationToken, serverMode, unknownServerMode, startSession: startServer }: StartInput, platform: string) { const syntaxOnly = hasArgument("--syntaxOnly"); @@ -58,17 +58,11 @@ namespace ts.server { console.warn = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(", "), Msg.Err); console.error = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(", "), Msg.Err); - let fshost; - if (vfs) { - fshost = new VirtualFS.VirtualServerHost({ - useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames, - executingFilePath: directorySeparator, // Use same executingFilePath as webserver - newLine: sys.newLine, - }); - } - else { - fshost = sys as ServerHost; - } + const fshost = vfs ? new VirtualFS.VirtualServerHost({ + useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames, + executingFilePath: directorySeparator, // Use same executingFilePath as webserver + newLine: sys.newLine, + }) : undefined; startServer( { globalPlugins: findArgumentStringArray("--globalPlugins"), diff --git a/src/tsserver/webServer.ts b/src/tsserver/webServer.ts index c8bb314cc28..8c0483767ec 100644 --- a/src/tsserver/webServer.ts +++ b/src/tsserver/webServer.ts @@ -97,7 +97,7 @@ namespace ts.server { return [seconds, nanoseconds]; } - function startWebSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fshost: ServerHost) { + function startWebSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, fshost: FileServerHost | undefined) { class WorkerSession extends server.WorkerSession { constructor() { super(sys as ServerHost, fshost, { writeMessage }, options, logger, cancellationToken, hrtime); diff --git a/src/webServer/webServer.ts b/src/webServer/webServer.ts index adc3a10b8b2..7fe1a8cee7f 100644 --- a/src/webServer/webServer.ts +++ b/src/webServer/webServer.ts @@ -178,7 +178,7 @@ namespace ts.server { serverMode: SessionOptions["serverMode"]; } export class WorkerSession extends Session<{}> { - constructor(host: ServerHost, fshost: FileServerHost, private webHost: HostWithWriteMessage, options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, hrtime: SessionOptions["hrtime"]) { + constructor(host: ServerHost, fshost: FileServerHost | undefined, private webHost: HostWithWriteMessage, options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, hrtime: SessionOptions["hrtime"]) { super({ host, fshost, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index fd60f12c2da..f72d1afadd5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6985,7 +6985,16 @@ declare namespace ts.server { trace?(s: string): void; require?(initialPath: string, moduleName: string): RequireResult; } - type FileServerHost = Pick; + type FileServerHost = Pick & { + ensureFileOrFolder(fileOrDirectoryOrSymLink: { + path: string; + } & ({ + content: string; + fileSize?: number; + } | {} | { + symLink: string; + }), ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean): void; + }; } declare namespace ts.server { enum LogLevel { @@ -9917,7 +9926,6 @@ declare namespace ts.server { function isDynamicFileName(fileName: NormalizedPath): boolean; class ScriptInfo { private readonly host; - private readonly fshost; readonly fileName: NormalizedPath; readonly scriptKind: ScriptKind; readonly hasMixedContent: boolean; @@ -9929,7 +9937,7 @@ declare namespace ts.server { private formatSettings; private preferences; private textStorage; - constructor(host: ServerHost, fshost: FileServerHost, fileName: NormalizedPath, scriptKind: ScriptKind, hasMixedContent: boolean, path: Path, initialVersion?: ScriptInfoVersion); + constructor(host: ServerHost, fileName: NormalizedPath, scriptKind: ScriptKind, hasMixedContent: boolean, path: Path, initialVersion?: ScriptInfoVersion); isScriptOpen(): boolean; open(newText: string): void; close(fileExists?: boolean): void; @@ -9993,7 +10001,6 @@ declare namespace ts.server { languageService: LanguageService; languageServiceHost: LanguageServiceHost; serverHost: ServerHost; - fsHost: FileServerHost; session?: Session; config: any; } @@ -10058,7 +10065,7 @@ declare namespace ts.server { private readonly cancellationToken; isNonTsProject(): boolean; isJsOnlyProject(): boolean; - static resolveModule(moduleName: string, initialDir: string, host: ServerHost, fshost: FileServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined; + static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined; isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptions): Promise; private get typingsCache(); @@ -10444,7 +10451,6 @@ declare namespace ts.server { readonly currentDirectory: NormalizedPath; readonly toCanonicalFileName: (f: string) => string; readonly host: ServerHost; - readonly fshost: FileServerHost; readonly logger: Logger; readonly cancellationToken: HostCancellationToken; readonly useSingleInferredProject: boolean; @@ -10689,7 +10695,6 @@ declare namespace ts.server { private suppressDiagnosticEvents?; private eventHandler; private readonly noGetErrOnBackgroundUpdate?; - private fshost; constructor(opts: SessionOptions); private sendRequestCompletedEvent; private addPerformanceData;