182 lines
6.1 KiB
Go
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
|
|
}
|