356 lines
9.2 KiB
Go
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()
|
|
}
|