Skip to content

Commit

Permalink
feat(language-service): autocomplete pipe binding expressions (#40032)
Browse files Browse the repository at this point in the history
This commit adds autocompletion for pipe expressions, built on existing APIs
for checking which pipes are in scope.

PR Close #40032
  • Loading branch information
alxhub committed Dec 14, 2020
1 parent 66378ed commit cbb6eae
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 6 deletions.
35 changes: 33 additions & 2 deletions packages/language-service/ivy/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import {AST, BindingPipe, EmptyExpr, ImplicitReceiver, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {CompletionKind, DirectiveInScope, TemplateDeclarationSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import {BoundEvent} from '@angular/compiler/src/render3/r3_ast';
Expand All @@ -23,6 +23,7 @@ type PropertyExpressionCompletionBuilder =
type ElementAttributeCompletionBuilder =
CompletionBuilder<TmplAstElement|TmplAstBoundAttribute|TmplAstTextAttribute|TmplAstBoundEvent>;

type PipeCompletionBuilder = CompletionBuilder<BindingPipe>;

export enum CompletionNodeContext {
None,
Expand Down Expand Up @@ -65,6 +66,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
return this.getElementTagCompletion();
} else if (this.isElementAttributeCompletion()) {
return this.getElementAttributeCompletions();
} else if (this.isPipeCompletion()) {
return this.getPipeCompletions();
} else {
return undefined;
}
Expand Down Expand Up @@ -577,6 +580,34 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
const completion = attrTable.get(name)!;
return getAttributeCompletionSymbol(completion, this.typeChecker) ?? undefined;
}

private isPipeCompletion(): this is PipeCompletionBuilder {
return this.node instanceof BindingPipe;
}

private getPipeCompletions(this: PipeCompletionBuilder):
ts.WithMetadata<ts.CompletionInfo>|undefined {
const pipes = this.templateTypeChecker.getPipesInScope(this.component);
if (pipes === null) {
return undefined;
}

const replacementSpan = makeReplacementSpanFromAst(this.node);

const entries: ts.CompletionEntry[] =
pipes.map(pipe => ({
name: pipe.name,
sortText: pipe.name,
kind: unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PIPE),
replacementSpan,
}));
return {
entries,
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
};
}
}

function makeReplacementSpanFromParseSourceSpan(span: ParseSourceSpan): ts.TextSpan {
Expand All @@ -587,7 +618,7 @@ function makeReplacementSpanFromParseSourceSpan(span: ParseSourceSpan): ts.TextS
}

function makeReplacementSpanFromAst(node: PropertyRead|PropertyWrite|MethodCall|SafePropertyRead|
SafeMethodCall): ts.TextSpan {
SafeMethodCall|BindingPipe): ts.TextSpan {
return {
start: node.nameSpan.start,
length: node.nameSpan.end - node.nameSpan.start,
Expand Down
52 changes: 48 additions & 4 deletions packages/language-service/ivy/test/completions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ const DIR_WITH_SELECTED_INPUT = {
`
};

const SOME_PIPE = {
'SomePipe': `
@Pipe({
name: 'somePipe',
})
export class SomePipe {
transform(value: string): string {
return value;
}
}
`
};

describe('completions', () => {
beforeEach(() => {
initMockFileSystem('Native');
Expand Down Expand Up @@ -445,6 +458,37 @@ describe('completions', () => {
});
});
});

describe('pipe scope', () => {
it('should complete a pipe binding', () => {
const {ngLS, fileName, cursor, text} = setup(`{{ foo | some¦ }}`, '', SOME_PIPE);
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
expectContain(
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PIPE),
['somePipe']);
expectReplacementText(completions, text, 'some');
});

// TODO(alxhub): currently disabled as the template targeting system identifies the cursor
// position as the entire Interpolation node, not the BindingPipe node. This happens because the
// BindingPipe node's span ends at the '|' character. To make this case work, the targeting
// system will need to artificially expand the BindingPipe's span to encompass any trailing
// spaces, which will be done in a future PR.
xit('should complete an empty pipe binding', () => {
const {ngLS, fileName, cursor, text} = setup(`{{ foo | ¦ }}`, '', SOME_PIPE);
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
expectContain(
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.PIPE),
['somePipe']);
expectReplacementText(completions, text, 'some');
});

it('should not return extraneous completions', () => {
const {ngLS, fileName, cursor, text} = setup(`{{ foo | some¦ }}`, '');
const completions = ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
expect(completions?.entries.length).toBe(0);
});
});
});

function expectContain(
Expand Down Expand Up @@ -495,7 +539,7 @@ function toText(displayParts?: ts.SymbolDisplayPart[]): string {

function setup(
templateWithCursor: string, classContents: string,
otherDirectives: {[name: string]: string} = {}): {
otherDeclarations: {[name: string]: string} = {}): {
env: LanguageServiceTestEnvironment,
fileName: AbsoluteFsPath,
AppCmp: ts.ClassDeclaration,
Expand All @@ -507,15 +551,15 @@ function setup(
const codePath = absoluteFrom('/test.ts');
const templatePath = absoluteFrom('/test.html');

const decls = ['AppCmp', ...Object.keys(otherDirectives)];
const decls = ['AppCmp', ...Object.keys(otherDeclarations)];

const otherDirectiveClassDecls = Object.values(otherDirectives).join('\n\n');
const otherDirectiveClassDecls = Object.values(otherDeclarations).join('\n\n');

const env = LanguageServiceTestEnvironment.setup([
{
name: codePath,
contents: `
import {Component, Directive, NgModule} from '@angular/core';
import {Component, Directive, NgModule, Pipe} from '@angular/core';
@Component({
templateUrl: './test.html',
Expand Down

0 comments on commit cbb6eae

Please sign in to comment.