diff --git a/vscode-ng-language-service/server/src/session.ts b/vscode-ng-language-service/server/src/session.ts index 4c4adff1a93..394998c4e62 100644 --- a/vscode-ng-language-service/server/src/session.ts +++ b/vscode-ng-language-service/server/src/session.ts @@ -535,7 +535,21 @@ export class Session { // If they are already part of a ConfiguredProject then the following is // not needed. if (!project || project.projectKind !== ts.server.ProjectKind.Configured) { - const {configFileName} = this.projectService.openClientFile(scriptInfo.fileName); + let {configFileName} = this.projectService.openClientFile(scriptInfo.fileName); + if (configFileName === undefined && isExternalTemplate(scriptInfo.fileName)) { + // When an external template loses its configured project after it was opened (e.g. + // because the corresponding component file was closed and the project graph was updated), + // the same best-effort used in `onDidOpenTextDocument` is needed here. + if (this.loadProjectForExternalTemplate(scriptInfo.fileName)) { + ({configFileName} = this.projectService.openClientFile(scriptInfo.fileName)); + } + if (configFileName === undefined) { + const componentProject = this.attachToComponentProject(scriptInfo); + if (componentProject) { + return componentProject; + } + } + } if (!configFileName) { // Failed to find a config file. There is nothing we could do. this.error(`No config file for ${scriptInfo.fileName}`); @@ -552,6 +566,49 @@ export class Session { return project; } + /** + * In a composite/solution-style project with references, TypeScript will _not_ open a project + * for an HTML file unless the file is explicitly included in the files/includes list. This is + * quite unlikely to be the case for HTML files. As a best-effort to fix this, we attempt to open + * a TS file with the same name so that its project is loaded. Most of the time, this is going to + * be the component file for the external template. + * https://github.com/angular/vscode-ng-language-service/issues/2149 + * + * Returns whether the component file was opened, i.e. whether the config lookup for the template + * is worth retrying. + */ + private loadProjectForExternalTemplate(templatePath: string): boolean { + const maybeComponentTsPath = componentPathForTemplate(templatePath); + if ( + this.projectService.openFiles.has(this.projectService.toPath(maybeComponentTsPath)) || + // Non-component HTML files (e.g. `src/index.html`) have no sibling `.ts`; skip the + // open/close so we don't walk directory trees looking for a config that can't exist. + !this.host.fileExists(maybeComponentTsPath) + ) { + return false; + } + this.projectService.openClientFile(maybeComponentTsPath); + this.projectService.closeClientFile(maybeComponentTsPath); + return true; + } + + /** + * Attaches an external template to the configured project of its component. Needed because + * `openClientFile` does not repeat the config lookup for a file that is already open, so it can + * still report no config file even though the component's project has since been loaded. + */ + private attachToComponentProject(scriptInfo: ts.server.ScriptInfo): ts.server.Project | null { + const componentProject = this.projectService + .getScriptInfo(componentPathForTemplate(scriptInfo.fileName)) + ?.containingProjects.find(isConfiguredProject); + if (!componentProject) { + return null; + } + scriptInfo.detachAllProjects(); + scriptInfo.attachToProject(componentProject); + return componentProject; + } + private onDidOpenTextDocument(params: lsp.DidOpenTextDocumentParams) { const {uri, languageId, text} = params.textDocument; const filePath = uriToFilePath(uri); @@ -568,19 +625,16 @@ export class Session { // buffer in the user's editor which has not been saved to disk. // See https://github.com/angular/vscode-ng-language-service/issues/632 let result = this.projectService.openClientFile(filePath, text, scriptKind); - // If the first opened file is an HTML file and the project is a composite/solution-style - // project with references, TypeScript will _not_ open a project unless the file is explicitly - // included in the files/includes list. This is quite unlikely to be the case for HTML files. - // As a best-effort to fix this, we attempt to open a TS file with the same name. Most of the - // time, this is going to be the component file for the external template. - // https://github.com/angular/vscode-ng-language-service/issues/2149 if (result.configFileName === undefined && languageId === LanguageId.HTML) { - const maybeComponentTsPath = filePath.replace(/\.html$/, '.ts'); - if (!this.projectService.openFiles.has(this.projectService.toPath(maybeComponentTsPath))) { - this.projectService.openClientFile(maybeComponentTsPath); - this.projectService.closeClientFile(maybeComponentTsPath); + if (this.loadProjectForExternalTemplate(filePath)) { result = this.projectService.openClientFile(filePath, text, scriptKind); } + if (result.configFileName === undefined) { + const scriptInfo = this.projectService.getScriptInfo(filePath); + if (scriptInfo) { + this.attachToComponentProject(scriptInfo); + } + } } const {configFileName, configFileErrors} = result; @@ -790,3 +844,7 @@ function isTypeScriptFile(path: string): boolean { function isExternalTemplate(path: string): boolean { return !isTypeScriptFile(path); } + +function componentPathForTemplate(templatePath: string): string { + return templatePath.replace(/\.html$/, '.ts'); +}