Files
kjol/tools/tsgo/internal/ls/format.go
2026-07-09 16:50:43 -04:00

182 lines
6.0 KiB
Go

package ls
import (
"context"
"iter"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/format"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/scanner"
)
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) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument(
ctx,
file,
formatOpts,
))
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) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange(
ctx,
file,
formatOpts,
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) {
if l.UserPreferences().EnableFormatting.IsFalse() {
return lsproto.TextEditsOrNull{}, nil
}
_, file := l.getProgramAndFile(documentURI)
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke(
ctx,
file,
formatOpts,
int(l.converters.LineAndCharacterToPosition(file, position)),
character,
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) getFormattingEditsForRange(
ctx context.Context,
file *ast.SourceFile,
options lsutil.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 lsutil.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 lsutil.FormatCodeSettings,
position int,
key string,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
tokenAtPosition := astnav.GetTokenAtPosition(file, position)
if isInComment(file, position, tokenAtPosition) == 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
}