Skip to content

Commit ed753c8

Browse files
authored
Revert "refactor(client): use SourceFile to detect the Angular context in the client (#2027)" (#2038)
This reverts commit 13d9776. Fixes #2037
1 parent 5139cea commit ed753c8

File tree

2 files changed

+83
-80
lines changed

2 files changed

+83
-80
lines changed

client/src/embedded_support.ts

+82-79
Original file line numberDiff line numberDiff line change
@@ -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
*/
5036
export 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
}

client/src/tests/embedded_support_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('embedded language support', () => {
3939
test(`const foo = {template: '<div></div>¦'}`, isInsideInlineTemplateRegion, true);
4040
});
4141

42-
it('works for inline templates after a template string', () => {
42+
xit('works for inline templates after a template string', () => {
4343
test(
4444
'const x = `${""}`;\n' +
4545
'const foo = {template: `hello ¦world`}',

0 commit comments

Comments
 (0)