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

182 lines
6.1 KiB
Go

package ls
import (
"context"
"iter"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/format"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
func toFormatCodeSettings(opt *lsproto.FormattingOptions, newLine string) *format.FormatCodeSettings {
initial := format.GetDefaultFormatCodeSettings(newLine)
initial.TabSize = int(opt.TabSize)
initial.IndentSize = int(opt.TabSize)
initial.ConvertTabsToSpaces = opt.InsertSpaces
if opt.TrimTrailingWhitespace != nil {
initial.TrimTrailingWhitespace = *opt.TrimTrailingWhitespace
}
// !!! get format settings
// TODO: We support a _lot_ more options than this
return initial
}
func (l *LanguageService) toLSProtoTextEdits(file *ast.SourceFile, changes []core.TextChange) []*lsproto.TextEdit {
result := make([]*lsproto.TextEdit, 0, len(changes))
for _, c := range changes {
result = append(result, &lsproto.TextEdit{
NewText: c.NewText,
Range: *l.createLspRangeFromBounds(c.Pos(), c.End(), file),
})
}
return result
}
func (l *LanguageService) ProvideFormatDocument(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
) (lsproto.DocumentFormattingResponse, error) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) ProvideFormatDocumentRange(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
r lsproto.Range,
) (lsproto.DocumentRangeFormattingResponse, error) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
l.converters.FromLSPRange(file, r),
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) ProvideFormatDocumentOnType(
ctx context.Context,
documentURI lsproto.DocumentUri,
options *lsproto.FormattingOptions,
position lsproto.Position,
character string,
) (lsproto.DocumentOnTypeFormattingResponse, error) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
int(l.converters.LineAndCharacterToPosition(file, position)),
character,
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) getFormattingEditsForRange(
ctx context.Context,
file *ast.SourceFile,
options *format.FormatCodeSettings,
r core.TextRange,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
return format.FormatSelection(ctx, file, r.Pos(), r.End())
}
func (l *LanguageService) getFormattingEditsForDocument(
ctx context.Context,
file *ast.SourceFile,
options *format.FormatCodeSettings,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
return format.FormatDocument(ctx, file)
}
func (l *LanguageService) getFormattingEditsAfterKeystroke(
ctx context.Context,
file *ast.SourceFile,
options *format.FormatCodeSettings,
position int,
key string,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
if isInComment(file, position, nil) == nil {
switch key {
case "{":
return format.FormatOnOpeningCurly(ctx, file, position)
case "}":
return format.FormatOnClosingCurly(ctx, file, position)
case ";":
return format.FormatOnSemicolon(ctx, file, position)
case "\n":
return format.FormatOnEnter(ctx, file, position)
default:
return nil
}
}
return nil
}
// Unlike the TS implementation, this function *will not* compute default values for
// `precedingToken` and `tokenAtPosition`.
// It is the caller's responsibility to call `astnav.GetTokenAtPosition` to compute a default `tokenAtPosition`,
// or `astnav.FindPrecedingToken` to compute a default `precedingToken`.
func getRangeOfEnclosingComment(
file *ast.SourceFile,
position int,
precedingToken *ast.Node,
tokenAtPosition *ast.Node,
) *ast.CommentRange {
jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
if jsdoc != nil {
tokenAtPosition = jsdoc.Parent
}
tokenStart := astnav.GetStartOfNode(tokenAtPosition, file, false /*includeJSDoc*/)
if tokenStart <= position && position < tokenAtPosition.End() {
return nil
}
// Between two consecutive tokens, all comments are either trailing on the former
// or leading on the latter (and none are in both lists).
var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
if precedingToken != nil {
trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, file.Text(), precedingToken.End())
}
leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, file)
commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
for commentRange := range commentRanges {
// The end marker of a single-line comment does not include the newline character.
// In the following case where the cursor is at `^`, we are inside a comment:
//
// // asdf ^\n
//
// But for closed multi-line comments, we don't want to be inside the comment in the following case:
//
// /* asdf */^
//
// Internally, we represent the end of the comment prior to the newline and at the '/', respectively.
//
// However, unterminated multi-line comments lack a `/`, end at the end of the file, and *do* contain their end.
//
if commentRange.ContainsExclusive(position) ||
position == commentRange.End() &&
(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(file.Text())) {
return &commentRange
}
}
return nil
}