• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.formatting {
3    const standardScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard);
4    const jsxScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.JSX);
5
6    export interface FormattingScanner {
7        advance(): void;
8        isOnToken(): boolean;
9        isOnEOF(): boolean;
10        readTokenInfo(n: Node): TokenInfo;
11        readEOFTokenRange(): TextRangeWithKind;
12        getCurrentLeadingTrivia(): TextRangeWithKind[] | undefined;
13        lastTrailingTriviaWasNewLine(): boolean;
14        skipToEndOf(node: Node): void;
15        skipToStartOf(node: Node): void;
16    }
17
18    const enum ScanAction {
19        Scan,
20        RescanGreaterThanToken,
21        RescanSlashToken,
22        RescanTemplateToken,
23        RescanJsxIdentifier,
24        RescanJsxText,
25        RescanJsxAttributeValue,
26    }
27
28    export function getFormattingScanner<T>(text: string, languageVariant: LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T {
29        const scanner = languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner;
30
31        scanner.setText(text);
32        scanner.setTextPos(startPos);
33
34        let wasNewLine = true;
35        let leadingTrivia: TextRangeWithTriviaKind[] | undefined;
36        let trailingTrivia: TextRangeWithTriviaKind[] | undefined;
37
38        let savedPos: number;
39        let lastScanAction: ScanAction | undefined;
40        let lastTokenInfo: TokenInfo | undefined;
41
42        const res = cb({
43            advance,
44            readTokenInfo,
45            readEOFTokenRange,
46            isOnToken,
47            isOnEOF,
48            getCurrentLeadingTrivia: () => leadingTrivia,
49            lastTrailingTriviaWasNewLine: () => wasNewLine,
50            skipToEndOf,
51            skipToStartOf,
52        });
53
54        lastTokenInfo = undefined;
55        scanner.setText(undefined);
56
57        return res;
58
59        function advance(): void {
60            lastTokenInfo = undefined;
61            const isStarted = scanner.getStartPos() !== startPos;
62
63            if (isStarted) {
64                wasNewLine = !!trailingTrivia && last(trailingTrivia).kind === SyntaxKind.NewLineTrivia;
65            }
66            else {
67                scanner.scan();
68            }
69
70            leadingTrivia = undefined;
71            trailingTrivia = undefined;
72
73            let pos = scanner.getStartPos();
74
75            // Read leading trivia and token
76            while (pos < endPos) {
77                const t = scanner.getToken();
78                if (!isTrivia(t)) {
79                    break;
80                }
81
82                // consume leading trivia
83                scanner.scan();
84                const item: TextRangeWithTriviaKind = {
85                    pos,
86                    end: scanner.getStartPos(),
87                    kind: t
88                };
89
90                pos = scanner.getStartPos();
91
92                leadingTrivia = append(leadingTrivia, item);
93            }
94
95            savedPos = scanner.getStartPos();
96        }
97
98        function shouldRescanGreaterThanToken(node: Node): boolean {
99            switch (node.kind) {
100                case SyntaxKind.GreaterThanEqualsToken:
101                case SyntaxKind.GreaterThanGreaterThanEqualsToken:
102                case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
103                case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
104                case SyntaxKind.GreaterThanGreaterThanToken:
105                    return true;
106            }
107
108            return false;
109        }
110
111        function shouldRescanJsxIdentifier(node: Node): boolean {
112            if (node.parent) {
113                switch (node.parent.kind) {
114                    case SyntaxKind.JsxAttribute:
115                    case SyntaxKind.JsxOpeningElement:
116                    case SyntaxKind.JsxClosingElement:
117                    case SyntaxKind.JsxSelfClosingElement:
118                        // May parse an identifier like `module-layout`; that will be scanned as a keyword at first, but we should parse the whole thing to get an identifier.
119                        return isKeyword(node.kind) || node.kind === SyntaxKind.Identifier;
120                }
121            }
122
123            return false;
124        }
125
126        function shouldRescanJsxText(node: Node): boolean {
127            const isJSXText = isJsxText(node);
128            if (isJSXText) {
129                const containingElement = findAncestor(node.parent, p => isJsxElement(p));
130                if (!containingElement) return false; // should never happen
131                return !isParenthesizedExpression(containingElement.parent);
132            }
133            return false;
134        }
135
136        function shouldRescanSlashToken(container: Node): boolean {
137            return container.kind === SyntaxKind.RegularExpressionLiteral;
138        }
139
140        function shouldRescanTemplateToken(container: Node): boolean {
141            return container.kind === SyntaxKind.TemplateMiddle ||
142                container.kind === SyntaxKind.TemplateTail;
143        }
144
145        function shouldRescanJsxAttributeValue(node: Node): boolean {
146            return node.parent && isJsxAttribute(node.parent) && node.parent.initializer === node;
147        }
148
149        function startsWithSlashToken(t: SyntaxKind): boolean {
150            return t === SyntaxKind.SlashToken || t === SyntaxKind.SlashEqualsToken;
151        }
152
153        function readTokenInfo(n: Node): TokenInfo {
154            Debug.assert(isOnToken());
155
156            // normally scanner returns the smallest available token
157            // check the kind of context node to determine if scanner should have more greedy behavior and consume more text.
158            const expectedScanAction = shouldRescanGreaterThanToken(n) ? ScanAction.RescanGreaterThanToken :
159                shouldRescanSlashToken(n) ? ScanAction.RescanSlashToken :
160                shouldRescanTemplateToken(n) ? ScanAction.RescanTemplateToken :
161                shouldRescanJsxIdentifier(n) ? ScanAction.RescanJsxIdentifier :
162                shouldRescanJsxText(n) ? ScanAction.RescanJsxText :
163                shouldRescanJsxAttributeValue(n) ? ScanAction.RescanJsxAttributeValue :
164                ScanAction.Scan;
165
166            if (lastTokenInfo && expectedScanAction === lastScanAction) {
167                // readTokenInfo was called before with the same expected scan action.
168                // No need to re-scan text, return existing 'lastTokenInfo'
169                // it is ok to call fixTokenKind here since it does not affect
170                // what portion of text is consumed. In contrast rescanning can change it,
171                // i.e. for '>=' when originally scanner eats just one character
172                // and rescanning forces it to consume more.
173                return fixTokenKind(lastTokenInfo, n);
174            }
175
176            if (scanner.getStartPos() !== savedPos) {
177                Debug.assert(lastTokenInfo !== undefined);
178                // readTokenInfo was called before but scan action differs - rescan text
179                scanner.setTextPos(savedPos);
180                scanner.scan();
181            }
182
183            let currentToken = getNextToken(n, expectedScanAction);
184
185            const token = createTextRangeWithKind(
186                scanner.getStartPos(),
187                scanner.getTextPos(),
188                currentToken,
189            );
190
191            // consume trailing trivia
192            if (trailingTrivia) {
193                trailingTrivia = undefined;
194            }
195            while (scanner.getStartPos() < endPos) {
196                currentToken = scanner.scan();
197                if (!isTrivia(currentToken)) {
198                    break;
199                }
200                const trivia = createTextRangeWithKind(
201                    scanner.getStartPos(),
202                    scanner.getTextPos(),
203                    currentToken,
204                );
205
206                if (!trailingTrivia) {
207                    trailingTrivia = [];
208                }
209
210                trailingTrivia.push(trivia);
211
212                if (currentToken === SyntaxKind.NewLineTrivia) {
213                    // move past new line
214                    scanner.scan();
215                    break;
216                }
217            }
218
219            lastTokenInfo = { leadingTrivia, trailingTrivia, token };
220
221            return fixTokenKind(lastTokenInfo, n);
222        }
223
224        function getNextToken(n: Node, expectedScanAction: ScanAction): SyntaxKind {
225            const token = scanner.getToken();
226            lastScanAction = ScanAction.Scan;
227            switch (expectedScanAction) {
228                case ScanAction.RescanGreaterThanToken:
229                    if (token === SyntaxKind.GreaterThanToken) {
230                        lastScanAction = ScanAction.RescanGreaterThanToken;
231                        const newToken = scanner.reScanGreaterToken();
232                        Debug.assert(n.kind === newToken);
233                        return newToken;
234                    }
235                    break;
236                case ScanAction.RescanSlashToken:
237                    if (startsWithSlashToken(token)) {
238                        lastScanAction = ScanAction.RescanSlashToken;
239                        const newToken = scanner.reScanSlashToken();
240                        Debug.assert(n.kind === newToken);
241                        return newToken;
242                    }
243                    break;
244                case ScanAction.RescanTemplateToken:
245                    if (token === SyntaxKind.CloseBraceToken) {
246                        lastScanAction = ScanAction.RescanTemplateToken;
247                        return scanner.reScanTemplateToken(/* isTaggedTemplate */ false);
248                    }
249                    break;
250                case ScanAction.RescanJsxIdentifier:
251                    lastScanAction = ScanAction.RescanJsxIdentifier;
252                    return scanner.scanJsxIdentifier();
253                case ScanAction.RescanJsxText:
254                    lastScanAction = ScanAction.RescanJsxText;
255                    return scanner.reScanJsxToken();
256                case ScanAction.RescanJsxAttributeValue:
257                    lastScanAction = ScanAction.RescanJsxAttributeValue;
258                    return scanner.reScanJsxAttributeValue();
259                case ScanAction.Scan:
260                    break;
261                default:
262                    Debug.assertNever(expectedScanAction);
263            }
264            return token;
265        }
266
267        function readEOFTokenRange(): TextRangeWithKind<SyntaxKind.EndOfFileToken> {
268            Debug.assert(isOnEOF());
269            return createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), SyntaxKind.EndOfFileToken);
270        }
271
272        function isOnToken(): boolean {
273            const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken();
274            const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos();
275            return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current);
276        }
277
278        function isOnEOF(): boolean {
279            const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken();
280            return current === SyntaxKind.EndOfFileToken;
281        }
282
283        // when containing node in the tree is token
284        // but its kind differs from the kind that was returned by the scanner,
285        // then kind needs to be fixed. This might happen in cases
286        // when parser interprets token differently, i.e keyword treated as identifier
287        function fixTokenKind(tokenInfo: TokenInfo, container: Node): TokenInfo {
288            if (isToken(container) && tokenInfo.token.kind !== container.kind) {
289                tokenInfo.token.kind = container.kind;
290            }
291            return tokenInfo;
292        }
293
294        function skipToEndOf(node: Node): void {
295            scanner.setTextPos(node.end);
296            savedPos = scanner.getStartPos();
297            lastScanAction = undefined;
298            lastTokenInfo = undefined;
299            wasNewLine = false;
300            leadingTrivia = undefined;
301            trailingTrivia = undefined;
302        }
303
304        function skipToStartOf(node: Node): void {
305            scanner.setTextPos(node.pos);
306            savedPos = scanner.getStartPos();
307            lastScanAction = undefined;
308            lastTokenInfo = undefined;
309            wasNewLine = false;
310            leadingTrivia = undefined;
311            trailingTrivia = undefined;
312        }
313    }
314}
315