feat(language-service): autocomplete pipe binding expressions (#40032)
This commit adds autocompletion for pipe expressions, built on existing APIs for checking which pipes are in scope. PR Close #40032
This commit is contained in:
		
							parent
							
								
									66378ed0ef
								
							
						
					
					
						commit
						cbb6eae4a9
					
				| @ -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'; | ||||
| @ -23,6 +23,7 @@ type PropertyExpressionCompletionBuilder = | ||||
| type ElementAttributeCompletionBuilder = | ||||
|     CompletionBuilder<TmplAstElement|TmplAstBoundAttribute|TmplAstTextAttribute|TmplAstBoundEvent>; | ||||
| 
 | ||||
| type PipeCompletionBuilder = CompletionBuilder<BindingPipe>; | ||||
| 
 | ||||
| export enum CompletionNodeContext { | ||||
|   None, | ||||
| @ -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; | ||||
|     } | ||||
| @ -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 { | ||||
| @ -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, | ||||
|  | ||||
| @ -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'); | ||||
| @ -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( | ||||
| @ -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, | ||||
| @ -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', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user