@@ -14,8 +14,13 @@ export function isInsideInlineTemplateRegion(
14
14
if ( document . languageId !== 'typescript' ) {
15
15
return true ;
16
16
}
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 ;
19
24
}
20
25
21
26
/** Determines if the position is inside an inline template, templateUrl, or string in styleUrls. */
@@ -24,102 +29,94 @@ export function isInsideComponentDecorator(
24
29
if ( document . languageId !== 'typescript' ) {
25
30
return true ;
26
31
}
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 ;
30
44
}
31
45
32
46
/**
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.
35
49
*/
36
50
export function isInsideStringLiteral (
37
51
document : vscode . TextDocument , position : vscode . Position ) : boolean {
38
52
if ( document . languageId !== 'typescript' ) {
39
53
return true ;
40
54
}
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 ;
55
59
}
56
- return false ;
60
+
61
+ return ts . isStringLiteralLike ( node ) ;
57
62
}
58
63
59
64
/**
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`.
73
68
*/
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 ;
117
72
}
73
+ return undefined ;
74
+ }
118
75
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 ;
120
87
}
121
88
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 ) ;
125
122
}
0 commit comments