From da2622cc7bb00f593bd8b128d70ed8d4f49515a6 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Tue, 1 Mar 2022 09:56:48 -0800 Subject: [PATCH] initial scribbles --- src/server/editorServices.ts | 64 ++++++++- src/server/protocol.ts | 36 ++++- src/server/session.ts | 29 ++-- src/server/tsconfig.json | 3 +- .../tsserver/applyChangesToOpenFiles.ts | 135 ++++++++++++++++++ src/testRunner/unittests/tsserver/session.ts | 3 +- 6 files changed, 255 insertions(+), 15 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 149332bb6f6..6edfb6adb8a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1746,11 +1746,11 @@ namespace ts.server { if (configFileExistenceInfo) { const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile?.get(info.path); - // Delete the info from map, since this file is no more open + // Delete the info from map, since this file is not open anymore configFileExistenceInfo.openFilesImpactedByConfigFile?.delete(info.path); // If the script info was not root of inferred project, - // there wont be config file watch open because of this script info + // there won't be config file watch open because of this script info if (infoIsRootOfInferredProject) { // But if it is a root, it could be the last script info that is root of inferred project // and hence we would need to close the config file watcher @@ -3685,6 +3685,66 @@ namespace ts.server { return files; } + /* @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 + // It is also WEIRD that we use iterators here not arrays + // TODO: Probably copy what vfsUtil does to hook up a virtual filesystem here. Maybe. + // ugggggggggg have to copy over all that stuff to here + // (though I probably need to create a vfs project anyway, so why not) + + const fs = new vfs.FileSystem(/*ignoreCase*/ true, { + files: { + [builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), resolver), + [testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver), + [projectsFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/projects"), resolver), + [srcFolder]: {} + }, + cwd: srcFolder, + meta: { defaultLibLocation: builtFolder } + }) + if (!this.fs) + this.fs = fs + if (createdFiles) { + for (const document of Array.from(createdFiles)) { + fs.mkdirpSync(vpath.dirname(document.file)); + fs.writeFileSync(document.file, document.text, "utf8"); + fs.filemeta(document.file).set("document", document); + // Add symlinks + const symlink = document.meta.get("symlink"); + if (symlink) { + for (const link of symlink.split(",").map(link => link.trim())) { + fs.mkdirpSync(vpath.dirname(link)); + fs.symlinkSync(vpath.resolve(fs.cwd(), document.file), link); + } + } + } + } + + // 1. set some internal tsserver state for mocked FS (if it hasn't already been set, this might not be the first message) + // - change this.host at least + this.host // no, it's readonly, we get this from a parent object + // - ScriptInfo instances might need to update their host -- what is textStorage? + // - they deffo have FileWatcher instances + // - TextStorage.host needs to update + // 2. create + // 3. update + // 4. delete + // - closeClientFile -> closeOpenFile -> ScriptInfo.close + // 5. ??? + // 6. success! + if (createdFiles) { + } + if (updatedFiles) { + } + if (deletedFiles) { + for (const file of deletedFiles) { + this.closeClientFile(file) // maybe copy stuff from applyChanges -- mostly I just want to leave a pointer to more code + + } + } + } + /* @internal */ applyChangesInOpenFiles(openFiles: Iterator | undefined, changedFiles?: Iterator, closedFiles?: string[]): void { let openScriptInfos: ScriptInfo[] | undefined; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index ca8dbd1cac6..34cff9b8c29 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -96,6 +96,7 @@ namespace ts.server.protocol { /* @internal */ ApplyChangedToOpenFiles = "applyChangedToOpenFiles", UpdateOpen = "updateOpen", + UpdateFileSystem = "updateFileSystem", /* @internal */ EncodedSyntacticClassificationsFull = "encodedSyntacticClassifications-full", /* @internal */ @@ -154,7 +155,7 @@ namespace ts.server.protocol { PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls", - ProvideInlayHints = "provideInlayHints" + ProvideInlayHints = "provideInlayHints", // NOTE: If updating this, be sure to also update `allCommandNames` in `testRunner/unittests/tsserver/session.ts`. } @@ -1805,6 +1806,7 @@ namespace ts.server.protocol { /** * Request to synchronize list of open files with the client + * TODO: Lots of unit tests refer to this too, a good starting point for UpdateFileSystemRequest */ export interface UpdateOpenRequest extends Request { command: CommandTypes.UpdateOpen; @@ -1820,7 +1822,7 @@ namespace ts.server.protocol { */ openFiles?: OpenRequestArgs[]; /** - * List of open files files that were changes + * List of open files files that were changed */ changedFiles?: FileCodeEdits[]; /** @@ -1829,6 +1831,36 @@ namespace ts.server.protocol { closedFiles?: string[]; } + export interface UpdateFileSystemRequest extends Request { + command: CommandTypes.UpdateFileSystem; + arguments: UpdateFileSystemRequestArgs; + } + + export interface UpdateFileSystemRequestArgs { + /** For now, only 'memfs', initially for exclusive in-memory operation, but it could be other in-memory names later */ + fileSystem: string; + /** For now, a list of newly created or newly available files. Probably need to ADD mocked file watchers */ + created: FileSystemRequestArgs[]; + /** Just-deleted files. Also needs to trigger and then remove file watchers (I think) */ + deleted: string[]; + /** Needs to replace what file watchers would normally listen to */ + updated: FileSystemRequestArgs[]; + } + + export interface FileSystemRequestArgs extends FileRequestArgs { + /** + * Used to replace the content that would be on disk. + * Then the known content will be used upon opening instead of the disk copy + */ + fileContent?: string; + /** + * Used to specify the script kind of the file explicitly. It could be one of the following: + * "TS", "JS", "TSX", "JSX" + * TODO: Not 100% sure this is needed. + */ + scriptKindName?: ScriptKindName; + } + /** * External projects have a typeAcquisition option so they need to be added separately to compiler options for inferred projects. */ diff --git a/src/server/session.ts b/src/server/session.ts index 25d9a6fe83b..550be274c94 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -714,6 +714,7 @@ namespace ts.server { export class Session implements EventSender { private readonly gcTimer: GcTimer; + // TODO: This will need to be replaced too protected projectService: ProjectService; private changeSeq = 0; @@ -722,6 +723,7 @@ namespace ts.server { private currentRequestId!: number; private errorCheck: MultistepOperation; + // TODO: Replace this one? protected host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; @@ -2643,16 +2645,16 @@ namespace ts.server { [CommandNames.UpdateOpen]: (request: protocol.UpdateOpenRequest) => { this.changeSeq++; this.projectService.applyChangesInOpenFiles( - request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), file => ({ - fileName: file.file, - content: file.fileContent, - scriptKind: file.scriptKindName, - projectRootPath: file.projectRootPath + request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), ({ file, fileContent, scriptKindName, projectRootPath }) => ({ + fileName: file, + content: fileContent, + scriptKind: scriptKindName, + projectRootPath })), - request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({ - fileName: file.fileName, - changes: mapDefinedIterator(arrayReverseIterator(file.textChanges), change => { - const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(file.fileName)); + request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), ({ fileName, textChanges}) => ({ + fileName, + changes: mapDefinedIterator(arrayReverseIterator(textChanges), change => { + const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(fileName)); const start = scriptInfo.lineOffsetToPosition(change.start.line, change.start.offset); const end = scriptInfo.lineOffsetToPosition(change.end.line, change.end.offset); return start >= 0 ? { span: { start, length: end - start }, newText: change.newText } : undefined; @@ -2676,6 +2678,15 @@ namespace ts.server { // TODO: report errors return this.requiredResponse(/*response*/ true); }, + [CommandNames.UpdateFileSystem]: (request: protocol.UpdateFileSystemRequest) => { + this.changeSeq++; + this.projectService.updateFileSystem( + request.arguments.created && arrayIterator(request.arguments.created), // open + request.arguments.updated && arrayIterator(request.arguments.updated), //change + request.arguments.deleted, // close + ); + return this.requiredResponse(/*response*/ true) + }, [CommandNames.Exit]: () => { this.exit(); return this.notRequired(); diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index b0a2f59ba77..d25b6c1b1dd 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -11,7 +11,8 @@ "references": [ { "path": "../compiler" }, { "path": "../jsTyping" }, - { "path": "../services" } + { "path": "../services" }, + { "path": "../vfs" } ], "files": [ "types.ts", diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 6d5ee91d68d..03ca5c29f74 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -1,4 +1,139 @@ namespace ts.projectSystem { + // TODO: Make a separate file at some point + describe("unittests:: tsserver:: updateFileSystem", () => { + interface Verify { + applyChangesToOpen: (session: TestSession) => void; + openFile1Again: (session: TestSession) => void; + } + const configFile: protocol.FileSystemRequestArgs = { + file: "/a/b/tsconfig.json", + fileContent: "{}" + }; + const file3: protocol.FileSystemRequestArgs = { + file: "/a/b/file3.ts", + fileContent: "export let xyz = 1;" + }; + const app: protocol.FileSystemRequestArgs = { + file: "/a/b/app.ts", + fileContent: "import { xyz } from './file3'; let x = xyz" + }; + function fileContentWithComment(file: protocol.FileSystemRequestArgs | File) { + return `// some copy right notice +${'content' in file ? file.content : file.fileContent}`; + } + function verify({ applyChangesToOpen, openFile1Again }: Verify) { + // TODO: Replace with createMemfsServerHost + const host = createServerHost([commonFile1, commonFile2, libFile]); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateFileSystem, + arguments:{ + fileSystem: 'memfs', + created: [configFile, file3, app], + deleted: [], // string[]; + updated: [], //FileSystemRequestArgs[]; + } + }); + const service = session.getProjectService(); + // session.host + const project = service.configuredProjects.get(configFile.file)!; + assert.isDefined(project); + verifyProjectVersion(project, 1); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: app + }); + verifyProjectVersion(project, 2); + + // TODO: Verify file watchers have updated + // Verify Texts + verifyText(service, commonFile1.path, commonFile1.content); + verifyText(service, commonFile2.path, commonFile2.content); + verifyText(service, app.file, app.fileContent!); + verifyText(service, file3.file, fileContentWithComment(file3)); + + // Apply changes + applyChangesToOpen(session); + + // Verify again + verifyProjectVersion(project, 3); + // Open file contents + verifyText(service, commonFile1.path, fileContentWithComment(commonFile1)); + verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); + verifyText(service, app.file, "let zzz = 10;let zz = 10;let z = 1;"); + verifyText(service, file3.file, file3.fileContent!); + + // Open file1 again + openFile1Again(session); + assert.isTrue(service.getScriptInfo(commonFile1.path)!.isScriptOpen()); + + // Verify that file1 contents are changed + verifyProjectVersion(project, 4); + verifyText(service, commonFile1.path, commonFile1.content); + verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); + verifyText(service, app.file, "let zzz = 10;let zz = 10;let z = 1;"); + verifyText(service, file3.file, file3.fileContent!); + } + + function verifyText(service: server.ProjectService, file: string, expected: string) { + const info = service.getScriptInfo(file)!; + const snap = info.getSnapshot(); + // Verified applied in reverse order + assert.equal(snap.getText(0, snap.getLength()), expected, `Text of changed file: ${file}`); + } + + function verifyProjectVersion(project: server.Project, expected: number) { + assert.equal(Number(project.getProjectVersion()), expected); + } + it("with updateOpen request", () => { + verify({ + applyChangesToOpen: session => session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [ + { + file: commonFile1.path, + fileContent: fileContentWithComment(commonFile1) + }, + { + file: commonFile2.path, + fileContent: fileContentWithComment(commonFile2) + } + ], + changedFiles: [ + { + fileName: app.file, + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zzz = 10;", + }, + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zz = 10;", + } + ] + } + ], + closedFiles: [ + file3.file + ] + } + }), + openFile1Again: session => session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [{ + file: commonFile1.path, + fileContent: commonFile1.content + }] + } + }), + }); + }); + }) describe("unittests:: tsserver:: applyChangesToOpenFiles", () => { const configFile: File = { path: "/a/b/tsconfig.json", diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 77daa4c813b..6e4c430f954 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -278,7 +278,8 @@ namespace ts.server { CommandNames.ToggleMultilineComment, CommandNames.CommentSelection, CommandNames.UncommentSelection, - CommandNames.ProvideInlayHints + CommandNames.ProvideInlayHints, + CommandNames.UpdateFileSystem, ]; it("should not throw when commands are executed with invalid arguments", () => {