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

186 lines
7.0 KiB
Go

package format
import (
"context"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
)
type FormatRequestKind int
const (
FormatRequestKindFormatDocument FormatRequestKind = iota
FormatRequestKindFormatSelection
FormatRequestKindFormatOnEnter
FormatRequestKindFormatOnSemicolon
FormatRequestKindFormatOnOpeningCurlyBrace
FormatRequestKindFormatOnClosingCurlyBrace
)
type formatContextKey int
const (
formatOptionsKey formatContextKey = iota
formatNewlineKey
)
func WithFormatCodeSettings(ctx context.Context, options *FormatCodeSettings, newLine string) context.Context {
ctx = context.WithValue(ctx, formatOptionsKey, options)
ctx = context.WithValue(ctx, formatNewlineKey, newLine)
// In strada, the rules map was both globally cached *and* cached into the context, for some reason. We skip that here and just use the global one.
return ctx
}
func GetFormatCodeSettingsFromContext(ctx context.Context) *FormatCodeSettings {
opt := ctx.Value(formatOptionsKey).(*FormatCodeSettings)
return opt
}
func GetNewLineOrDefaultFromContext(ctx context.Context) string { // TODO: Move into broader LS - more than just the formatter uses the newline editor setting/host new line
opt := GetFormatCodeSettingsFromContext(ctx)
if opt != nil && len(opt.NewLineCharacter) > 0 {
return opt.NewLineCharacter
}
host := ctx.Value(formatNewlineKey).(string)
if len(host) > 0 {
return host
}
return "\n"
}
func FormatSpan(ctx context.Context, span core.TextRange, file *ast.SourceFile, kind FormatRequestKind) []core.TextChange {
// find the smallest node that fully wraps the range and compute the initial indentation for the node
enclosingNode := findEnclosingNode(span, file)
opts := GetFormatCodeSettingsFromContext(ctx)
return newFormattingScanner(
file.Text(),
file.LanguageVariant,
getScanStartPosition(enclosingNode, span, file),
span.End(),
newFormatSpanWorker(
ctx,
span,
enclosingNode,
GetIndentationForNode(enclosingNode, &span, file, opts),
getOwnOrInheritedDelta(enclosingNode, opts, file),
kind,
prepareRangeContainsErrorFunction(file.Diagnostics(), span),
file,
),
)
}
func FormatNodeGivenIndentation(ctx context.Context, node *ast.Node, file *ast.SourceFile, languageVariant core.LanguageVariant, initialIndentation int, delta int) []core.TextChange {
textRange := core.NewTextRange(node.Pos(), node.End())
return newFormattingScanner(
file.Text(),
languageVariant,
textRange.Pos(),
textRange.End(),
newFormatSpanWorker(
ctx,
textRange,
node,
initialIndentation,
delta,
FormatRequestKindFormatSelection,
func(core.TextRange) bool { return false }, // assume that node does not have any errors
file,
))
}
func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast.Node, requestKind FormatRequestKind) []core.TextChange {
if node == nil {
return nil
}
tokenStart := scanner.GetTokenPosOfNode(node, sourceFile, false)
lineStart := GetLineStartPositionForPosition(tokenStart, sourceFile)
span := core.NewTextRange(lineStart, node.End())
return FormatSpan(ctx, span, sourceFile, requestKind)
}
func FormatDocument(ctx context.Context, sourceFile *ast.SourceFile) []core.TextChange {
return FormatSpan(ctx, core.NewTextRange(0, sourceFile.End()), sourceFile, FormatRequestKindFormatDocument)
}
func FormatSelection(ctx context.Context, sourceFile *ast.SourceFile, start int, end int) []core.TextChange {
return FormatSpan(ctx, core.NewTextRange(GetLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection)
}
func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
openingCurly := findImmediatelyPrecedingTokenOfKind(position, ast.KindOpenBraceToken, sourceFile)
if openingCurly == nil {
return nil
}
curlyBraceRange := openingCurly.Parent
outermostNode := findOutermostNodeWithinListLevel(curlyBraceRange)
/**
* We limit the span to end at the opening curly to handle the case where
* the brace matched to that just typed will be incorrect after further edits.
* For example, we could type the opening curly for the following method
* body without brace-matching activated:
* ```
* class C {
* foo()
* }
* ```
* and we wouldn't want to move the closing brace.
*/
textRange := core.NewTextRange(GetLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position)
return FormatSpan(ctx, textRange, sourceFile, FormatRequestKindFormatOnOpeningCurlyBrace)
}
func FormatOnClosingCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
precedingToken := findImmediatelyPrecedingTokenOfKind(position, ast.KindCloseBraceToken, sourceFile)
return formatNodeLines(ctx, sourceFile, findOutermostNodeWithinListLevel(precedingToken), FormatRequestKindFormatOnClosingCurlyBrace)
}
func FormatOnSemicolon(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
semicolon := findImmediatelyPrecedingTokenOfKind(position, ast.KindSemicolonToken, sourceFile)
return formatNodeLines(ctx, sourceFile, findOutermostNodeWithinListLevel(semicolon), FormatRequestKindFormatOnSemicolon)
}
func FormatOnEnter(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange {
line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, position)
if line == 0 {
return nil
}
// get start position for the previous line
startPos := int(scanner.GetECMALineStarts(sourceFile)[line-1])
// After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters.
// If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as
// trailing whitespaces. So the end of the formatting span should be the later one between:
// 1. the end of the previous line
// 2. the last non-whitespace character in the current line
endOfFormatSpan := scanner.GetECMAEndLinePosition(sourceFile, line)
for endOfFormatSpan > startPos {
ch, s := utf8.DecodeRuneInString(sourceFile.Text()[endOfFormatSpan:])
if s == 0 || stringutil.IsWhiteSpaceSingleLine(ch) { // on multibyte character keep backing up
endOfFormatSpan--
continue
}
break
}
// if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to
// touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the
// previous character before the end of format span is line break character as well.
ch, _ := utf8.DecodeRuneInString(sourceFile.Text()[endOfFormatSpan:])
if stringutil.IsLineBreak(ch) {
endOfFormatSpan--
}
span := core.NewTextRange(
startPos,
// end value is exclusive so add 1 to the result
endOfFormatSpan+1,
)
return FormatSpan(ctx, span, sourceFile, FormatRequestKindFormatOnEnter)
}