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