@@ -14,8 +14,13 @@ export function isInsideInlineTemplateRegion(
1414 if ( document . languageId !== 'typescript' ) {
1515 return true ;
1616 }
17- return isPropertyAssignmentToStringOrStringInArray (
18- document . getText ( ) , document . offsetAt ( position ) , [ 'template' ] ) ;
17+ const node = getNodeAtDocumentPosition ( document , position ) ;
18+
19+ if ( ! node ) {
20+ return false ;
21+ }
22+
23+ return getPropertyAssignmentFromValue ( node , 'template' ) !== null ;
1924}
2025
2126/** Determines if the position is inside an inline template, templateUrl, or string in styleUrls. */
@@ -24,102 +29,94 @@ export function isInsideComponentDecorator(
2429 if ( document . languageId !== 'typescript' ) {
2530 return true ;
2631 }
27- return isPropertyAssignmentToStringOrStringInArray (
28- document . getText ( ) , document . offsetAt ( position ) ,
29- [ 'template' , 'templateUrl' , 'styleUrls' , 'styleUrl' ] ) ;
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 ;
3044}
3145
3246/**
33- * Determines if the position is inside a string literal. Returns `true` if the document language is
34- * not TypeScript.
47+ * Determines if the position is inside a string literal. Returns `true` if the document language
48+ * is not TypeScript.
3549 */
3650export function isInsideStringLiteral (
3751 document : vscode . TextDocument , position : vscode . Position ) : boolean {
3852 if ( document . languageId !== 'typescript' ) {
3953 return true ;
4054 }
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 ( ) ;
55+ const node = getNodeAtDocumentPosition ( document , position ) ;
56+
57+ if ( ! node ) {
58+ return false ;
5559 }
56- return false ;
60+
61+ return ts . isStringLiteralLike ( node ) ;
5762}
5863
5964/**
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
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`.
7368 */
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 ( ) ;
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 ;
11772 }
73+ return undefined ;
74+ }
11875
119- return false ;
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 ;
12087}
12188
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 ;
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 ) ;
125122}
0 commit comments