@@ -11,11 +11,14 @@ import * as vscode from 'vscode';
1111const ANGULAR_PROPERTY_ASSIGNMENTS = new Set ( [
1212 'template' ,
1313 'templateUrl' ,
14+ 'styles' ,
1415 'styleUrls' ,
1516 'styleUrl' ,
1617 'host' ,
1718] ) ;
1819
20+ const ANGULAR_HOST_BINDING_DECORATORS = new Set ( [ 'HostBinding' , 'HostListener' ] ) ;
21+
1922/**
2023 * Determines if the position is inside a decorator
2124 * property that supports language service features.
@@ -27,10 +30,11 @@ export function isNotTypescriptOrSupportedDecoratorField(
2730 if ( document . languageId !== 'typescript' ) {
2831 return true ;
2932 }
30- return isPropertyAssignmentToStringOrStringInArray (
33+ return isInsideAngularStringLiteral (
3134 document . getText ( ) ,
3235 document . offsetAt ( position ) ,
3336 ANGULAR_PROPERTY_ASSIGNMENTS ,
37+ ANGULAR_HOST_BINDING_DECORATORS ,
3438 ) ;
3539}
3640
@@ -65,27 +69,21 @@ export function isInsideStringLiteral(
6569 return true ;
6670 }
6771 const offset = document . offsetAt ( position ) ;
68- const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
69- scanner . setText ( document . getText ( ) ) ;
72+ const { scanner, scan} = createTemplateAwareScanner ( document . getText ( ) ) ;
7073
71- let token : ts . SyntaxKind = scanner . scan ( ) ;
74+ let token : ts . SyntaxKind = scan ( ) ;
7275 while ( token !== ts . SyntaxKind . EndOfFileToken && scanner . getStartPos ( ) < offset ) {
73- const isStringToken =
74- token === ts . SyntaxKind . StringLiteral ||
75- token === ts . SyntaxKind . NoSubstitutionTemplateLiteral ;
76- const isCursorInToken =
77- scanner . getStartPos ( ) <= offset &&
78- scanner . getStartPos ( ) + scanner . getTokenText ( ) . length >= offset ;
79- if ( isCursorInToken && isStringToken ) {
76+ if ( isStringToken ( token ) && isCursorInToken ( scanner , offset ) ) {
8077 return true ;
8178 }
82- token = scanner . scan ( ) ;
79+ token = scan ( ) ;
8380 }
8481 return false ;
8582}
8683
8784/**
88- * Basic scanner to determine if we're inside a string of a property with one of the given names.
85+ * Basic scanner to determine if we're inside a string of a property with one of the given names,
86+ * or inside a string argument of a decorator with one of the given names.
8987 *
9088 * This scanner is not currently robust or perfect but provides us with an accurate answer _most_ of
9189 * the time.
@@ -95,24 +93,32 @@ export function isInsideStringLiteral(
9593 * `@Component` or `{styleUrls: [someFunction('stringL¦iteral')]}`, the @angular/language-service
9694 * will always give us the correct answer. This helper gives us a quick win for optimizing the
9795 * number of requests we send to the server.
98- *
99- * TODO(atscott): tagged templates don't work: #1872 /
100- * https://github.com/Microsoft/TypeScript/issues/20055
10196 */
102- function isPropertyAssignmentToStringOrStringInArray (
97+ function isInsideAngularStringLiteral (
10398 documentText : string ,
10499 offset : number ,
105100 propertyAssignmentNames : Set < string > ,
101+ decoratorNames : Set < string > ,
106102) : boolean {
107- const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
108- scanner . setText ( documentText ) ;
103+ const { scanner, scan} = createTemplateAwareScanner ( documentText ) ;
109104
110- let token : ts . SyntaxKind = scanner . scan ( ) ;
105+ let token : ts . SyntaxKind = scan ( ) ;
111106 let lastToken : ts . SyntaxKind | undefined ;
112107 let lastTokenText : string | undefined ;
108+ let secondLastToken : ts . SyntaxKind | undefined ;
113109 let unclosedBraces = 0 ;
114110 let unclosedBrackets = 0 ;
111+ let unclosedParens = 0 ;
115112 let propertyAssignmentContext = false ;
113+ // Nesting depth at which the current property assignment context was entered. The context ends
114+ // at the first terminator found at that same depth, i.e. the one that ends the assignment's
115+ // initializer rather than one nested inside of it.
116+ let contextBraces = 0 ;
117+ let contextBrackets = 0 ;
118+ let decoratorContext = false ;
119+ // Parenthesis depth at which the current decorator context was entered. The context ends at the
120+ // closing parenthesis of the decorator call.
121+ let contextParens = 0 ;
116122 while ( token !== ts . SyntaxKind . EndOfFileToken && scanner . getStartPos ( ) < offset ) {
117123 if (
118124 lastToken === ts . SyntaxKind . Identifier &&
@@ -121,36 +127,64 @@ function isPropertyAssignmentToStringOrStringInArray(
121127 propertyAssignmentNames . has ( lastTokenText )
122128 ) {
123129 propertyAssignmentContext = true ;
124- token = scanner . scan ( ) ;
130+ contextBraces = unclosedBraces ;
131+ contextBrackets = unclosedBrackets ;
132+ token = scan ( ) ;
125133 continue ;
126134 }
127- if ( unclosedBraces === 0 && unclosedBrackets === 0 && isPropertyAssignmentTerminator ( token ) ) {
135+ if (
136+ propertyAssignmentContext &&
137+ unclosedBraces === contextBraces &&
138+ unclosedBrackets === contextBrackets &&
139+ isPropertyAssignmentTerminator ( token )
140+ ) {
128141 propertyAssignmentContext = false ;
129142 }
130143
144+ if (
145+ secondLastToken === ts . SyntaxKind . AtToken &&
146+ lastToken === ts . SyntaxKind . Identifier &&
147+ lastTokenText !== undefined &&
148+ token === ts . SyntaxKind . OpenParenToken &&
149+ decoratorNames . has ( lastTokenText )
150+ ) {
151+ decoratorContext = true ;
152+ contextParens = unclosedParens ;
153+ }
154+ if (
155+ decoratorContext &&
156+ token === ts . SyntaxKind . CloseParenToken &&
157+ unclosedParens === contextParens + 1
158+ ) {
159+ decoratorContext = false ;
160+ }
161+
131162 if ( token === ts . SyntaxKind . OpenBracketToken ) {
132163 unclosedBrackets ++ ;
133164 } else if ( token === ts . SyntaxKind . OpenBraceToken ) {
134165 unclosedBraces ++ ;
166+ } else if ( token === ts . SyntaxKind . OpenParenToken ) {
167+ unclosedParens ++ ;
135168 } else if ( token === ts . SyntaxKind . CloseBracketToken ) {
136169 unclosedBrackets -- ;
137170 } else if ( token === ts . SyntaxKind . CloseBraceToken ) {
138171 unclosedBraces -- ;
172+ } else if ( token === ts . SyntaxKind . CloseParenToken ) {
173+ unclosedParens -- ;
139174 }
140175
141- const isStringToken =
142- token === ts . SyntaxKind . StringLiteral ||
143- token === ts . SyntaxKind . NoSubstitutionTemplateLiteral ;
144- const isCursorInToken =
145- scanner . getStartPos ( ) <= offset &&
146- scanner . getStartPos ( ) + scanner . getTokenText ( ) . length >= offset ;
147- if ( propertyAssignmentContext && isCursorInToken && isStringToken ) {
176+ if (
177+ ( propertyAssignmentContext || decoratorContext ) &&
178+ isStringToken ( token ) &&
179+ isCursorInToken ( scanner , offset )
180+ ) {
148181 return true ;
149182 }
150183
151184 lastTokenText = scanner . getTokenText ( ) ;
185+ secondLastToken = lastToken ;
152186 lastToken = token ;
153- token = scanner . scan ( ) ;
187+ token = scan ( ) ;
154188 }
155189
156190 return false ;
@@ -164,3 +198,62 @@ function isPropertyAssignmentTerminator(token: ts.SyntaxKind) {
164198 token === ts . SyntaxKind . CloseBraceToken
165199 ) ;
166200}
201+
202+ function isStringToken ( token : ts . SyntaxKind ) : boolean {
203+ return (
204+ token === ts . SyntaxKind . StringLiteral || token === ts . SyntaxKind . NoSubstitutionTemplateLiteral
205+ ) ;
206+ }
207+
208+ function isCursorInToken ( scanner : ts . Scanner , offset : number ) : boolean {
209+ return (
210+ scanner . getStartPos ( ) <= offset &&
211+ scanner . getStartPos ( ) + scanner . getTokenText ( ) . length >= offset
212+ ) ;
213+ }
214+
215+ /**
216+ * Creates a `ts.Scanner` for the given text along with a `scan` function that, unlike
217+ * `ts.Scanner#scan`, handles template literals with substitutions correctly.
218+ *
219+ * The scanner has no notion of template substitutions on its own: it reports the `}` closing a
220+ * `${...}` substitution as a plain `CloseBraceToken` and then treats the remainder of the template
221+ * literal as the beginning of a new one, mis-scanning everything that follows it (see #65494). Just
222+ * like the TypeScript parser, the returned `scan` function re-scans such braces as template
223+ * continuations (`TemplateMiddle`/`TemplateTail`) instead.
224+ */
225+ function createTemplateAwareScanner ( text : string ) : {
226+ scanner : ts . Scanner ;
227+ scan : ( ) => ts . SyntaxKind ;
228+ } {
229+ const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
230+ scanner . setText ( text ) ;
231+
232+ // Brace depth at which each currently open template substitution started, innermost last.
233+ const substitutionDepths : number [ ] = [ ] ;
234+ let braceDepth = 0 ;
235+
236+ const scan = ( ) : ts . SyntaxKind => {
237+ let token = scanner . scan ( ) ;
238+ if ( token === ts . SyntaxKind . OpenBraceToken ) {
239+ braceDepth ++ ;
240+ } else if ( token === ts . SyntaxKind . CloseBraceToken ) {
241+ if (
242+ substitutionDepths . length > 0 &&
243+ braceDepth === substitutionDepths [ substitutionDepths . length - 1 ]
244+ ) {
245+ // This brace closes a template substitution rather than a block or object literal.
246+ substitutionDepths . pop ( ) ;
247+ token = scanner . reScanTemplateToken ( false /* isTaggedTemplate */ ) ;
248+ } else {
249+ braceDepth -- ;
250+ }
251+ }
252+ if ( token === ts . SyntaxKind . TemplateHead || token === ts . SyntaxKind . TemplateMiddle ) {
253+ substitutionDepths . push ( braceDepth ) ;
254+ }
255+ return token ;
256+ } ;
257+
258+ return { scanner, scan} ;
259+ }
0 commit comments