186 lines
7.0 KiB
Go
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)
|
|
}
|