From 22c9470b69c6fed4cd8164f36c2c3ee871dbf555 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Wed, 30 Mar 2022 08:21:07 -0700 Subject: [PATCH] Add tsserver entrypoint for vfs --- src/server/editorServices.ts | 27 ++++------ .../tsserver/applyChangesToOpenFiles.ts | 44 +++++++---------- src/tsserver/server.ts | 10 ++++ src/tsserver/tsconfig.json | 1 + src/tsserver/webServer.ts | 49 +++++++++++++++++++ .../reference/api/tsserverlibrary.d.ts | 4 +- .../types.asyncGenerators.es2018.2.errors.txt | 4 +- 7 files changed, 93 insertions(+), 46 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 594fc7c2e07..c6cadda32d4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -757,8 +757,7 @@ namespace ts.server { readonly currentDirectory: NormalizedPath; readonly toCanonicalFileName: (f: string) => string; - public host: ServerHost; - public fs: ts.TestFSWithWatch.VirtualServerHost | undefined; + public readonly host: ServerHost; public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; @@ -3693,24 +3692,18 @@ namespace ts.server { /* @internal */ updateFileSystem(createdFiles: Iterator | undefined, updatedFiles?: Iterator, deletedFiles?: string[]) { // TODO: Maybe it is somehow gauche or verboten to use protocol types but the translation in applyChangesInOpenFiles seems stupid - // 1. set some internal tsserver state for mocked FS (if it hasn't already been set, this might not be the first message) - if (!this.fs) { - // -nervous laugh- - this.fs = ts.TestFSWithWatch.createVirtualServerHost([], { withSafeList: false }) - ;(this as any).host = this.fs - ;(this.session as any).host = this.fs - } // 2. update vfs // I THINK that only vfs needs to update, because none of these files should be open. // (I guess files could update from the filesystem while they are still open, but that's something to solve at the end of prototyping I think) + const fs = this.host as ts.TestFSWithWatch.VirtualServerHost; if (createdFiles) { let it while (!(it = createdFiles.next()).done) { const document = it.value if (document.fileContent) { - if (!this.fs.directoryExists(ts.getDirectoryPath(document.file))) - this.fs.createDirectory(ts.getDirectoryPath(document.file), /*recursive*/ true); - this.fs.writeFile(document.file, document.fileContent); + if (!fs.directoryExists(ts.getDirectoryPath(document.file))) + fs.createDirectory(ts.getDirectoryPath(document.file), /*recursive*/ true); + fs.writeFile(document.file, document.fileContent); } } } @@ -3719,12 +3712,12 @@ namespace ts.server { let it while (!(it = updatedFiles.next()).done) { if (it.value.fileContent) { - if (this.fs.fileExists(it.value.file)) { - this.fs.modifyFile(it.value.file, it.value.fileContent) + if (fs.fileExists(it.value.file)) { + fs.modifyFile(it.value.file, it.value.fileContent) } else { - this.fs.createDirectory(ts.getDirectoryPath(it.value.file), /*recursive*/ true); - this.fs.writeFile(it.value.file, it.value.fileContent); + fs.createDirectory(ts.getDirectoryPath(it.value.file), /*recursive*/ true); + fs.writeFile(it.value.file, it.value.fileContent); } } } @@ -3732,7 +3725,7 @@ namespace ts.server { if (deletedFiles) { // TODO: Probably want to delete empty parent folders while they are empty too for (const file of deletedFiles) { - this.fs.deleteFile(file) + fs.deleteFile(file) } } } diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 828fa445458..2b9cd6a9bce 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -45,7 +45,7 @@ ${'content' in file ? file.content : file.fileContent}`; // 1. make sure that everything in files is there assert.isDefined(host) const fs = (host as any).fs as ESMap - console.log(Array.from(fs.values() as any as Iterable).filter(ts.TestFSWithWatch.isFsFile)) + // console.log(Array.from(fs.values() as any as Iterable).filter(ts.TestFSWithWatch.isFsFile)) assert.equal(Array.from(fs.values() as any as Iterable).filter(ts.TestFSWithWatch.isFsFile).length, files.length) for (const { file, fileContent } of files) { assert(host?.fileExists(file)) @@ -54,11 +54,8 @@ ${'content' in file ? file.content : file.fileContent}`; // 2. make sure nothing else is } it("with updateFileSystem request", () => { - // TODO: Create a virtual host and make sure it works with session etc - // TODO: File watchers still seem wrong - // 5. send updateFS request with a create/update/delete of a watched file, assert that file watchers fired - // 6. probably some other watcher tests, not sure what - const host = createServerHost([]); // old path goes into virtualFileSystemWithWatch.ts, so I guess it's getting the old host, not the replaced one + // TODO: probably some other watcher tests, not sure what + const host = ts.TestFSWithWatch.createVirtualServerHost([], { withSafeList: false }); const session = createSession(host); const created = [app, file1, file2, file3, config, lib] session.executeCommandSeq({ @@ -79,8 +76,8 @@ ${'content' in file ? file.content : file.fileContent}`; const fakehost = (session as any).host as ts.TestFSWithWatch.VirtualServerHost assert.isDefined(project); assert.equal(fakehost.fsWatches.size, 0) - assert.equal(fakehost.fsWatchesRecursive.size, 0) - assert.equal(fakehost.watchedFiles.size, 0) + assert.equal(fakehost.fsWatchesRecursive.size, 2) + assert.equal(fakehost.watchedFiles.size, 5) verifyProjectVersion(project, 1); session.executeCommandSeq({ command: protocol.CommandTypes.Open, @@ -92,14 +89,14 @@ ${'content' in file ? file.content : file.fileContent}`; verifyProjectVersion(project, 2); // Verify Texts - verifyFileSystem(service.fs, created) + verifyFileSystem(host, created) verifyText(service, file1.file, file1.fileContent!); verifyText(service, commonFile2.path, commonFile2.content); verifyText(service, app.file, app.fileContent!); verifyText(service, file3.file, fileContentWithComment(file3)); assert.equal(fakehost.fsWatches.size, 0) - assert.equal(fakehost.fsWatchesRecursive.size, 0) - assert.equal(fakehost.watchedFiles.size, 0) + assert.equal(fakehost.fsWatchesRecursive.size, 2) + assert.equal(fakehost.watchedFiles.size, 4) session.executeCommandSeq({ command: protocol.CommandTypes.UpdateFileSystem, @@ -114,14 +111,14 @@ ${'content' in file ? file.content : file.fileContent}`; verifyProjectVersion(project, 2); // Verify Texts - verifyFileSystem(service.fs, created) + verifyFileSystem(host, created) verifyText(service, file1.file, file1.fileContent!); verifyText(service, commonFile2.path, commonFile2.content); verifyText(service, app.file, app.fileContent!); verifyText(service, file3.file, fileContentWithComment(file3)); assert.equal(fakehost.fsWatches.size, 0) - assert.equal(fakehost.fsWatchesRecursive.size, 0) - assert.equal(fakehost.watchedFiles.size, 0) + assert.equal(fakehost.fsWatchesRecursive.size, 2) + assert.equal(fakehost.watchedFiles.size, 4) session.executeCommandSeq({ command: protocol.CommandTypes.UpdateFileSystem, @@ -132,18 +129,16 @@ ${'content' in file ? file.content : file.fileContent}`; updated: [], //FileSystemRequestArgs[]; } }); - // also no change when deleting a file (changes session version but not project version?) - verifyProjectVersion(project, 2); + verifyProjectVersion(project, 3); // Verify Texts - verifyFileSystem(service.fs, [app, file2, file3, config, lib]) - verifyText(service, file1.file, file1.fileContent!); + verifyFileSystem(host, [app, file2, file3, config, lib]) verifyText(service, commonFile2.path, commonFile2.content); verifyText(service, app.file, app.fileContent!); verifyText(service, file3.file, fileContentWithComment(file3)); assert.equal(fakehost.fsWatches.size, 0) - assert.equal(fakehost.fsWatchesRecursive.size, 0) - assert.equal(fakehost.watchedFiles.size, 0) + assert.equal(fakehost.fsWatchesRecursive.size, 2) + assert.equal(fakehost.watchedFiles.size, 3) session.executeCommandSeq({ command: protocol.CommandTypes.Close, @@ -151,17 +146,16 @@ ${'content' in file ? file.content : file.fileContent}`; }); // also no change when closing a file??? (changes session version but not project version?) - verifyProjectVersion(project, 2); + verifyProjectVersion(project, 3); // Verify Texts - verifyFileSystem(service.fs, [app, file2, file3, config, lib]) - verifyText(service, file1.file, file1.fileContent!); + verifyFileSystem(host, [app, file2, file3, config, lib]) verifyText(service, commonFile2.path, commonFile2.content); verifyText(service, app.file, app.fileContent!); verifyText(service, file3.file, fileContentWithComment(file3)); assert.equal(fakehost.fsWatches.size, 0) - assert.equal(fakehost.fsWatchesRecursive.size, 0) - assert.equal(fakehost.watchedFiles.size, 0) + assert.equal(fakehost.fsWatchesRecursive.size, 2) + assert.equal(fakehost.watchedFiles.size, 4) }); function verifyText(service: server.ProjectService, file: string, expected: string) { diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index 4d9cbeb441c..084bea62e1d 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -79,6 +79,16 @@ namespace ts.server { if (typeof process !== "undefined") { start(initializeNodeSystem(), require("os").platform()); } + // TODO: Learn how to pass arguments to server + else if (findArgument("vfs")) { + // Get args from first message + const listener = (e: any) => { + removeEventListener("message", listener); + const args = e.data; + start(initializeVirtualFileSystem(args), "vfs"); + }; + addEventListener("message", listener); + } else { // Get args from first message const listener = (e: any) => { diff --git a/src/tsserver/tsconfig.json b/src/tsserver/tsconfig.json index 6643ecf2f1b..9ec2446de8f 100644 --- a/src/tsserver/tsconfig.json +++ b/src/tsserver/tsconfig.json @@ -18,6 +18,7 @@ { "path": "../jsTyping", "prepend": true }, { "path": "../server", "prepend": true }, { "path": "../webServer", "prepend": true }, + { "path": "../vfs", "prepend": true }, { "path": "../deprecatedCompat", "prepend": true } ] } diff --git a/src/tsserver/webServer.ts b/src/tsserver/webServer.ts index 704fd3d2887..86361b0611d 100644 --- a/src/tsserver/webServer.ts +++ b/src/tsserver/webServer.ts @@ -46,6 +46,18 @@ namespace ts.server { }; } + export function initializeVirtualFileSystem(args: string[]): StartInput { + createVirtualFileSystem(); + return { + args, + logger: createLogger(), + cancellationToken: nullCancellationToken, + // Only semantic mode right now + serverMode: LanguageServiceMode.Semantic, + startSession: startVirtualFileSystemSession + }; + } + function createLogger() { const cmdLineVerbosity = getLogLevel(findArgument("--logVerbosity")); return cmdLineVerbosity !== undefined ? new MainProcessLogger(cmdLineVerbosity, { writeMessage }) : nullLogger; @@ -81,6 +93,19 @@ namespace ts.server { } } + + function createVirtualFileSystem() { + Debug.assert(ts.sys === undefined); + // TODO: I don't think I need the XMLHttpRequest code since vfs will get its files from messages sent by the owner + // ...but this means I may not need the webSession-copied code in startVirtualFileSystemSession + const vfshost = ts.TestFSWithWatch.createVirtualServerHost([]) + setSys(vfshost); + const localeStr = findArgument("--locale"); + if (localeStr) { + validateLocaleAndSetLanguage(localeStr, sys); + } + } + function hrtime(previous?: [number, number]) { const now = self.performance.now() * 1e-3; let seconds = Math.floor(now); @@ -120,4 +145,28 @@ namespace ts.server { // Start listening session.listen(); } + + function startVirtualFileSystemSession(options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken) { + class WorkerSession extends server.WorkerSession { + constructor() { + super(sys as ServerHost, { writeMessage }, options, logger, cancellationToken, hrtime); + } + + exit() { + this.logger.info("Exiting..."); + this.projectService.closeLog(); + close(); + } + + listen() { + addEventListener("message", (message: any) => { + this.onMessage(message.data); + }); + } + } + const session = new WorkerSession(); + + // Start listening + session.listen(); + } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index edec6392836..6438dd8f44b 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -10496,7 +10496,7 @@ declare namespace ts.server { private ensureProjectForOpenFiles; /** * Open file whose contents is managed by the client - * @param filename is absolute pathname + * @param fileName is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult; @@ -10512,7 +10512,7 @@ declare namespace ts.server { private telemetryOnOpenFile; /** * Close file whose contents is managed by the client - * @param filename is absolute pathname + * @param uncheckedFileName is absolute pathname */ closeClientFile(uncheckedFileName: string): void; private collectChanges; diff --git a/tests/baselines/reference/types.asyncGenerators.es2018.2.errors.txt b/tests/baselines/reference/types.asyncGenerators.es2018.2.errors.txt index c2b79942f29..8811eac5e3e 100644 --- a/tests/baselines/reference/types.asyncGenerators.es2018.2.errors.txt +++ b/tests/baselines/reference/types.asyncGenerators.es2018.2.errors.txt @@ -168,13 +168,13 @@ tests/cases/conformance/types/asyncGenerators/types.asyncGenerators.es2018.2.ts( async function * explicitReturnType10(): IterableIterator { ~~~~~~~~~~~~~~~~~~~~~~~~ !!! error TS2741: Property '[Symbol.iterator]' is missing in type 'AsyncGenerator' but required in type 'IterableIterator'. -!!! related TS2728 /.ts/lib.es2015.iterable.d.ts:55:5: '[Symbol.iterator]' is declared here. +!!! related TS2728 /.ts/lib.es2015.iterable.d.ts:90:5: '[Symbol.iterator]' is declared here. yield 1; } async function * explicitReturnType11(): Iterable { ~~~~~~~~~~~~~~~~ !!! error TS2741: Property '[Symbol.iterator]' is missing in type 'AsyncGenerator' but required in type 'Iterable'. -!!! related TS2728 /.ts/lib.es2015.iterable.d.ts:51:5: '[Symbol.iterator]' is declared here. +!!! related TS2728 /.ts/lib.es2015.iterable.d.ts:82:5: '[Symbol.iterator]' is declared here. yield 1; } async function * explicitReturnType12(): Iterator {