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