perf(language-service): short-circuit LS operations (#40946)
When certain information is requested from the Angular Language Service, we know that there will be no additional Angular information if the requested position is not in an inline template, template url, or style url. To avoid unnecessary compiler compilations, we short circuit and return `undefined` before asking the compiler for any type of answer which would trigger a partial compilation, at the very least. fixes https://github.com/angular/vscode-ng-language-service/issues/1104 PR Close #40946
This commit is contained in:
parent
b84f719747
commit
f31a6015a0
|
@ -26,6 +26,7 @@ import {DefinitionBuilder} from './definitions';
|
||||||
import {QuickInfoBuilder} from './quick_info';
|
import {QuickInfoBuilder} from './quick_info';
|
||||||
import {ReferencesAndRenameBuilder} from './references';
|
import {ReferencesAndRenameBuilder} from './references';
|
||||||
import {getTargetAtPosition, TargetContext, TargetNodeKind} from './template_target';
|
import {getTargetAtPosition, TargetContext, TargetNodeKind} from './template_target';
|
||||||
|
import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
|
||||||
import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils';
|
import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils';
|
||||||
|
|
||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
|
@ -74,20 +75,24 @@ export class LanguageService {
|
||||||
|
|
||||||
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|
||||||
|undefined {
|
|undefined {
|
||||||
const compiler = this.compilerFactory.getOrCreate();
|
return this.withCompiler((compiler) => {
|
||||||
const results =
|
if (!isInAngularContext(compiler.getNextProgram(), fileName, position)) {
|
||||||
new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position);
|
return undefined;
|
||||||
this.compilerFactory.registerLastKnownProgram();
|
}
|
||||||
return results;
|
return new DefinitionBuilder(this.tsLS, compiler)
|
||||||
|
.getDefinitionAndBoundSpan(fileName, position);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypeDefinitionAtPosition(fileName: string, position: number):
|
getTypeDefinitionAtPosition(fileName: string, position: number):
|
||||||
readonly ts.DefinitionInfo[]|undefined {
|
readonly ts.DefinitionInfo[]|undefined {
|
||||||
const compiler = this.compilerFactory.getOrCreate();
|
return this.withCompiler((compiler) => {
|
||||||
const results =
|
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
|
||||||
new DefinitionBuilder(this.tsLS, compiler).getTypeDefinitionsAtPosition(fileName, position);
|
return undefined;
|
||||||
this.compilerFactory.registerLastKnownProgram();
|
}
|
||||||
return results;
|
return new DefinitionBuilder(this.tsLS, compiler)
|
||||||
|
.getTypeDefinitionsAtPosition(fileName, position);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
|
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
|
||||||
|
@ -169,37 +174,51 @@ export class LanguageService {
|
||||||
getCompletionsAtPosition(
|
getCompletionsAtPosition(
|
||||||
fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined):
|
fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions|undefined):
|
||||||
ts.WithMetadata<ts.CompletionInfo>|undefined {
|
ts.WithMetadata<ts.CompletionInfo>|undefined {
|
||||||
const builder = this.getCompletionBuilder(fileName, position);
|
return this.withCompiler((compiler) => {
|
||||||
if (builder === null) {
|
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result = builder.getCompletionsAtPosition(options);
|
|
||||||
this.compilerFactory.registerLastKnownProgram();
|
const builder = this.getCompletionBuilder(fileName, position);
|
||||||
return result;
|
if (builder === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return builder.getCompletionsAtPosition(options);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletionEntryDetails(
|
getCompletionEntryDetails(
|
||||||
fileName: string, position: number, entryName: string,
|
fileName: string, position: number, entryName: string,
|
||||||
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
formatOptions: ts.FormatCodeOptions|ts.FormatCodeSettings|undefined,
|
||||||
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
preferences: ts.UserPreferences|undefined): ts.CompletionEntryDetails|undefined {
|
||||||
const builder = this.getCompletionBuilder(fileName, position);
|
return this.withCompiler((compiler) => {
|
||||||
if (builder === null) {
|
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result = builder.getCompletionEntryDetails(entryName, formatOptions, preferences);
|
|
||||||
this.compilerFactory.registerLastKnownProgram();
|
const builder = this.getCompletionBuilder(fileName, position);
|
||||||
return result;
|
if (builder === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return builder.getCompletionEntryDetails(entryName, formatOptions, preferences);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): ts.Symbol
|
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): ts.Symbol
|
||||||
|undefined {
|
|undefined {
|
||||||
const builder = this.getCompletionBuilder(fileName, position);
|
return this.withCompiler((compiler) => {
|
||||||
if (builder === null) {
|
if (!isTemplateContext(compiler.getNextProgram(), fileName, position)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result = builder.getCompletionEntrySymbol(entryName);
|
|
||||||
this.compilerFactory.registerLastKnownProgram();
|
const builder = this.getCompletionBuilder(fileName, position);
|
||||||
return result;
|
if (builder === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const result = builder.getCompletionEntrySymbol(entryName);
|
||||||
|
this.compilerFactory.registerLastKnownProgram();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getComponentLocationsForTemplate(fileName: string): GetComponentLocationsForTemplateResponse {
|
getComponentLocationsForTemplate(fileName: string): GetComponentLocationsForTemplateResponse {
|
||||||
|
@ -432,3 +451,46 @@ function nodeContextFromTarget(target: TargetContext): CompletionNodeContext {
|
||||||
return CompletionNodeContext.None;
|
return CompletionNodeContext.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTemplateContext(program: ts.Program, fileName: string, position: number): boolean {
|
||||||
|
if (!isTypeScriptFile(fileName)) {
|
||||||
|
// If we aren't in a TS file, we must be in an HTML file, which we treat as template context
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = findTightestNodeAtPosition(program, fileName, position);
|
||||||
|
if (node === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let asgn = getPropertyAssignmentFromValue(node, 'template');
|
||||||
|
if (asgn === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getClassDeclFromDecoratorProp(asgn) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInAngularContext(program: ts.Program, fileName: string, position: number) {
|
||||||
|
if (!isTypeScriptFile(fileName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = findTightestNodeAtPosition(program, fileName, position);
|
||||||
|
if (node === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asgn = getPropertyAssignmentFromValue(node, 'template') ??
|
||||||
|
getPropertyAssignmentFromValue(node, 'templateUrl') ??
|
||||||
|
getPropertyAssignmentFromValue(node.parent, 'styleUrls');
|
||||||
|
return asgn !== null && getClassDeclFromDecoratorProp(asgn) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTightestNodeAtPosition(program: ts.Program, fileName: string, position: number) {
|
||||||
|
const sourceFile = program.getSourceFile(fileName);
|
||||||
|
if (sourceFile === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findTightestNode(sourceFile, position);
|
||||||
|
}
|
|
@ -27,4 +27,55 @@ export function getParentClassDeclaration(startNode: ts.Node): ts.ClassDeclarati
|
||||||
startNode = startNode.parent;
|
startNode = startNode.parent;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a property assignment from the assignment value if the property name
|
||||||
|
* matches the specified `key`, or `null` if there is no match.
|
||||||
|
*/
|
||||||
|
export function getPropertyAssignmentFromValue(value: ts.Node, key: string): ts.PropertyAssignment|
|
||||||
|
null {
|
||||||
|
const propAssignment = value.parent;
|
||||||
|
if (!propAssignment || !ts.isPropertyAssignment(propAssignment) ||
|
||||||
|
propAssignment.name.getText() !== key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return propAssignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
|
||||||
|
* directive class the property applies to.
|
||||||
|
* If the property assignment is not on a class decorator, no declaration is returned.
|
||||||
|
*
|
||||||
|
* For example,
|
||||||
|
*
|
||||||
|
* @Component({
|
||||||
|
* template: '<div></div>'
|
||||||
|
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
|
||||||
|
* })
|
||||||
|
* class AppComponent {}
|
||||||
|
* ^---- class declaration node
|
||||||
|
*
|
||||||
|
* @param propAsgnNode property assignment
|
||||||
|
*/
|
||||||
|
export function getClassDeclFromDecoratorProp(propAsgnNode: ts.PropertyAssignment):
|
||||||
|
ts.ClassDeclaration|undefined {
|
||||||
|
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const objLitExprNode = propAsgnNode.parent;
|
||||||
|
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callExprNode = objLitExprNode.parent;
|
||||||
|
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const decorator = callExprNode.parent;
|
||||||
|
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const classDeclNode = decorator.parent;
|
||||||
|
return classDeclNode;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue