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