From ca7a3af5e607a6c8589f5f600d6c4960a8bc07d4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 10 Nov 2023 12:45:39 -0800 Subject: [PATCH] Always update LS state through Project (#56356) Co-authored-by: Daniel Rosenwasser --- src/server/project.ts | 12 +- src/services/services.ts | 9 + src/services/types.ts | 3 + src/testRunner/unittests/tsserver/plugins.ts | 37 ++ ...LS-to-get-program-and-update-is-pending.js | 318 ++++++++++++++++++ 5 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/tsserver/plugins/when-plugins-use-LS-to-get-program-and-update-is-pending.js diff --git a/src/server/project.ts b/src/server/project.ts index 49aaeece0f4..d0cf5f2c321 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1330,6 +1330,14 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo this.hasAddedOrRemovedSymlinks = true; } + /** @internal */ + updateFromProjectInProgress = false; + + /** @internal */ + updateFromProject() { + updateProjectIfDirty(this); + } + /** * Updates set of files that contribute to this project * @returns: true if set of files in the project stays the same and false - otherwise. @@ -1523,8 +1531,10 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo this.hasInvalidatedResolutions = hasInvalidatedResolutions; this.hasInvalidatedLibResolutions = hasInvalidatedLibResolutions; this.resolutionCache.startCachingPerDirectoryResolution(); - this.program = this.languageService.getProgram(); // TODO: GH#18217 this.dirty = false; + this.updateFromProjectInProgress = true; + this.program = this.languageService.getProgram(); // TODO: GH#18217 + this.updateFromProjectInProgress = false; tracing?.push(tracing.Phase.Session, "finishCachingPerDirectoryResolution"); this.resolutionCache.finishCachingPerDirectoryResolution(this.program, oldProgram); tracing?.pop(); diff --git a/src/services/services.ts b/src/services/services.ts index 30eee5e120c..812eafd2c94 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1620,6 +1620,15 @@ export function createLanguageService( } function synchronizeHostData(): void { + if (host.updateFromProject && !host.updateFromProjectInProgress) { + host.updateFromProject(); + } + else { + synchronizeHostDataWorker(); + } + } + + function synchronizeHostDataWorker(): void { Debug.assert(languageServiceMode !== LanguageServiceMode.Syntactic); // perform fast check if host supports it if (host.getProjectVersion) { diff --git a/src/services/types.ts b/src/services/types.ts index c1d92f808dc..ef0315b557c 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -316,6 +316,9 @@ export interface IncompleteCompletionsCache { export interface LanguageServiceHost extends GetEffectiveTypeRootsHost, MinimalResolutionCacheHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; + /** @internal */ updateFromProject?(): void; + /** @internal */ updateFromProjectInProgress?: boolean; + getProjectVersion?(): string; getScriptFileNames(): string[]; getScriptKind?(fileName: string): ScriptKind; diff --git a/src/testRunner/unittests/tsserver/plugins.ts b/src/testRunner/unittests/tsserver/plugins.ts index 073c5b53b23..f984abc9377 100644 --- a/src/testRunner/unittests/tsserver/plugins.ts +++ b/src/testRunner/unittests/tsserver/plugins.ts @@ -28,6 +28,8 @@ describe("unittests:: tsserver:: plugins:: loading", () => { create(info: ts.server.PluginCreateInfo) { info.session?.addProtocolHandler(testProtocolCommand, request => { session.logger.log(`addProtocolHandler: ${jsonToReadableText(request)}`); + // Assume this one needs program + info.languageService.getProgram(); return { response: testProtocolCommandResponse, }; @@ -101,6 +103,41 @@ describe("unittests:: tsserver:: plugins:: loading", () => { baselineTsserverLogs("plugins", "With session and custom protocol message", session); }); + it("when plugins use LS to get program and update is pending", () => { + const pluginName = "some-plugin"; + const aTs: File = { + path: "/user/username/projects/project/a.ts", + content: `/// `, + }; + const tsconfig: File = { + path: "/user/username/projects/project/tsconfig.json", + content: jsonToReadableText({ + compilerOptions: { + plugins: [ + { name: pluginName }, + ], + }, + }), + }; + + const { session, host } = createHostWithPlugin([aTs, tsconfig, libFile]); + + openFilesForSession([aTs], session); + // Write the missing file (referenced by 'a.ts') to schedule an update. + host.writeFile("/user/username/projects/project/b.ts", "const y = 10;"); + + // This should update the language service with a new program. + session.executeCommandSeq({ + command: testProtocolCommand, + arguments: testProtocolCommandRequest, + }); + + // This results in a program update. + host.runQueuedTimeoutCallbacks(); + + baselineTsserverLogs("plugins", "when plugins use LS to get program and update is pending", session); + }); + it("gets external files with config file reload", () => { const aTs: File = { path: `/user/username/projects/myproject/a.ts`, content: `export const x = 10;` }; const tsconfig: File = { diff --git a/tests/baselines/reference/tsserver/plugins/when-plugins-use-LS-to-get-program-and-update-is-pending.js b/tests/baselines/reference/tsserver/plugins/when-plugins-use-LS-to-get-program-and-update-is-pending.js new file mode 100644 index 00000000000..24a0618c6f0 --- /dev/null +++ b/tests/baselines/reference/tsserver/plugins/when-plugins-use-LS-to-get-program-and-update-is-pending.js @@ -0,0 +1,318 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/typesMap.json" doesn't exist +Before request +//// [/user/username/projects/project/a.ts] +/// + +//// [/user/username/projects/project/tsconfig.json] +{ + "compilerOptions": { + "plugins": [ + { + "name": "some-plugin" + } + ] + } +} + +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/user/username/projects/project/a.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: /user/username/projects/project +Info seq [hh:mm:ss:mss] For info: /user/username/projects/project/a.ts :: Config file name: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/project/tsconfig.json 2000 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingStart", + "body": { + "projectName": "/user/username/projects/project/tsconfig.json", + "reason": "Creating possible configured project for /user/username/projects/project/a.ts to open" + } + } +Info seq [hh:mm:ss:mss] Config: /user/username/projects/project/tsconfig.json : { + "rootNames": [ + "/user/username/projects/project/a.ts" + ], + "options": { + "plugins": [ + { + "name": "some-plugin" + } + ], + "configFilePath": "/user/username/projects/project/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/project 1 undefined Config: /user/username/projects/project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/project 1 undefined Config: /user/username/projects/project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Enabling plugin some-plugin from candidate paths: /a/lib/tsc.js/../../.. +Info seq [hh:mm:ss:mss] Loading some-plugin from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules) +Loading plugin: some-plugin +Info seq [hh:mm:ss:mss] Plugin validation succeeded +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/project/b.ts 500 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/project/node_modules/@types 1 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/project/node_modules/@types 1 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /user/username/projects/project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/user/username/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + /a/lib/lib.d.ts Text-1 "/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }" + /user/username/projects/project/a.ts SVC-1-0 "/// " + + + ../../../../a/lib/lib.d.ts + Default library for target 'es5' + a.ts + Matched by default include pattern '**/*' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingFinish", + "body": { + "projectName": "/user/username/projects/project/tsconfig.json" + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "telemetry", + "body": { + "telemetryEventName": "projectInfo", + "payload": { + "projectId": "ff5803d884ff4e4485901596e00c181622d4efba4fec19a41fe48adf94ccdf94", + "fileStats": { + "js": 0, + "jsSize": 0, + "jsx": 0, + "jsxSize": 0, + "ts": 1, + "tsSize": 30, + "tsx": 0, + "tsxSize": 0, + "dts": 1, + "dtsSize": 334, + "deferred": 0, + "deferredSize": 0 + }, + "compilerOptions": { + "plugins": [ + "" + ] + }, + "typeAcquisition": { + "enable": false, + "include": false, + "exclude": false + }, + "extends": false, + "files": false, + "include": false, + "exclude": false, + "compileOnSave": false, + "configFileName": "tsconfig.json", + "projectType": "configured", + "languageServiceEnabled": true, + "version": "FakeVersion" + } + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "configFileDiag", + "body": { + "triggerFile": "/user/username/projects/project/a.ts", + "configFile": "/user/username/projects/project/tsconfig.json", + "diagnostics": [] + } + } +Info seq [hh:mm:ss:mss] Project '/user/username/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /user/username/projects/project/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/project/b.ts: *new* + {"pollingInterval":500} +/user/username/projects/project/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: *new* + {} +/user/username/projects/project/tsconfig.json: *new* + {} + +FsWatchesRecursive:: +/user/username/projects/project: *new* + {} + +Info seq [hh:mm:ss:mss] FileWatcher:: Triggered with /user/username/projects/project/b.ts 0:: WatchInfo: /user/username/projects/project/b.ts 500 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] FileWatcher:: Close:: WatchInfo: /user/username/projects/project/b.ts 500 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Scheduled: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] Scheduled: *ensureProjectForOpenFiles* +Info seq [hh:mm:ss:mss] Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/project/b.ts 0:: WatchInfo: /user/username/projects/project/b.ts 500 undefined Project: /user/username/projects/project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Triggered with /user/username/projects/project/b.ts :: WatchInfo: /user/username/projects/project 1 undefined Config: /user/username/projects/project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Scheduled: /user/username/projects/project/tsconfig.json, Cancelled earlier one +Info seq [hh:mm:ss:mss] Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/project/b.ts :: WatchInfo: /user/username/projects/project 1 undefined Config: /user/username/projects/project/tsconfig.json WatchType: Wild card directory +Before request +//// [/user/username/projects/project/b.ts] +const y = 10; + + +PolledWatches:: +/user/username/projects/node_modules/@types: + {"pollingInterval":500} +/user/username/projects/project/node_modules/@types: + {"pollingInterval":500} + +PolledWatches *deleted*:: +/user/username/projects/project/b.ts: + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: + {} +/user/username/projects/project/tsconfig.json: + {} + +FsWatchesRecursive:: +/user/username/projects/project: + {} + +Timeout callback:: count: 2 +3: /user/username/projects/project/tsconfig.json *new* +4: *ensureProjectForOpenFiles* *new* + +Info seq [hh:mm:ss:mss] request: + { + "command": "testProtocolCommand", + "arguments": "testProtocolCommandRequest", + "seq": 2, + "type": "request" + } +addProtocolHandler: { + "command": "testProtocolCommand", + "arguments": "testProtocolCommandRequest", + "seq": 2, + "type": "request" +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /user/username/projects/project/b.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /user/username/projects/project/tsconfig.json Version: 2 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/user/username/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + /a/lib/lib.d.ts Text-1 "/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }" + /user/username/projects/project/b.ts Text-1 "const y = 10;" + /user/username/projects/project/a.ts SVC-1-0 "/// " + + + ../../../../a/lib/lib.d.ts + Default library for target 'es5' + b.ts + Referenced via './b.ts' from file 'a.ts' + Matched by default include pattern '**/*' + a.ts + Matched by default include pattern '**/*' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] response: + { + "response": "testProtocolCommandResponse" + } +After request + +PolledWatches:: +/user/username/projects/node_modules/@types: + {"pollingInterval":500} +/user/username/projects/project/node_modules/@types: + {"pollingInterval":500} + +FsWatches:: +/a/lib/lib.d.ts: + {} +/user/username/projects/project/b.ts: *new* + {} +/user/username/projects/project/tsconfig.json: + {} + +FsWatchesRecursive:: +/user/username/projects/project: + {} + +Before running Timeout callback:: count: 2 +3: /user/username/projects/project/tsconfig.json +4: *ensureProjectForOpenFiles* + +Info seq [hh:mm:ss:mss] Running: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] Running: *ensureProjectForOpenFiles* +Info seq [hh:mm:ss:mss] Before ensureProjectForOpenFiles: +Info seq [hh:mm:ss:mss] Project '/user/username/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /user/username/projects/project/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] After ensureProjectForOpenFiles: +Info seq [hh:mm:ss:mss] Project '/user/username/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (3) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /user/username/projects/project/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /user/username/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] got projects updated in background /user/username/projects/project/a.ts +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectsUpdatedInBackground", + "body": { + "openFiles": [ + "/user/username/projects/project/a.ts" + ] + } + } +After running Timeout callback:: count: 0