diff --git a/packages/language-service/ivy/adapters.ts b/packages/language-service/ivy/adapters.ts index 28855dd210..f77ac2b2b8 100644 --- a/packages/language-service/ivy/adapters.ts +++ b/packages/language-service/ivy/adapters.ts @@ -25,7 +25,13 @@ export class LanguageServiceAdapter implements NgCompilerAdapter { readonly factoryTracker = null; // no .ngfactory shims readonly unifiedModulesHost = null; // only used in Bazel readonly rootDirs: AbsoluteFsPath[]; - private readonly templateVersion = new Map(); + + /** + * Map of resource filenames to the version of the file last read via `readResource`. + * + * Used to implement `getModifiedResourceFiles`. + */ + private readonly lastReadResourceVersion = new Map(); constructor(private readonly project: ts.server.Project) { this.rootDirs = getRootDirs(this, project.getCompilationSettings()); @@ -81,14 +87,18 @@ export class LanguageServiceAdapter implements NgCompilerAdapter { throw new Error(`Failed to get script snapshot while trying to read ${fileName}`); } const version = this.project.getScriptVersion(fileName); - this.templateVersion.set(fileName, version); + this.lastReadResourceVersion.set(fileName, version); return snapshot.getText(0, snapshot.getLength()); } - isTemplateDirty(fileName: string): boolean { - const lastVersion = this.templateVersion.get(fileName); - const latestVersion = this.project.getScriptVersion(fileName); - return lastVersion !== latestVersion; + getModifiedResourceFiles(): Set|undefined { + const modifiedFiles = new Set(); + for (const [fileName, oldVersion] of this.lastReadResourceVersion) { + if (this.project.getScriptVersion(fileName) !== oldVersion) { + modifiedFiles.add(fileName); + } + } + return modifiedFiles.size > 0 ? modifiedFiles : undefined; } } diff --git a/packages/language-service/ivy/compiler_factory.ts b/packages/language-service/ivy/compiler_factory.ts index fed921d857..d243f0b164 100644 --- a/packages/language-service/ivy/compiler_factory.ts +++ b/packages/language-service/ivy/compiler_factory.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; +import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; import {TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; @@ -37,53 +37,32 @@ export class CompilerFactory { getOrCreate(): NgCompiler { const program = this.programStrategy.getProgram(); - if (this.compiler === null || program !== this.lastKnownProgram) { - let ticket: CompilationTicket; - if (this.compiler === null || this.lastKnownProgram === null) { - ticket = freshCompilationTicket( - program, this.options, this.incrementalStrategy, this.programStrategy, true, true); - } else { - ticket = incrementalFromCompilerTicket( - this.compiler, program, this.incrementalStrategy, this.programStrategy, new Set()); + const modifiedResourceFiles = this.adapter.getModifiedResourceFiles() ?? new Set(); + + if (this.compiler !== null && program === this.lastKnownProgram) { + if (modifiedResourceFiles.size > 0) { + // Only resource files have changed since the last NgCompiler was created. + const ticket = resourceChangeTicket(this.compiler, modifiedResourceFiles); + this.compiler = NgCompiler.fromTicket(ticket, this.adapter); } - this.compiler = NgCompiler.fromTicket(ticket, this.adapter); - this.lastKnownProgram = program; + + return this.compiler; } + + let ticket: CompilationTicket; + if (this.compiler === null || this.lastKnownProgram === null) { + ticket = freshCompilationTicket( + program, this.options, this.incrementalStrategy, this.programStrategy, true, true); + } else { + ticket = incrementalFromCompilerTicket( + this.compiler, program, this.incrementalStrategy, this.programStrategy, + modifiedResourceFiles); + } + this.compiler = NgCompiler.fromTicket(ticket, this.adapter); + this.lastKnownProgram = program; return this.compiler; } - /** - * Create a new instance of the Ivy compiler if the program has changed since - * the last time the compiler was instantiated. If the program has not changed, - * return the existing instance. - * @param fileName override the template if this is an external template file - * @param options angular compiler options - */ - getOrCreateWithChangedFile(fileName: string): NgCompiler { - const compiler = this.getOrCreate(); - if (isExternalTemplate(fileName)) { - this.overrideTemplate(fileName, compiler); - } - return compiler; - } - - private overrideTemplate(fileName: string, compiler: NgCompiler) { - if (!this.adapter.isTemplateDirty(fileName)) { - return; - } - // 1. Get the latest snapshot - const latestTemplate = this.adapter.readResource(fileName); - // 2. Find all components that use the template - const ttc = compiler.getTemplateTypeChecker(); - const components = compiler.getComponentsWithTemplateFile(fileName); - // 3. Update component template - for (const component of components) { - if (ts.isClassDeclaration(component)) { - ttc.overrideComponentTemplate(component, latestTemplate); - } - } - } - registerLastKnownProgram() { this.lastKnownProgram = this.programStrategy.getProgram(); } diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 1895ad595d..1b3a199b48 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -67,7 +67,7 @@ export class LanguageService { } getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const ttc = compiler.getTemplateTypeChecker(); const diagnostics: ts.Diagnostic[] = []; if (isTypeScriptFile(fileName)) { @@ -90,7 +90,7 @@ export class LanguageService { getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan |undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const results = new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -99,7 +99,7 @@ export class LanguageService { getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[]|undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const results = new DefinitionBuilder(this.tsLS, compiler).getTypeDefinitionsAtPosition(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -107,7 +107,7 @@ export class LanguageService { } getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); if (templateInfo === undefined) { return undefined; @@ -129,7 +129,7 @@ export class LanguageService { } getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[]|undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) .getReferencesAtPosition(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -137,7 +137,7 @@ export class LanguageService { } getRenameInfo(fileName: string, position: number): ts.RenameInfo { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const renameInfo = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) .getRenameInfo(absoluteFrom(fileName), position); if (!renameInfo.canRename) { @@ -152,7 +152,7 @@ export class LanguageService { } findRenameLocations(fileName: string, position: number): readonly ts.RenameLocation[]|undefined { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler) .findRenameLocations(fileName, position); this.compilerFactory.registerLastKnownProgram(); @@ -161,7 +161,7 @@ export class LanguageService { private getCompletionBuilder(fileName: string, position: number): CompletionBuilder|null { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + const compiler = this.compilerFactory.getOrCreate(); const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); if (templateInfo === undefined) { return null; @@ -219,7 +219,7 @@ export class LanguageService { } getTcb(fileName: string, position: number): GetTcbResponse { - return this.withCompiler(fileName, compiler => { + return this.withCompiler(compiler => { const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler); if (templateInfo === undefined) { return undefined; @@ -263,8 +263,8 @@ export class LanguageService { }); } - private withCompiler(fileName: string, p: (compiler: NgCompiler) => T): T { - const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName); + private withCompiler(p: (compiler: NgCompiler) => T): T { + const compiler = this.compilerFactory.getOrCreate(); const result = p(compiler); this.compilerFactory.registerLastKnownProgram(); return result; diff --git a/packages/language-service/ivy/test/compiler_spec.ts b/packages/language-service/ivy/test/compiler_spec.ts index 980eafca62..87883fafc7 100644 --- a/packages/language-service/ivy/test/compiler_spec.ts +++ b/packages/language-service/ivy/test/compiler_spec.ts @@ -17,6 +17,38 @@ describe('language-service/compiler integration', () => { initMockFileSystem('Native'); }); + it('should react to a change in an external template', () => { + const cmpFile = absoluteFrom('/test.ts'); + const tmplFile = absoluteFrom('/test.html'); + + const env = LanguageServiceTestEnvironment.setup([ + { + name: cmpFile, + contents: ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + templateUrl: './test.html', + }) + export class TestCmp {} + `, + isRoot: true, + }, + { + name: tmplFile, + contents: 'Test', + }, + ]); + + const diags = env.ngLS.getSemanticDiagnostics(cmpFile); + expect(diags.length).toBeGreaterThan(0); + + env.updateFile(tmplFile, '
Test
'); + const afterDiags = env.ngLS.getSemanticDiagnostics(cmpFile); + expect(afterDiags.length).toBe(0); + }); + it('should not produce errors from inline test declarations mixing with those of the app', () => { const appCmpFile = absoluteFrom('/test.cmp.ts'); const appModuleFile = absoluteFrom('/test.mod.ts'); diff --git a/packages/language-service/ivy/test/completions_spec.ts b/packages/language-service/ivy/test/completions_spec.ts index be61b17aef..080abfbf9b 100644 --- a/packages/language-service/ivy/test/completions_spec.ts +++ b/packages/language-service/ivy/test/completions_spec.ts @@ -6,14 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {TmplAstNode} from '@angular/compiler'; import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import * as ts from 'typescript'; import {DisplayInfoKind, unsafeCastDisplayInfoKindToScriptElementKind} from '../display_parts'; import {LanguageService} from '../language_service'; -import {LanguageServiceTestEnvironment} from './env'; +import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; const DIR_WITH_INPUT = { 'Dir': ` @@ -640,7 +639,6 @@ function setup( AppCmp: ts.ClassDeclaration, ngLS: LanguageService, cursor: number, - nodes: TmplAstNode[], text: string, } { const codePath = absoluteFrom('/test.ts'); @@ -650,6 +648,7 @@ function setup( const otherDirectiveClassDecls = Object.values(otherDeclarations).join('\n\n'); + const {cursor, text: templateWithoutCursor} = extractCursorInfo(templateWithCursor); const env = LanguageServiceTestEnvironment.setup([ { name: codePath, @@ -675,18 +674,15 @@ function setup( }, { name: templatePath, - contents: 'Placeholder template', + contents: templateWithoutCursor, } ]); - const {nodes, cursor, text} = - env.overrideTemplateWithCursor(codePath, 'AppCmp', templateWithCursor); return { env, fileName: templatePath, AppCmp: env.getClass(codePath, 'AppCmp'), ngLS: env.ngLS, - nodes, - text, + text: templateWithoutCursor, cursor, }; } diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index 5b0cd19aaa..056bf1b2a9 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -27,8 +27,8 @@ describe('definitions', () => { }; const env = createModuleWithDeclarations([appFile], [templateFile]); - const {cursor} = env.overrideTemplateWithCursor( - absoluteFrom('/app.ts'), 'AppCmp', ' {{myIn¦put.value}}'); + const {cursor} = env.updateFileWithCursor( + absoluteFrom('/app.html'), ' {{myIn¦put.value}}'); env.expectNoSourceDiagnostics(); const {definitions} = env.ngLS.getDefinitionAndBoundSpan(absoluteFrom('/app.html'), cursor)!; expect(definitions![0].name).toEqual('myInput'); diff --git a/packages/language-service/ivy/test/env.ts b/packages/language-service/ivy/test/env.ts index 697432be9f..9da1e74836 100644 --- a/packages/language-service/ivy/test/env.ts +++ b/packages/language-service/ivy/test/env.ts @@ -46,13 +46,6 @@ function writeTsconfig( export type TestableOptions = StrictTemplateOptions; -export interface TemplateOverwriteResult { - cursor: number; - nodes: TmplAstNode[]; - component: ts.ClassDeclaration; - text: string; -} - export class LanguageServiceTestEnvironment { private constructor( readonly tsLS: ts.LanguageService, readonly ngLS: LanguageService, @@ -118,26 +111,16 @@ export class LanguageServiceTestEnvironment { return getClassOrError(sf, className); } - overrideTemplateWithCursor(fileName: AbsoluteFsPath, className: string, contents: string): - TemplateOverwriteResult { - const program = this.tsLS.getProgram(); - if (program === undefined) { - throw new Error(`Expected to get a ts.Program`); - } - const sf = getSourceFileOrError(program, fileName); - const component = getClassOrError(sf, className); - - const ngCompiler = this.ngLS.compilerFactory.getOrCreate(); - const templateTypeChecker = ngCompiler.getTemplateTypeChecker(); - + updateFileWithCursor(fileName: AbsoluteFsPath, contents: string): {cursor: number, text: string} { const {cursor, text} = extractCursorInfo(contents); - - const {nodes} = templateTypeChecker.overrideComponentTemplate(component, text); - return {cursor, nodes, component, text}; + this.updateFile(fileName, text); + return {cursor, text}; } updateFile(fileName: AbsoluteFsPath, contents: string): void { - const scriptInfo = this.projectService.getScriptInfo(fileName); + const normalFileName = ts.server.toNormalizedPath(fileName); + const scriptInfo = + this.projectService.getOrCreateScriptInfoForNormalizedPath(normalFileName, true, ''); if (scriptInfo === undefined) { throw new Error(`Could not find a file named ${fileName}`); } diff --git a/packages/language-service/ivy/test/legacy/adapters_spec.ts b/packages/language-service/ivy/test/legacy/adapters_spec.ts deleted file mode 100644 index 0b0a357d9d..0000000000 --- a/packages/language-service/ivy/test/legacy/adapters_spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import * as ts from 'typescript/lib/tsserverlibrary'; - -import {LanguageServiceAdapter} from '../../adapters'; - -import {MockService, setup, TEST_TEMPLATE} from './mock_host'; - -describe('Language service adapter', () => { - let project: ts.server.Project; - let service: MockService; - - beforeAll(() => { - const {project: _project, service: _service} = setup(); - project = _project; - service = _service; - }); - - it('should mark template dirty if it has not seen the template before', () => { - const adapter = new LanguageServiceAdapter(project); - expect(adapter.isTemplateDirty(TEST_TEMPLATE)).toBeTrue(); - }); - - it('should not mark template dirty if template has not changed', () => { - const adapter = new LanguageServiceAdapter(project); - adapter.readResource(TEST_TEMPLATE); - expect(adapter.isTemplateDirty(TEST_TEMPLATE)).toBeFalse(); - }); - - it('should mark template dirty if template has changed', () => { - const adapter = new LanguageServiceAdapter(project); - service.overwrite(TEST_TEMPLATE, '

Hello World

'); - expect(adapter.isTemplateDirty(TEST_TEMPLATE)).toBeTrue(); - }); -}); diff --git a/packages/language-service/ivy/test/quick_info_spec.ts b/packages/language-service/ivy/test/quick_info_spec.ts index 8a23f4b5a9..d7713bc047 100644 --- a/packages/language-service/ivy/test/quick_info_spec.ts +++ b/packages/language-service/ivy/test/quick_info_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import * as ts from 'typescript/lib/tsserverlibrary'; @@ -476,8 +476,8 @@ describe('quick info', () => { }); it('should provide documentation', () => { - const {cursor} = env.overrideTemplateWithCursor( - absoluteFrom('/app.ts'), 'AppCmp', `
{{¦title}}
`); + const {cursor} = + env.updateFileWithCursor(absoluteFrom('/app.html'), `
{{¦title}}
`); const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); const documentation = toText(quickInfo!.documentation); expect(documentation).toBe('This is the title of the `AppCmp` Component.'); @@ -522,8 +522,7 @@ describe('quick info', () => { {templateOverride, expectedSpanText, expectedDisplayString}: {templateOverride: string, expectedSpanText: string, expectedDisplayString: string}): ts.QuickInfo { - const {cursor, text} = - env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride); + const {cursor, text} = env.updateFileWithCursor(absoluteFrom('/app.html'), templateOverride); env.expectNoSourceDiagnostics(); env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp'); const quickInfo = env.ngLS.getQuickInfoAtPosition(absoluteFrom('/app.html'), cursor); diff --git a/packages/language-service/ivy/test/type_definitions_spec.ts b/packages/language-service/ivy/test/type_definitions_spec.ts index 5822586745..01ec679cad 100644 --- a/packages/language-service/ivy/test/type_definitions_spec.ts +++ b/packages/language-service/ivy/test/type_definitions_spec.ts @@ -49,8 +49,7 @@ describe('type definitions', () => { }); function getTypeDefinitionsAndAssertBoundSpan({templateOverride}: {templateOverride: string}) { - const {cursor, text} = - env.overrideTemplateWithCursor(absoluteFrom('/app.ts'), 'AppCmp', templateOverride); + const {cursor, text} = env.updateFileWithCursor(absoluteFrom('/app.html'), templateOverride); env.expectNoSourceDiagnostics(); env.expectNoTemplateDiagnostics(absoluteFrom('/app.ts'), 'AppCmp'); const defs = env.ngLS.getTypeDefinitionAtPosition(absoluteFrom('/app.html'), cursor);