2025-10-15 10:12:44 +03:00

356 lines
9.2 KiB
Go

package format
import (
"slices"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
type TextRangeWithKind struct {
Loc core.TextRange
Kind ast.Kind
}
func NewTextRangeWithKind(pos int, end int, kind ast.Kind) TextRangeWithKind {
return TextRangeWithKind{
Loc: core.NewTextRange(pos, end),
Kind: kind,
}
}
type tokenInfo struct {
leadingTrivia []TextRangeWithKind
token TextRangeWithKind
trailingTrivia []TextRangeWithKind
}
type formattingScanner struct {
s *scanner.Scanner
startPos int
endPos int
savedPos int
hasLastTokenInfo bool
lastTokenInfo tokenInfo
lastScanAction scanAction
leadingTrivia []TextRangeWithKind
trailingTrivia []TextRangeWithKind
wasNewLine bool
}
func newFormattingScanner(text string, languageVariant core.LanguageVariant, startPos int, endPos int, worker *formatSpanWorker) []core.TextChange {
scan := scanner.NewScanner()
scan.Reset()
scan.SetSkipTrivia(false)
scan.SetLanguageVariant(languageVariant)
scan.SetText(text)
fmtScn := &formattingScanner{
s: scan,
startPos: startPos,
endPos: endPos,
wasNewLine: true,
}
res := worker.execute(fmtScn)
fmtScn.hasLastTokenInfo = false
scan.Reset()
return res
}
func (s *formattingScanner) advance() {
s.hasLastTokenInfo = false
isStarted := s.s.TokenFullStart() != s.startPos
if isStarted {
s.wasNewLine = len(s.trailingTrivia) > 0 && core.LastOrNil(s.trailingTrivia).Kind == ast.KindNewLineTrivia
} else {
s.s.Scan()
}
s.leadingTrivia = nil
s.trailingTrivia = nil
pos := s.s.TokenFullStart()
// Read leading trivia and token
for pos < s.endPos {
t := s.s.Token()
if !ast.IsTrivia(t) {
break
}
// consume leading trivia
s.s.Scan()
item := NewTextRangeWithKind(pos, s.s.TokenFullStart(), t)
pos = s.s.TokenFullStart()
s.leadingTrivia = append(s.leadingTrivia, item)
}
s.savedPos = s.s.TokenFullStart()
}
func shouldRescanGreaterThanToken(node *ast.Node) bool {
switch node.Kind {
case ast.KindGreaterThanEqualsToken,
ast.KindGreaterThanGreaterThanEqualsToken,
ast.KindGreaterThanGreaterThanGreaterThanEqualsToken,
ast.KindGreaterThanGreaterThanGreaterThanToken,
ast.KindGreaterThanGreaterThanToken:
return true
}
return false
}
func shouldRescanJsxIdentifier(node *ast.Node) bool {
if node.Parent != nil {
switch node.Parent.Kind {
case ast.KindJsxAttribute,
ast.KindJsxOpeningElement,
ast.KindJsxClosingElement,
ast.KindJsxSelfClosingElement:
// 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.
return ast.IsKeywordKind(node.Kind) || node.Kind == ast.KindIdentifier
}
}
return false
}
func (s *formattingScanner) shouldRescanJsxText(node *ast.Node) bool {
if ast.IsJsxText(node) {
return true
}
if !ast.IsJsxElement(node) || s.hasLastTokenInfo == false {
return false
}
return s.lastTokenInfo.token.Kind == ast.KindJsxText
}
func shouldRescanSlashToken(container *ast.Node) bool {
return container.Kind == ast.KindRegularExpressionLiteral
}
func shouldRescanTemplateToken(container *ast.Node) bool {
return container.Kind == ast.KindTemplateMiddle ||
container.Kind == ast.KindTemplateTail
}
func shouldRescanJsxAttributeValue(node *ast.Node) bool {
return node.Parent != nil && ast.IsJsxAttribute(node.Parent) && node.Parent.Initializer() == node
}
func startsWithSlashToken(t ast.Kind) bool {
return t == ast.KindSlashToken || t == ast.KindSlashEqualsToken
}
type scanAction int
const (
actionScan scanAction = iota
actionRescanGreaterThanToken
actionRescanSlashToken
actionRescanTemplateToken
actionRescanJsxIdentifier
actionRescanJsxText
actionRescanJsxAttributeValue
)
func fixTokenKind(tokenInfo tokenInfo, container *ast.Node) tokenInfo {
if ast.IsTokenKind(container.Kind) && tokenInfo.token.Kind != container.Kind {
tokenInfo.token.Kind = container.Kind
}
return tokenInfo
}
func (s *formattingScanner) readTokenInfo(n *ast.Node) tokenInfo {
debug.Assert(s.isOnToken())
// normally scanner returns the smallest available token
// check the kind of context node to determine if scanner should have more greedy behavior and consume more text.
var expectedScanAction scanAction
if shouldRescanGreaterThanToken(n) {
expectedScanAction = actionRescanGreaterThanToken
} else if shouldRescanSlashToken(n) {
expectedScanAction = actionRescanSlashToken
} else if shouldRescanTemplateToken(n) {
expectedScanAction = actionRescanTemplateToken
} else if shouldRescanJsxIdentifier(n) {
expectedScanAction = actionRescanJsxIdentifier
} else if s.shouldRescanJsxText(n) {
expectedScanAction = actionRescanJsxText
} else if shouldRescanJsxAttributeValue(n) {
expectedScanAction = actionRescanJsxAttributeValue
} else {
expectedScanAction = actionScan
}
if s.hasLastTokenInfo && expectedScanAction == s.lastScanAction {
// readTokenInfo was called before with the same expected scan action.
// No need to re-scan text, return existing 'lastTokenInfo'
// it is ok to call fixTokenKind here since it does not affect
// what portion of text is consumed. In contrast rescanning can change it,
// i.e. for '>=' when originally scanner eats just one character
// and rescanning forces it to consume more.
s.lastTokenInfo = fixTokenKind(s.lastTokenInfo, n)
return s.lastTokenInfo
}
if s.s.TokenFullStart() != s.savedPos {
// readTokenInfo was called before but scan action differs - rescan text
s.s.ResetTokenState(s.savedPos)
s.s.Scan()
}
currentToken := s.getNextToken(n, expectedScanAction)
token := NewTextRangeWithKind(
s.s.TokenFullStart(),
s.s.TokenEnd(),
currentToken,
)
// consume trailing trivia
s.trailingTrivia = nil
for s.s.TokenFullStart() < s.endPos {
currentToken = s.s.Scan()
if !ast.IsTrivia(currentToken) {
break
}
trivia := NewTextRangeWithKind(
s.s.TokenFullStart(),
s.s.TokenEnd(),
currentToken,
)
s.trailingTrivia = append(s.trailingTrivia, trivia)
if currentToken == ast.KindNewLineTrivia {
// move past new line
s.s.Scan()
break
}
}
s.hasLastTokenInfo = true
s.lastTokenInfo = tokenInfo{
leadingTrivia: slices.Clone(s.leadingTrivia),
token: token,
trailingTrivia: slices.Clone(s.trailingTrivia),
}
s.lastTokenInfo = fixTokenKind(s.lastTokenInfo, n)
return s.lastTokenInfo
}
func (s *formattingScanner) getNextToken(n *ast.Node, expectedScanAction scanAction) ast.Kind {
token := s.s.Token()
s.lastScanAction = actionScan
switch expectedScanAction {
case actionRescanGreaterThanToken:
if token == ast.KindGreaterThanToken {
s.lastScanAction = actionRescanGreaterThanToken
newToken := s.s.ReScanGreaterThanToken()
debug.Assert(n.Kind == newToken)
return newToken
}
case actionRescanSlashToken:
if startsWithSlashToken(token) {
s.lastScanAction = actionRescanSlashToken
newToken := s.s.ReScanSlashToken()
debug.Assert(n.Kind == newToken)
return newToken
}
case actionRescanTemplateToken:
if token == ast.KindCloseBraceToken {
s.lastScanAction = actionRescanTemplateToken
return s.s.ReScanTemplateToken( /*isTaggedTemplate*/ false)
}
case actionRescanJsxIdentifier:
s.lastScanAction = actionRescanJsxIdentifier
return s.s.ScanJsxIdentifier()
case actionRescanJsxText:
s.lastScanAction = actionRescanJsxText
return s.s.ReScanJsxToken( /*allowMultilineJsxText*/ false)
case actionRescanJsxAttributeValue:
s.lastScanAction = actionRescanJsxAttributeValue
return s.s.ReScanJsxAttributeValue()
case actionScan:
break
default:
debug.AssertNever(expectedScanAction, "unhandled scan action kind")
}
return token
}
func (s *formattingScanner) readEOFTokenRange() TextRangeWithKind {
debug.Assert(s.isOnEOF())
return NewTextRangeWithKind(
s.s.TokenFullStart(),
s.s.TokenEnd(),
ast.KindEndOfFile,
)
}
func (s *formattingScanner) isOnToken() bool {
current := s.s.Token()
if s.hasLastTokenInfo {
current = s.lastTokenInfo.token.Kind
}
return current != ast.KindEndOfFile && !ast.IsTrivia(current)
}
func (s *formattingScanner) isOnEOF() bool {
current := s.s.Token()
if s.hasLastTokenInfo {
current = s.lastTokenInfo.token.Kind
}
return current == ast.KindEndOfFile
}
func (s *formattingScanner) skipToEndOf(r *core.TextRange) {
s.s.ResetTokenState(r.End())
s.savedPos = s.s.TokenFullStart()
s.lastScanAction = actionScan
s.hasLastTokenInfo = false
s.wasNewLine = false
s.leadingTrivia = nil
s.trailingTrivia = nil
}
func (s *formattingScanner) skipToStartOf(r *core.TextRange) {
s.s.ResetTokenState(r.Pos())
s.savedPos = s.s.TokenFullStart()
s.lastScanAction = actionScan
s.hasLastTokenInfo = false
s.wasNewLine = false
s.leadingTrivia = nil
s.trailingTrivia = nil
}
func (s *formattingScanner) getCurrentLeadingTrivia() []TextRangeWithKind {
return s.leadingTrivia
}
func (s *formattingScanner) lastTrailingTriviaWasNewLine() bool {
return s.wasNewLine
}
func (s *formattingScanner) getTokenFullStart() int {
if s.hasLastTokenInfo {
return s.lastTokenInfo.token.Loc.Pos()
}
return s.s.TokenFullStart()
}
func (s *formattingScanner) getStartPos() int { // TODO: redundant?
return s.getTokenFullStart()
}