99import { AST , ASTWithSource , BindingPipe , BindingType , Call , EmptyExpr , ImplicitReceiver , LiteralPrimitive , ParsedEventType , ParseSourceSpan , PropertyRead , PropertyWrite , SafePropertyRead , TmplAstBoundAttribute , TmplAstBoundEvent , TmplAstElement , TmplAstNode , TmplAstReference , TmplAstTemplate , TmplAstText , TmplAstTextAttribute , TmplAstVariable } from '@angular/compiler' ;
1010import { NgCompiler } from '@angular/compiler-cli/src/ngtsc/core' ;
1111import { CompletionKind , PotentialDirective , SymbolKind , TemplateDeclarationSymbol } from '@angular/compiler-cli/src/ngtsc/typecheck/api' ;
12- import { BoundEvent , TextAttribute } from '@angular/compiler/src/render3/r3_ast' ;
12+ import { BoundEvent , DeferredBlock , TextAttribute , UnknownBlock } from '@angular/compiler/src/render3/r3_ast' ;
1313import ts from 'typescript' ;
1414
15- import { addAttributeCompletionEntries , AttributeCompletionKind , buildAnimationCompletionEntries , buildAttributeCompletionTable , getAttributeCompletionSymbol } from './attribute_completions' ;
15+ import { addAttributeCompletionEntries , AsciiSortPriority , AttributeCompletionKind , buildAnimationCompletionEntries , buildAttributeCompletionTable , getAttributeCompletionSymbol } from './attribute_completions' ;
1616import { DisplayInfo , DisplayInfoKind , getDirectiveDisplayInfo , getSymbolDisplayInfo , getTsSymbolDisplayInfo , unsafeCastDisplayInfoKindToScriptElementKind } from './display_parts' ;
1717import { TargetContext , TargetNodeKind , TemplateTarget } from './template_target' ;
1818import { filterAliasImports , isBoundEventWithSyntheticHandler , isWithin } from './utils' ;
@@ -29,6 +29,8 @@ type LiteralCompletionBuilder = CompletionBuilder<LiteralPrimitive|TextAttribute
2929
3030type ElementAnimationCompletionBuilder = CompletionBuilder < TmplAstBoundAttribute | TmplAstBoundEvent > ;
3131
32+ type BlockCompletionBuilder = CompletionBuilder < UnknownBlock > ;
33+
3234export enum CompletionNodeContext {
3335 None ,
3436 ElementTag ,
@@ -40,6 +42,16 @@ export enum CompletionNodeContext {
4042
4143const ANIMATION_PHASES = [ 'start' , 'done' ] ;
4244
45+ function buildBlockSnippet ( insertSnippet : boolean , text : string , withParens : boolean ) : string {
46+ if ( ! insertSnippet ) {
47+ return text ;
48+ }
49+ if ( withParens ) {
50+ return `${ text } ($1) {$2}` ;
51+ }
52+ return `${ text } {$1}` ;
53+ }
54+
4355/**
4456 * Performs autocompletion operations on a given node in the template.
4557 *
@@ -83,11 +95,62 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
8395 return this . getPipeCompletions ( ) ;
8496 } else if ( this . isLiteralCompletion ( ) ) {
8597 return this . getLiteralCompletions ( options ) ;
98+ } else if ( this . isBlockCompletion ( ) ) {
99+ return this . getBlockCompletions ( options ) ;
86100 } else {
87101 return undefined ;
88102 }
89103 }
90104
105+ private isBlockCompletion ( ) : this is BlockCompletionBuilder {
106+ return this . node instanceof UnknownBlock ;
107+ }
108+
109+ private getBlockCompletions (
110+ this : BlockCompletionBuilder , options : ts . GetCompletionsAtPositionOptions | undefined ) :
111+ ts . WithMetadata < ts . CompletionInfo > | undefined {
112+ const blocksWithParens = [ 'if' , 'else if' , 'for' , 'switch' , 'case' , 'defer' ] ;
113+ const blocksWithoutParens = [ 'else' , 'empty' , 'placeholder' , 'error' , 'loading' , 'default' ] ;
114+
115+ // Determine whether to provide a snippet, which includes parens and curly braces.
116+ // If the block has any expressions or a body, don't provide a snippet as the completion.
117+ // TODO: We can be smarter about this, e.g. include `default` in `switch` if it is missing.
118+ const incompleteBlockHasExpressionsOrBody =
119+ this . node . sourceSpan . toString ( ) . substring ( 1 + this . node . name . length ) . trim ( ) . length > 0 ;
120+ const useSnippet = ( options ?. includeCompletionsWithSnippetText ?? false ) &&
121+ ! incompleteBlockHasExpressionsOrBody ;
122+
123+ // Generate the list of completions, one for each block.
124+ // TODO: Exclude connected blocks (e.g. `else` when the preceding block isn't `if` or `else
125+ // if`).
126+ const partialCompletionEntryWholeBlock = {
127+ kind : unsafeCastDisplayInfoKindToScriptElementKind ( DisplayInfoKind . BLOCK ) ,
128+ replacementSpan : {
129+ start : this . node . sourceSpan . start . offset + 1 ,
130+ length : this . node . name . length ,
131+ }
132+ } ;
133+ const completionEntries : ts . CompletionEntry [ ] = [
134+ ...blocksWithParens , ...blocksWithoutParens
135+ ] . map ( name => ( {
136+ name,
137+ sortText : `${ AsciiSortPriority . First } ${ name } ` ,
138+ insertText : buildBlockSnippet ( useSnippet , name , blocksWithParens . includes ( name ) ) ,
139+ isSnippet : useSnippet || undefined ,
140+ ...partialCompletionEntryWholeBlock ,
141+ } ) ) ;
142+
143+ // Return the completions.
144+ const completionInfo : ts . CompletionInfo = {
145+ flags : ts . CompletionInfoFlags . IsContinuation ,
146+ isMemberCompletion : false ,
147+ isGlobalCompletion : false ,
148+ isNewIdentifierLocation : false ,
149+ entries : completionEntries ,
150+ } ;
151+ return completionInfo ;
152+ }
153+
91154 private isLiteralCompletion ( ) : this is LiteralCompletionBuilder {
92155 return this . node instanceof LiteralPrimitive ||
93156 ( this . node instanceof TextAttribute &&
@@ -118,7 +181,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
118181 if ( this . node instanceof LiteralPrimitive ) {
119182 if ( typeof this . node . value === 'string' && this . node . value . length > 0 ) {
120183 replacementSpan = {
121- // The sourceSpan of `LiteralPrimitive` includes the open quote and the completion entries
184+ // The sourceSpan of `LiteralPrimitive` includes the open quote and the completion
185+ // entries
122186 // don't, so skip the open quote here.
123187 start : this . node . sourceSpan . start + 1 ,
124188 length : this . node . value . length ,
@@ -366,8 +430,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
366430 return {
367431 entries,
368432 // Although this completion is "global" in the sense of an Angular expression (there is no
369- // explicit receiver), it is not "global" in a TypeScript sense since Angular expressions have
370- // the component as an implicit receiver.
433+ // explicit receiver), it is not "global" in a TypeScript sense since Angular expressions
434+ // have the component as an implicit receiver.
371435 isGlobalCompletion : false ,
372436 isMemberCompletion : true ,
373437 isNewIdentifierLocation : false ,
@@ -392,8 +456,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
392456
393457 if ( templateContext . has ( entryName ) ) {
394458 const entry = templateContext . get ( entryName ) ! ;
395- // Entries that reference a symbol in the template context refer either to local references or
396- // variables.
459+ // Entries that reference a symbol in the template context refer either to local references
460+ // or variables.
397461 const symbol = this . templateTypeChecker . getSymbolOfNode ( entry . node , this . component ) as
398462 TemplateDeclarationSymbol |
399463 null ;
@@ -449,8 +513,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
449513 private isElementTagCompletion ( ) : this is CompletionBuilder < TmplAstElement | TmplAstText > {
450514 if ( this . node instanceof TmplAstText ) {
451515 const positionInTextNode = this . position - this . node . sourceSpan . start . offset ;
452- // We only provide element completions in a text node when there is an open tag immediately to
453- // the left of the position.
516+ // We only provide element completions in a text node when there is an open tag immediately
517+ // to the left of the position.
454518 return this . node . value . substring ( 0 , positionInTextNode ) . endsWith ( '<' ) ;
455519 } else if ( this . node instanceof TmplAstElement ) {
456520 return this . nodeContext === CompletionNodeContext . ElementTag ;
@@ -479,8 +543,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
479543 const replacementSpan : ts . TextSpan = { start, length} ;
480544
481545 let potentialTags = Array . from ( templateTypeChecker . getPotentialElementTags ( this . component ) ) ;
482- // Don't provide non-Angular tags (directive === null) because we expect other extensions (i.e.
483- // Emmet) to provide those for HTML files.
546+ // Don't provide non-Angular tags (directive === null) because we expect other extensions
547+ // (i.e. Emmet) to provide those for HTML files.
484548 potentialTags = potentialTags . filter ( ( [ _ , directive ] ) => directive !== null ) ;
485549 const entries : ts . CompletionEntry [ ] = potentialTags . map ( ( [ tag , directive ] ) => ( {
486550 kind : tagCompletionKind ( directive ) ,
@@ -648,8 +712,9 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
648712 }
649713
650714 if ( this . node instanceof TmplAstTextAttribute && this . node . keySpan !== undefined ) {
651- // The `sourceSpan` only includes `ngFor` and the `valueSpan` is always empty even if there
652- // is something there because we split this up into the desugared AST, `ngFor ngForOf=""`.
715+ // The `sourceSpan` only includes `ngFor` and the `valueSpan` is always empty even if
716+ // there is something there because we split this up into the desugared AST, `ngFor
717+ // ngForOf=""`.
653718 const nodeStart = this . node . keySpan . start . getContext ( 1 , 1 ) ;
654719 if ( nodeStart ?. before [ 0 ] === '*' ) {
655720 const nodeEnd = this . node . keySpan . end . getContext ( 1 , 1 ) ;
@@ -768,8 +833,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
768833 case AttributeCompletionKind . DomAttribute :
769834 case AttributeCompletionKind . DomProperty :
770835 // TODO(alxhub): ideally we would show the same documentation as quick info here. However,
771- // since these bindings don't exist in the TCB, there is no straightforward way to retrieve
772- // a `ts.Symbol` for the field in the TS DOM definition.
836+ // since these bindings don't exist in the TCB, there is no straightforward way to
837+ // retrieve a `ts.Symbol` for the field in the TS DOM definition.
773838 displayParts = [ ] ;
774839 break ;
775840 case AttributeCompletionKind . DirectiveAttribute :
0 commit comments