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