angular-cn/packages/language-service/ivy/language_service.ts

183 lines
7.3 KiB
TypeScript

/**
* @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 {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript/lib/tsserverlibrary';
import {DefinitionBuilder} from './definitions';
import {isExternalTemplate, isTypeScriptFile, LanguageServiceAdapter} from './language_service_adapter';
import {QuickInfoBuilder} from './quick_info';
export class LanguageService {
private options: CompilerOptions;
private lastKnownProgram: ts.Program|null = null;
private readonly strategy: TypeCheckingProgramStrategy;
private readonly adapter: LanguageServiceAdapter;
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
this.options = parseNgCompilerOptions(project);
this.strategy = createTypeCheckingProgramStrategy(project);
this.adapter = new LanguageServiceAdapter(project);
this.watchConfigFile(project);
}
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
const program = this.strategy.getProgram();
const compiler = this.createCompiler(program, fileName);
const ttc = compiler.getTemplateTypeChecker();
const diagnostics: ts.Diagnostic[] = [];
if (isTypeScriptFile(fileName)) {
const sourceFile = program.getSourceFile(fileName);
if (sourceFile) {
diagnostics.push(...ttc.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile));
}
} else {
const components = compiler.getComponentsWithTemplateFile(fileName);
for (const component of components) {
if (ts.isClassDeclaration(component)) {
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
}
}
}
this.lastKnownProgram = compiler.getNextProgram();
return diagnostics;
}
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|undefined {
const program = this.strategy.getProgram();
const compiler = this.createCompiler(program, fileName);
return new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position);
}
getTypeDefinitionAtPosition(fileName: string, position: number):
readonly ts.DefinitionInfo[]|undefined {
const program = this.strategy.getProgram();
const compiler = this.createCompiler(program, fileName);
return new DefinitionBuilder(this.tsLS, compiler)
.getTypeDefinitionsAtPosition(fileName, position);
}
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
const program = this.strategy.getProgram();
const compiler = this.createCompiler(program, fileName);
return new QuickInfoBuilder(this.tsLS, compiler).get(fileName, position);
}
/**
* Create a new instance of Ivy compiler.
* If the specified `fileName` refers to an external template, check if it has
* changed since the last time it was read. If it has changed, signal the
* compiler to reload the file via the adapter.
*/
private createCompiler(program: ts.Program, fileName: string): NgCompiler {
if (isExternalTemplate(fileName)) {
this.adapter.registerTemplateUpdate(fileName);
}
return new NgCompiler(
this.adapter,
this.options,
program,
this.strategy,
new PatchedProgramIncrementalBuildStrategy(),
/** enableTemplateTypeChecker */ true,
this.lastKnownProgram,
/** perfRecorder (use default) */ undefined,
);
}
private watchConfigFile(project: ts.server.Project) {
// TODO: Check the case when the project is disposed. An InferredProject
// could be disposed when a tsconfig.json is added to the workspace,
// in which case it becomes a ConfiguredProject (or vice-versa).
// We need to make sure that the FileWatcher is closed.
if (!(project instanceof ts.server.ConfiguredProject)) {
return;
}
const {host} = project.projectService;
host.watchFile(
project.getConfigFilePath(), (fileName: string, eventKind: ts.FileWatcherEventKind) => {
project.log(`Config file changed: ${fileName}`);
if (eventKind === ts.FileWatcherEventKind.Changed) {
this.options = parseNgCompilerOptions(project);
}
});
}
}
export function parseNgCompilerOptions(project: ts.server.Project): CompilerOptions {
let config = {};
if (project instanceof ts.server.ConfiguredProject) {
const configPath = project.getConfigFilePath();
const result = ts.readConfigFile(configPath, path => project.readFile(path));
if (result.error) {
project.error(ts.flattenDiagnosticMessageText(result.error.messageText, '\n'));
}
config = result.config || config;
}
const basePath = project.getCurrentDirectory();
return createNgCompilerOptions(basePath, config, project.getCompilationSettings());
}
function createTypeCheckingProgramStrategy(project: ts.server.Project):
TypeCheckingProgramStrategy {
return {
supportsInlineOperations: false,
shimPathForComponent(component: ts.ClassDeclaration): AbsoluteFsPath {
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(component.getSourceFile()));
},
getProgram(): ts.Program {
const program = project.getLanguageService().getProgram();
if (!program) {
throw new Error('Language service does not have a program!');
}
return program;
},
updateFiles(contents: Map<AbsoluteFsPath, string>) {
for (const [fileName, newText] of contents) {
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
const snapshot = scriptInfo.getSnapshot();
const length = snapshot.getLength();
scriptInfo.editContent(0, length, newText);
}
},
};
}
function getOrCreateTypeCheckScriptInfo(
project: ts.server.Project, tcf: string): ts.server.ScriptInfo {
// First check if there is already a ScriptInfo for the tcf
const {projectService} = project;
let scriptInfo = projectService.getScriptInfo(tcf);
if (!scriptInfo) {
// ScriptInfo needs to be opened by client to be able to set its user-defined
// content. We must also provide file content, otherwise the service will
// attempt to fetch the content from disk and fail.
scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(
ts.server.toNormalizedPath(tcf),
true, // openedByClient
'', // fileContent
ts.ScriptKind.TS, // scriptKind
);
if (!scriptInfo) {
throw new Error(`Failed to create script info for ${tcf}`);
}
}
// Add ScriptInfo to project if it's missing. A ScriptInfo needs to be part of
// the project so that it becomes part of the program.
if (!project.containsScriptInfo(scriptInfo)) {
project.addRoot(scriptInfo);
}
return scriptInfo;
}