@@ -14,13 +14,8 @@ export function isInsideInlineTemplateRegion(
1414 if ( document . languageId !== 'typescript' ) {
1515 return true ;
1616 }
17- const node = getNodeAtDocumentPosition ( document , position ) ;
18-
19- if ( ! node ) {
20- return false ;
21- }
22-
23- return getPropertyAssignmentFromValue ( node , 'template' ) !== null ;
17+ return isPropertyAssignmentToStringOrStringInArray (
18+ document . getText ( ) , document . offsetAt ( position ) , [ 'template' ] ) ;
2419}
2520
2621/** Determines if the position is inside an inline template, templateUrl, or string in styleUrls. */
@@ -29,94 +24,102 @@ export function isInsideComponentDecorator(
2924 if ( document . languageId !== 'typescript' ) {
3025 return true ;
3126 }
32-
33- const node = getNodeAtDocumentPosition ( document , position ) ;
34- if ( ! node ) {
35- return false ;
36- }
37- const assignment = getPropertyAssignmentFromValue ( node , 'template' ) ??
38- getPropertyAssignmentFromValue ( node , 'templateUrl' ) ??
39- // `node.parent` is used because the string is a child of an array element and we want to get
40- // the property name
41- getPropertyAssignmentFromValue ( node . parent , 'styleUrls' ) ??
42- getPropertyAssignmentFromValue ( node , 'styleUrl' ) ;
43- return assignment !== null ;
27+ return isPropertyAssignmentToStringOrStringInArray (
28+ document . getText ( ) , document . offsetAt ( position ) ,
29+ [ 'template' , 'templateUrl' , 'styleUrls' , 'styleUrl' ] ) ;
4430}
4531
4632/**
47- * Determines if the position is inside a string literal. Returns `true` if the document language
48- * is not TypeScript.
33+ * Determines if the position is inside a string literal. Returns `true` if the document language is
34+ * not TypeScript.
4935 */
5036export function isInsideStringLiteral (
5137 document : vscode . TextDocument , position : vscode . Position ) : boolean {
5238 if ( document . languageId !== 'typescript' ) {
5339 return true ;
5440 }
55- const node = getNodeAtDocumentPosition ( document , position ) ;
56-
57- if ( ! node ) {
58- return false ;
41+ const offset = document . offsetAt ( position ) ;
42+ const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
43+ scanner . setText ( document . getText ( ) ) ;
44+
45+ let token : ts . SyntaxKind = scanner . scan ( ) ;
46+ while ( token !== ts . SyntaxKind . EndOfFileToken && scanner . getStartPos ( ) < offset ) {
47+ const isStringToken = token === ts . SyntaxKind . StringLiteral ||
48+ token === ts . SyntaxKind . NoSubstitutionTemplateLiteral ;
49+ const isCursorInToken = scanner . getStartPos ( ) <= offset &&
50+ scanner . getStartPos ( ) + scanner . getTokenText ( ) . length >= offset ;
51+ if ( isCursorInToken && isStringToken ) {
52+ return true ;
53+ }
54+ token = scanner . scan ( ) ;
5955 }
60-
61- return ts . isStringLiteralLike ( node ) ;
56+ return false ;
6257}
6358
6459/**
65- * Return the node that most tightly encompasses the specified `position`.
66- * @param node The starting node to start the top-down search.
67- * @param position The target position within the `node`.
60+ * Basic scanner to determine if we're inside a string of a property with one of the given names.
61+ *
62+ * This scanner is not currently robust or perfect but provides us with an accurate answer _most_ of
63+ * the time.
64+ *
65+ * False positives are OK here. Though this will give some false positives for determining if a
66+ * position is within an Angular context, i.e. an object like `{template: ''}` that is not inside an
67+ * `@Component` or `{styleUrls: [someFunction('stringL¦iteral')]}`, the @angular/language-service
68+ * will always give us the correct answer. This helper gives us a quick win for optimizing the
69+ * number of requests we send to the server.
70+ *
71+ * TODO(atscott): tagged templates don't work: #1872 /
72+ * https://github.com/Microsoft/TypeScript/issues/20055
6873 */
69- function findTightestNodeAtPosition ( node : ts . Node , position : number ) : ts . Node | undefined {
70- if ( node . getStart ( ) <= position && position < node . getEnd ( ) ) {
71- return node . forEachChild ( c => findTightestNodeAtPosition ( c , position ) ) ?? node ;
74+ function isPropertyAssignmentToStringOrStringInArray (
75+ documentText : string , offset : number , propertyAssignmentNames : string [ ] ) : boolean {
76+ const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
77+ scanner . setText ( documentText ) ;
78+
79+ let token : ts . SyntaxKind = scanner . scan ( ) ;
80+ let lastToken : ts . SyntaxKind | undefined ;
81+ let lastTokenText : string | undefined ;
82+ let unclosedBraces = 0 ;
83+ let unclosedBrackets = 0 ;
84+ let propertyAssignmentContext = false ;
85+ while ( token !== ts . SyntaxKind . EndOfFileToken && scanner . getStartPos ( ) < offset ) {
86+ if ( lastToken === ts . SyntaxKind . Identifier && lastTokenText !== undefined &&
87+ propertyAssignmentNames . includes ( lastTokenText ) && token === ts . SyntaxKind . ColonToken ) {
88+ propertyAssignmentContext = true ;
89+ token = scanner . scan ( ) ;
90+ continue ;
91+ }
92+ if ( unclosedBraces === 0 && unclosedBrackets === 0 && isPropertyAssignmentTerminator ( token ) ) {
93+ propertyAssignmentContext = false ;
94+ }
95+
96+ if ( token === ts . SyntaxKind . OpenBracketToken ) {
97+ unclosedBrackets ++ ;
98+ } else if ( token === ts . SyntaxKind . OpenBraceToken ) {
99+ unclosedBraces ++ ;
100+ } else if ( token === ts . SyntaxKind . CloseBracketToken ) {
101+ unclosedBrackets -- ;
102+ } else if ( token === ts . SyntaxKind . CloseBraceToken ) {
103+ unclosedBraces -- ;
104+ }
105+
106+ const isStringToken = token === ts . SyntaxKind . StringLiteral ||
107+ token === ts . SyntaxKind . NoSubstitutionTemplateLiteral ;
108+ const isCursorInToken = scanner . getStartPos ( ) <= offset &&
109+ scanner . getStartPos ( ) + scanner . getTokenText ( ) . length >= offset ;
110+ if ( propertyAssignmentContext && isCursorInToken && isStringToken ) {
111+ return true ;
112+ }
113+
114+ lastTokenText = scanner . getTokenText ( ) ;
115+ lastToken = token ;
116+ token = scanner . scan ( ) ;
72117 }
73- return undefined ;
74- }
75118
76- /**
77- * Returns a property assignment from the assignment value if the property name
78- * matches the specified `key`, or `null` if there is no match.
79- */
80- function getPropertyAssignmentFromValue ( value : ts . Node , key : string ) : ts . PropertyAssignment | null {
81- const propAssignment = value . parent ;
82- if ( ! propAssignment || ! ts . isPropertyAssignment ( propAssignment ) ||
83- propAssignment . name . getText ( ) !== key ) {
84- return null ;
85- }
86- return propAssignment ;
119+ return false ;
87120}
88121
89- type NgLSClientSourceFile = ts . SourceFile & { [ NgLSClientSourceFileVersion ] : number } ;
90-
91- /**
92- * The `TextDocument` is not extensible, so the `WeakMap` is used here.
93- */
94- const ngLSClientSourceFileMap = new WeakMap < vscode . TextDocument , NgLSClientSourceFile > ( ) ;
95- const NgLSClientSourceFileVersion = Symbol ( 'NgLSClientSourceFileVersion' ) ;
96-
97- /**
98- *
99- * Parse the document to `SourceFile` and return the node at the document position.
100- */
101- function getNodeAtDocumentPosition (
102- document : vscode . TextDocument , position : vscode . Position ) : ts . Node | undefined {
103- const offset = document . offsetAt ( position ) ;
104-
105- let sourceFile = ngLSClientSourceFileMap . get ( document ) ;
106- if ( ! sourceFile || sourceFile [ NgLSClientSourceFileVersion ] !== document . version ) {
107- sourceFile =
108- ts . createSourceFile (
109- document . fileName , document . getText ( ) , {
110- languageVersion : ts . ScriptTarget . ESNext ,
111- jsDocParsingMode : ts . JSDocParsingMode . ParseNone ,
112- } ,
113- /** setParentNodes */
114- true /** If not set, the `findTightestNodeAtPosition` will throw an error */ ) as
115- NgLSClientSourceFile ;
116- sourceFile [ NgLSClientSourceFileVersion ] = document . version ;
117-
118- ngLSClientSourceFileMap . set ( document , sourceFile ) ;
119- }
120-
121- return findTightestNodeAtPosition ( sourceFile , offset ) ;
122+ function isPropertyAssignmentTerminator ( token : ts . SyntaxKind ) {
123+ return token === ts . SyntaxKind . EndOfFileToken || token === ts . SyntaxKind . CommaToken ||
124+ token === ts . SyntaxKind . SemicolonToken || token === ts . SyntaxKind . CloseBraceToken ;
122125}
0 commit comments