@@ -11,115 +11,105 @@ import * as vscode from 'vscode';
11
11
/** Determines if the position is inside an inline template. */
12
12
export function isInsideInlineTemplateRegion (
13
13
document : vscode . TextDocument , position : vscode . Position ) : boolean {
14
- if ( document . languageId !== 'typescript' ) {
15
- return true ;
14
+ const node = getNodeAtDocumentPosition ( document , position ) ;
15
+
16
+ if ( ! node ) {
17
+ return false ;
16
18
}
17
- return isPropertyAssignmentToStringOrStringInArray (
18
- document . getText ( ) , document . offsetAt ( position ) , [ 'template' ] ) ;
19
+
20
+ return getPropertyAssignmentFromValue ( node , 'template' ) !== null ;
19
21
}
20
22
21
23
/** Determines if the position is inside an inline template, templateUrl, or string in styleUrls. */
22
24
export function isInsideComponentDecorator (
23
25
document : vscode . TextDocument , position : vscode . Position ) : boolean {
24
- if ( document . languageId !== 'typescript' ) {
25
- return true ;
26
+ const node = getNodeAtDocumentPosition ( document , position ) ;
27
+ if ( ! node ) {
28
+ return false ;
26
29
}
27
- return isPropertyAssignmentToStringOrStringInArray (
28
- document . getText ( ) , document . offsetAt ( position ) ,
29
- [ 'template' , 'templateUrl' , 'styleUrls' , 'styleUrl' ] ) ;
30
+ const assignment = getPropertyAssignmentFromValue ( node , 'template' ) ??
31
+ getPropertyAssignmentFromValue ( node , 'templateUrl' ) ??
32
+ // `node.parent` is used because the string is a child of an array element and we want to get
33
+ // the property name
34
+ getPropertyAssignmentFromValue ( node . parent , 'styleUrls' ) ??
35
+ getPropertyAssignmentFromValue ( node , 'styleUrl' ) ;
36
+ return assignment !== null ;
30
37
}
31
38
32
39
/**
33
- * Determines if the position is inside a string literal. Returns `true` if the document language is
34
- * not TypeScript.
40
+ * Determines if the position is inside a string literal. Returns `true` if the document language
41
+ * is not TypeScript.
35
42
*/
36
43
export function isInsideStringLiteral (
37
44
document : vscode . TextDocument , position : vscode . Position ) : boolean {
38
- if ( document . languageId !== 'typescript' ) {
39
- return true ;
40
- }
41
- const offset = document . offsetAt ( position ) ;
42
- const scanner = ts . createScanner ( ts . ScriptTarget . ESNext , true /* skipTrivia */ ) ;
43
- scanner . setText ( document . getText ( ) ) ;
45
+ const node = getNodeAtDocumentPosition ( document , position ) ;
44
46
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 ( ) ;
47
+ if ( ! node ) {
48
+ return false ;
55
49
}
56
- return false ;
50
+
51
+ return ts . isStringLiteralLike ( node ) ;
57
52
}
58
53
59
54
/**
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
55
+ * Return the node that most tightly encompasses the specified `position`.
56
+ * @param node The starting node to start the top-down search.
57
+ * @param position The target position within the `node`.
73
58
*/
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 ) ;
59
+ function findTightestNodeAtPosition ( node : ts . Node , position : number ) : ts . Node | undefined {
60
+ if ( node . getStart ( ) <= position && position < node . getEnd ( ) ) {
61
+ return node . forEachChild ( c => findTightestNodeAtPosition ( c , position ) ) ?? node ;
62
+ }
63
+ return undefined ;
64
+ }
78
65
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
- }
66
+ /**
67
+ * Returns a property assignment from the assignment value if the property name
68
+ * matches the specified `key`, or `null` if there is no match.
69
+ */
70
+ function getPropertyAssignmentFromValue ( value : ts . Node , key : string ) : ts . PropertyAssignment | null {
71
+ const propAssignment = value . parent ;
72
+ if ( ! propAssignment || ! ts . isPropertyAssignment ( propAssignment ) ||
73
+ propAssignment . name . getText ( ) !== key ) {
74
+ return null ;
75
+ }
76
+ return propAssignment ;
77
+ }
95
78
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
- }
79
+ type NgLSClientSourceFile = ts . SourceFile & { [ NgLSClientSourceFileVersion ] : number } ;
105
80
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
- }
81
+ /**
82
+ * The `TextDocument` is not extensible, so the `WeakMap` is used here.
83
+ */
84
+ const ngLSClientSourceFileMap = new WeakMap < vscode . TextDocument , NgLSClientSourceFile > ( ) ;
85
+ const NgLSClientSourceFileVersion = Symbol ( 'NgLSClientSourceFileVersion' ) ;
113
86
114
- lastTokenText = scanner . getTokenText ( ) ;
115
- lastToken = token ;
116
- token = scanner . scan ( ) ;
87
+ /**
88
+ *
89
+ * Parse the document to `SourceFile` and return the node at the document position.
90
+ */
91
+ function getNodeAtDocumentPosition (
92
+ document : vscode . TextDocument , position : vscode . Position ) : ts . Node | undefined {
93
+ if ( document . languageId !== 'typescript' ) {
94
+ return undefined ;
117
95
}
96
+ const offset = document . offsetAt ( position ) ;
118
97
119
- return false ;
120
- }
98
+ let sourceFile = ngLSClientSourceFileMap . get ( document ) ;
99
+ if ( ! sourceFile || sourceFile [ NgLSClientSourceFileVersion ] !== document . version ) {
100
+ sourceFile =
101
+ ts . createSourceFile (
102
+ document . fileName , document . getText ( ) , {
103
+ languageVersion : ts . ScriptTarget . ESNext ,
104
+ jsDocParsingMode : ts . JSDocParsingMode . ParseNone ,
105
+ } ,
106
+ /** setParentNodes */
107
+ true /** If not set, the `findTightestNodeAtPosition` will throw an error */ ) as
108
+ NgLSClientSourceFile ;
109
+ sourceFile [ NgLSClientSourceFileVersion ] = document . version ;
110
+
111
+ ngLSClientSourceFileMap . set ( document , sourceFile ) ;
112
+ }
121
113
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 ;
114
+ return findTightestNodeAtPosition ( sourceFile , offset ) ;
125
115
}
0 commit comments