Skip to content

Commit b400e2e

Browse files
ivanwonderalxhub
authored andcommitted
feat(language-service): autocompletion for the component not imported (#55595)
This PR allows the language service to suggest imports for all directives returned from the compiler, and generate the TypeScript module import and the decorator import when the component is selected by the user. PR Close #55595
1 parent 70157aa commit b400e2e

File tree

8 files changed

+256
-113
lines changed

8 files changed

+256
-113
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -752,22 +752,21 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
752752
tagMap.set(tag, null);
753753
}
754754

755-
const scope = this.getScopeData(component);
756-
if (scope !== null) {
757-
for (const directive of scope.directives) {
758-
if (directive.selector === null) {
759-
continue;
760-
}
755+
const potentialDirectives = this.getPotentialTemplateDirectives(component);
761756

762-
for (const selector of CssSelector.parse(directive.selector)) {
763-
if (selector.element === null || tagMap.has(selector.element)) {
764-
// Skip this directive if it doesn't match an element tag, or if another directive has
765-
// already been included with the same element name.
766-
continue;
767-
}
757+
for (const directive of potentialDirectives) {
758+
if (directive.selector === null) {
759+
continue;
760+
}
768761

769-
tagMap.set(selector.element, directive);
762+
for (const selector of CssSelector.parse(directive.selector)) {
763+
if (selector.element === null || tagMap.has(selector.element)) {
764+
// Skip this directive if it doesn't match an element tag, or if another directive has
765+
// already been included with the same element name.
766+
continue;
770767
}
768+
769+
tagMap.set(selector.element, directive);
771770
}
772771
}
773772

packages/language-service/src/codefixes/fix_missing_import.ts

Lines changed: 11 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ import {
1919
import ts from 'typescript';
2020

2121
import {getTargetAtPosition, TargetNodeKind} from '../template_target';
22-
import {
23-
standaloneTraitOrNgModule,
24-
updateImportsForAngularTrait,
25-
updateImportsForTypescriptFile,
26-
} from '../ts_utils';
22+
import {getCodeActionToImportTheDirectiveDeclaration, standaloneTraitOrNgModule} from '../ts_utils';
2723
import {getDirectiveMatchesForElementTag} from '../utils';
2824

2925
import {CodeActionContext, CodeActionMeta, FixIdForCodeFixesAll} from './utils';
@@ -90,68 +86,17 @@ function getCodeActions({
9086
return [];
9187
}
9288
for (const currMatch of matches.values()) {
93-
const currMatchSymbol = currMatch.tsSymbol.valueDeclaration!;
94-
const potentialImports = checker.getPotentialImportsFor(
95-
currMatch.ref,
96-
importOn,
97-
PotentialImportMode.Normal,
98-
);
99-
for (const potentialImport of potentialImports) {
100-
const fileImportChanges: ts.TextChange[] = [];
101-
let importName: string;
102-
let forwardRefName: string | null = null;
103-
104-
if (potentialImport.moduleSpecifier) {
105-
const [importChanges, generatedImportName] = updateImportsForTypescriptFile(
106-
tsChecker,
107-
importOn.getSourceFile(),
108-
potentialImport.symbolName,
109-
potentialImport.moduleSpecifier,
110-
currMatchSymbol.getSourceFile(),
111-
);
112-
importName = generatedImportName;
113-
fileImportChanges.push(...importChanges);
114-
} else {
115-
if (potentialImport.isForwardReference) {
116-
// Note that we pass the `importOn` file twice since we know that the potential import
117-
// is within the same file, because it doesn't have a `moduleSpecifier`.
118-
const [forwardRefImports, generatedForwardRefName] = updateImportsForTypescriptFile(
119-
tsChecker,
120-
importOn.getSourceFile(),
121-
'forwardRef',
122-
'@angular/core',
123-
importOn.getSourceFile(),
124-
);
125-
fileImportChanges.push(...forwardRefImports);
126-
forwardRefName = generatedForwardRefName;
127-
}
128-
importName = potentialImport.symbolName;
129-
}
89+
const currentMatchCodeAction =
90+
getCodeActionToImportTheDirectiveDeclaration(compiler, importOn, currMatch) ?? [];
13091

131-
// Always update the trait import, although the TS import might already be present.
132-
const traitImportChanges = updateImportsForAngularTrait(
133-
checker,
134-
importOn,
135-
importName,
136-
forwardRefName,
137-
);
138-
if (traitImportChanges.length === 0) continue;
139-
140-
let description = `Import ${importName}`;
141-
if (potentialImport.moduleSpecifier !== undefined) {
142-
description += ` from '${potentialImport.moduleSpecifier}' on ${importOn.name!.text}`;
143-
}
144-
codeActions.push({
145-
fixName: FixIdForCodeFixesAll.FIX_MISSING_IMPORT,
146-
description,
147-
changes: [
148-
{
149-
fileName: importOn.getSourceFile().fileName,
150-
textChanges: [...fileImportChanges, ...traitImportChanges],
151-
},
152-
],
153-
});
154-
}
92+
codeActions.push(
93+
...currentMatchCodeAction.map<ts.CodeFixAction>((action) => {
94+
return {
95+
fixName: FixIdForCodeFixesAll.FIX_MISSING_IMPORT,
96+
...action,
97+
};
98+
}),
99+
);
155100
}
156101

157102
return codeActions;

packages/language-service/src/completions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
unsafeCastDisplayInfoKindToScriptElementKind,
6262
} from './display_parts';
6363
import {TargetContext, TargetNodeKind, TemplateTarget} from './template_target';
64+
import {getCodeActionToImportTheDirectiveDeclaration, standaloneTraitOrNgModule} from './ts_utils';
6465
import {filterAliasImports, isBoundEventWithSyntheticHandler, isWithin} from './utils';
6566

6667
type PropertyExpressionCompletionBuilder = CompletionBuilder<
@@ -721,6 +722,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
721722
name: tag,
722723
sortText: tag,
723724
replacementSpan,
725+
hasAction: directive?.isInScope === true ? undefined : true,
724726
}));
725727

726728
return {
@@ -755,13 +757,25 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
755757
tags = displayInfo.tags;
756758
}
757759

760+
let codeActions: ts.CodeAction[] | undefined;
761+
762+
if (!directive.isInScope) {
763+
const importOn = standaloneTraitOrNgModule(templateTypeChecker, this.component);
764+
765+
codeActions =
766+
importOn !== null
767+
? getCodeActionToImportTheDirectiveDeclaration(this.compiler, importOn, directive)
768+
: undefined;
769+
}
770+
758771
return {
759772
kind: tagCompletionKind(directive),
760773
name: entryName,
761774
kindModifiers: ts.ScriptElementKindModifier.none,
762775
displayParts,
763776
documentation,
764777
tags,
778+
codeActions,
765779
};
766780
}
767781

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
/**
12+
* Try to guess the indentation of the node.
13+
*
14+
* This function returns the indentation only if the start character of this node is
15+
* the first non-whitespace character in a line where the node is, otherwise,
16+
* it returns `undefined`. When computing the start of the node, it should include
17+
* the leading comments.
18+
*/
19+
export function guessIndentationInSingleLine(
20+
node: ts.Node,
21+
sourceFile: ts.SourceFile,
22+
): number | undefined {
23+
const leadingComment = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
24+
const firstLeadingComment =
25+
leadingComment !== undefined && leadingComment.length > 0 ? leadingComment[0] : undefined;
26+
const nodeStartWithComment = firstLeadingComment?.pos ?? node.getStart();
27+
const lineNumber = sourceFile.getLineAndCharacterOfPosition(nodeStartWithComment).line;
28+
const lineStart = sourceFile.getLineStarts()[lineNumber];
29+
30+
let haveChar = false;
31+
for (let pos = lineStart; pos < nodeStartWithComment; pos++) {
32+
const ch = sourceFile.text.charCodeAt(pos);
33+
if (!ts.isWhiteSpaceSingleLine(ch)) {
34+
haveChar = true;
35+
break;
36+
}
37+
}
38+
return haveChar ? undefined : nodeStartWithComment - lineStart;
39+
}

0 commit comments

Comments
 (0)