234 lines
9.0 KiB
TypeScript
234 lines
9.0 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 {AST, TmplAstNode} from '@angular/compiler';
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
|
|
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
|
|
import * as ts from 'typescript';
|
|
|
|
import {convertToTemplateDocumentSpan, createLocationKey, getRenameTextAndSpanAtPosition, getTargetDetailsAtTemplatePosition} from './references_and_rename_utils';
|
|
import {findTightestNode} from './ts_utils';
|
|
import {getTemplateInfoAtPosition, TemplateInfo} from './utils';
|
|
|
|
|
|
enum RequestKind {
|
|
Template,
|
|
TypeScript,
|
|
}
|
|
|
|
interface TemplateRequest {
|
|
kind: RequestKind.Template;
|
|
requestNode: TmplAstNode|AST;
|
|
position: number;
|
|
}
|
|
|
|
interface TypeScriptRequest {
|
|
kind: RequestKind.TypeScript;
|
|
requestNode: ts.Node;
|
|
}
|
|
|
|
type RequestOrigin = TemplateRequest|TypeScriptRequest;
|
|
|
|
export class ReferencesAndRenameBuilder {
|
|
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
|
|
|
constructor(
|
|
private readonly driver: ProgramDriver, private readonly tsLS: ts.LanguageService,
|
|
private readonly compiler: NgCompiler) {}
|
|
|
|
getRenameInfo(filePath: string, position: number):
|
|
Omit<ts.RenameInfoSuccess, 'kind'|'kindModifiers'>|ts.RenameInfoFailure {
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
// We could not get a template at position so we assume the request came from outside the
|
|
// template.
|
|
if (templateInfo === undefined) {
|
|
return this.tsLS.getRenameInfo(filePath, position);
|
|
}
|
|
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return {
|
|
canRename: false,
|
|
localizedErrorMessage: 'Could not find template node at position.'
|
|
};
|
|
}
|
|
const {templateTarget} = allTargetDetails[0];
|
|
const templateTextAndSpan = getRenameTextAndSpanAtPosition(
|
|
templateTarget,
|
|
position,
|
|
);
|
|
if (templateTextAndSpan === null) {
|
|
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
|
|
}
|
|
const {text, span} = templateTextAndSpan;
|
|
return {
|
|
canRename: true,
|
|
displayName: text,
|
|
fullDisplayName: text,
|
|
triggerSpan: span,
|
|
};
|
|
});
|
|
}
|
|
|
|
findRenameLocations(filePath: string, position: number): readonly ts.RenameLocation[]|undefined {
|
|
this.ttc.generateAllTypeCheckBlocks();
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
// We could not get a template at position so we assume the request came from outside the
|
|
// template.
|
|
if (templateInfo === undefined) {
|
|
const requestNode = this.getTsNodeAtPosition(filePath, position);
|
|
if (requestNode === null) {
|
|
return undefined;
|
|
}
|
|
const requestOrigin: TypeScriptRequest = {kind: RequestKind.TypeScript, requestNode};
|
|
return this.findRenameLocationsAtTypescriptPosition(filePath, position, requestOrigin);
|
|
}
|
|
|
|
return this.findRenameLocationsAtTemplatePosition(templateInfo, position);
|
|
});
|
|
}
|
|
|
|
private findRenameLocationsAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
|
readonly ts.RenameLocation[]|undefined {
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return undefined;
|
|
}
|
|
|
|
const allRenameLocations: ts.RenameLocation[] = [];
|
|
for (const targetDetails of allTargetDetails) {
|
|
const requestOrigin: TemplateRequest = {
|
|
kind: RequestKind.Template,
|
|
requestNode: targetDetails.templateTarget,
|
|
position,
|
|
};
|
|
|
|
for (const location of targetDetails.typescriptLocations) {
|
|
const locations = this.findRenameLocationsAtTypescriptPosition(
|
|
location.fileName, location.position, requestOrigin);
|
|
// If we couldn't find rename locations for _any_ result, we should not allow renaming to
|
|
// proceed instead of having a partially complete rename.
|
|
if (locations === undefined) {
|
|
return undefined;
|
|
}
|
|
allRenameLocations.push(...locations);
|
|
}
|
|
}
|
|
return allRenameLocations.length > 0 ? allRenameLocations : undefined;
|
|
}
|
|
|
|
private getTsNodeAtPosition(filePath: string, position: number): ts.Node|null {
|
|
const sf = this.driver.getProgram().getSourceFile(filePath);
|
|
if (!sf) {
|
|
return null;
|
|
}
|
|
return findTightestNode(sf, position) ?? null;
|
|
}
|
|
|
|
findRenameLocationsAtTypescriptPosition(
|
|
filePath: string, position: number,
|
|
requestOrigin: RequestOrigin): readonly ts.RenameLocation[]|undefined {
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
let originalNodeText: string;
|
|
if (requestOrigin.kind === RequestKind.TypeScript) {
|
|
originalNodeText = requestOrigin.requestNode.getText();
|
|
} else {
|
|
const templateNodeText =
|
|
getRenameTextAndSpanAtPosition(requestOrigin.requestNode, requestOrigin.position);
|
|
if (templateNodeText === null) {
|
|
return undefined;
|
|
}
|
|
originalNodeText = templateNodeText.text;
|
|
}
|
|
|
|
const locations = this.tsLS.findRenameLocations(
|
|
filePath, position, /*findInStrings*/ false, /*findInComments*/ false);
|
|
if (locations === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const entries: Map<string, ts.RenameLocation> = new Map();
|
|
for (const location of locations) {
|
|
// TODO(atscott): Determine if a file is a shim file in a more robust way and make the API
|
|
// available in an appropriate location.
|
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) {
|
|
const entry = convertToTemplateDocumentSpan(
|
|
location, this.ttc, this.driver.getProgram(), originalNodeText);
|
|
// There is no template node whose text matches the original rename request. Bail on
|
|
// renaming completely rather than providing incomplete results.
|
|
if (entry === null) {
|
|
return undefined;
|
|
}
|
|
entries.set(createLocationKey(entry), entry);
|
|
} else {
|
|
// Ensure we only allow renaming a TS result with matching text
|
|
const refNode = this.getTsNodeAtPosition(location.fileName, location.textSpan.start);
|
|
if (refNode === null || refNode.getText() !== originalNodeText) {
|
|
return undefined;
|
|
}
|
|
entries.set(createLocationKey(location), location);
|
|
}
|
|
}
|
|
return Array.from(entries.values());
|
|
});
|
|
}
|
|
|
|
getReferencesAtPosition(filePath: string, position: number): ts.ReferenceEntry[]|undefined {
|
|
this.ttc.generateAllTypeCheckBlocks();
|
|
|
|
return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => {
|
|
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
|
if (templateInfo === undefined) {
|
|
return this.getReferencesAtTypescriptPosition(filePath, position);
|
|
}
|
|
return this.getReferencesAtTemplatePosition(templateInfo, position);
|
|
});
|
|
}
|
|
|
|
private getReferencesAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
|
ts.ReferenceEntry[]|undefined {
|
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
|
if (allTargetDetails === null) {
|
|
return undefined;
|
|
}
|
|
const allReferences: ts.ReferenceEntry[] = [];
|
|
for (const targetDetails of allTargetDetails) {
|
|
for (const location of targetDetails.typescriptLocations) {
|
|
const refs = this.getReferencesAtTypescriptPosition(location.fileName, location.position);
|
|
if (refs !== undefined) {
|
|
allReferences.push(...refs);
|
|
}
|
|
}
|
|
}
|
|
return allReferences.length > 0 ? allReferences : undefined;
|
|
}
|
|
|
|
private getReferencesAtTypescriptPosition(fileName: string, position: number):
|
|
ts.ReferenceEntry[]|undefined {
|
|
const refs = this.tsLS.getReferencesAtPosition(fileName, position);
|
|
if (refs === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const entries: Map<string, ts.ReferenceEntry> = new Map();
|
|
for (const ref of refs) {
|
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) {
|
|
const entry = convertToTemplateDocumentSpan(ref, this.ttc, this.driver.getProgram());
|
|
if (entry !== null) {
|
|
entries.set(createLocationKey(entry), entry);
|
|
}
|
|
} else {
|
|
entries.set(createLocationKey(ref), ref);
|
|
}
|
|
}
|
|
return Array.from(entries.values());
|
|
}
|
|
} |