vendor tsgo
This commit is contained in:
356
tools/tsgo/internal/ls/lsconv/converters.go
Normal file
356
tools/tsgo/internal/ls/lsconv/converters.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package lsconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type Converters struct {
|
||||
getLineMap func(fileName string) *LSPLineMap
|
||||
positionEncoding lsproto.PositionEncodingKind
|
||||
}
|
||||
|
||||
type Script interface {
|
||||
FileName() string
|
||||
Text() string
|
||||
}
|
||||
|
||||
func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LSPLineMap) *Converters {
|
||||
return &Converters{
|
||||
getLineMap: getLineMap,
|
||||
positionEncoding: positionEncoding,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) ToLSPRange(script Script, textRange core.TextRange) lsproto.Range {
|
||||
return lsproto.Range{
|
||||
Start: c.PositionToLineAndCharacter(script, core.TextPos(textRange.Pos())),
|
||||
End: c.PositionToLineAndCharacter(script, core.TextPos(textRange.End())),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) FromLSPRange(script Script, textRange lsproto.Range) core.TextRange {
|
||||
return core.NewTextRange(
|
||||
int(c.LineAndCharacterToPosition(script, textRange.Start)),
|
||||
int(c.LineAndCharacterToPosition(script, textRange.End)),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Converters) FromLSPTextChange(script Script, change *lsproto.TextDocumentContentChangePartial) core.TextChange {
|
||||
return core.TextChange{
|
||||
TextRange: c.FromLSPRange(script, change.Range),
|
||||
NewText: change.Text,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) lsproto.Location {
|
||||
return lsproto.Location{
|
||||
Uri: FileNameToDocumentURI(script.FileName()),
|
||||
Range: c.ToLSPRange(script, rng),
|
||||
}
|
||||
}
|
||||
|
||||
func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind {
|
||||
switch languageID {
|
||||
case "typescript":
|
||||
return core.ScriptKindTS
|
||||
case "typescriptreact":
|
||||
return core.ScriptKindTSX
|
||||
case "javascript":
|
||||
return core.ScriptKindJS
|
||||
case "javascriptreact":
|
||||
return core.ScriptKindJSX
|
||||
case "json":
|
||||
return core.ScriptKindJSON
|
||||
default:
|
||||
return core.ScriptKindUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455
|
||||
var extraEscapeReplacer = strings.NewReplacer(
|
||||
":", "%3A",
|
||||
"/", "%2F",
|
||||
"?", "%3F",
|
||||
"#", "%23",
|
||||
"[", "%5B",
|
||||
"]", "%5D",
|
||||
"@", "%40",
|
||||
|
||||
"!", "%21",
|
||||
"$", "%24",
|
||||
"&", "%26",
|
||||
"'", "%27",
|
||||
"(", "%28",
|
||||
")", "%29",
|
||||
"*", "%2A",
|
||||
"+", "%2B",
|
||||
",", "%2C",
|
||||
";", "%3B",
|
||||
"=", "%3D",
|
||||
|
||||
" ", "%20",
|
||||
)
|
||||
|
||||
func FileNameToDocumentURI(fileName string) lsproto.DocumentUri {
|
||||
if bundled.IsBundled(fileName) {
|
||||
return lsproto.DocumentUri(fileName)
|
||||
}
|
||||
if tspath.IsDynamicFileName(fileName) {
|
||||
scheme, rest, ok := strings.Cut(fileName[2:], "/")
|
||||
if !ok {
|
||||
panic("invalid file name: " + fileName)
|
||||
}
|
||||
authority, path, ok := strings.Cut(rest, "/")
|
||||
if !ok {
|
||||
panic("invalid file name: " + fileName)
|
||||
}
|
||||
if authority == "ts-nul-authority" {
|
||||
return lsproto.DocumentUri(scheme + ":" + path)
|
||||
}
|
||||
return lsproto.DocumentUri(scheme + "://" + authority + "/" + path)
|
||||
}
|
||||
|
||||
volume, fileName, _ := tspath.SplitVolumePath(fileName)
|
||||
if volume != "" {
|
||||
volume = "/" + extraEscapeReplacer.Replace(volume)
|
||||
}
|
||||
|
||||
fileName = strings.TrimPrefix(fileName, "//")
|
||||
|
||||
parts := strings.Split(fileName, "/")
|
||||
for i, part := range parts {
|
||||
parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part))
|
||||
}
|
||||
|
||||
return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/"))
|
||||
}
|
||||
|
||||
func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos {
|
||||
// UTF-8/16 0-indexed line and character to UTF-8 offset
|
||||
|
||||
lineMap := c.getLineMap(script.FileName())
|
||||
|
||||
line := core.TextPos(lineAndCharacter.Line)
|
||||
char := core.TextPos(lineAndCharacter.Character)
|
||||
|
||||
textLen := core.TextPos(len(script.Text()))
|
||||
|
||||
// Clamp line to valid range.
|
||||
if int(line) >= len(lineMap.LineStarts) {
|
||||
return textLen
|
||||
}
|
||||
|
||||
start := lineMap.LineStarts[line]
|
||||
|
||||
// Determine the end of this line (start of next line, or end of text).
|
||||
var lineEnd core.TextPos
|
||||
if int(line)+1 < len(lineMap.LineStarts) {
|
||||
lineEnd = lineMap.LineStarts[int(line)+1]
|
||||
} else {
|
||||
lineEnd = textLen
|
||||
}
|
||||
|
||||
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
|
||||
return max(start, min(start+char, lineEnd))
|
||||
}
|
||||
|
||||
// Scan from line start counting UTF-16 code units to find the byte position.
|
||||
// Uses DecodeRuneInString (not range + RuneLen) so that invalid UTF-8 bytes
|
||||
// advance by their actual size (1) rather than RuneLen(RuneError) == 3.
|
||||
// This matches the approach in scanner.ComputePositionOfLineAndUTF16Character.
|
||||
var utf16Char core.TextPos
|
||||
pos := int(start)
|
||||
end := int(lineEnd)
|
||||
text := script.Text()
|
||||
for pos < end {
|
||||
r, size := utf8.DecodeRuneInString(text[pos:])
|
||||
u16Len := core.TextPos(utf16.RuneLen(r))
|
||||
if utf16Char+u16Len > char {
|
||||
break
|
||||
}
|
||||
utf16Char += u16Len
|
||||
pos += size
|
||||
}
|
||||
|
||||
return core.TextPos(pos)
|
||||
}
|
||||
|
||||
func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position {
|
||||
// UTF-8 offset to UTF-8/16 0-indexed line and character
|
||||
|
||||
position = max(0, min(position, core.TextPos(len(script.Text()))))
|
||||
|
||||
lineMap := c.getLineMap(script.FileName())
|
||||
|
||||
line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position)
|
||||
if !isLineStart {
|
||||
line--
|
||||
}
|
||||
line = max(0, min(line, len(lineMap.LineStarts)-1))
|
||||
|
||||
// The current line ranges from lineMap.LineStarts[line] (or 0) to lineMap.LineStarts[line+1] (or len(text)).
|
||||
|
||||
start := lineMap.LineStarts[line]
|
||||
|
||||
var character core.TextPos
|
||||
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
|
||||
character = position - start
|
||||
} else {
|
||||
// We need to rescan the text as UTF-16 to find the character offset.
|
||||
for _, r := range script.Text()[start:position] {
|
||||
character += core.TextPos(utf16.RuneLen(r))
|
||||
}
|
||||
}
|
||||
|
||||
return lsproto.Position{
|
||||
Line: uint32(line),
|
||||
Character: uint32(character),
|
||||
}
|
||||
}
|
||||
|
||||
type diagnosticOptions struct {
|
||||
reportStyleChecksAsWarnings bool
|
||||
relatedInformation bool
|
||||
tagValueSet []lsproto.DiagnosticTag
|
||||
visualStudio bool
|
||||
}
|
||||
|
||||
// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
|
||||
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, reportStyleChecksAsWarnings bool) *lsproto.Diagnostic {
|
||||
clientCaps := lsproto.GetClientCapabilities(ctx)
|
||||
clientDiagnosticCaps := clientCaps.TextDocument.Diagnostic
|
||||
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
|
||||
reportStyleChecksAsWarnings: reportStyleChecksAsWarnings, // !!! get through context UserPreferences
|
||||
relatedInformation: clientDiagnosticCaps.RelatedInformation,
|
||||
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
|
||||
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
|
||||
})
|
||||
}
|
||||
|
||||
// DiagnosticToLSPPush converts a diagnostic for push diagnostics (textDocument/publishDiagnostics)
|
||||
func DiagnosticToLSPPush(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
|
||||
clientCaps := lsproto.GetClientCapabilities(ctx)
|
||||
clientDiagnosticCaps := clientCaps.TextDocument.PublishDiagnostics
|
||||
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
|
||||
relatedInformation: clientDiagnosticCaps.RelatedInformation,
|
||||
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
|
||||
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/microsoft/vscode/blob/93e08afe0469712706ca4e268f778cfadf1a43ef/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts#L40C7-L40C29
|
||||
var styleCheckDiagnostics = collections.NewSetFromItems(
|
||||
diagnostics.X_0_is_declared_but_never_used.Code(),
|
||||
diagnostics.X_0_is_declared_but_its_value_is_never_read.Code(),
|
||||
diagnostics.Property_0_is_declared_but_its_value_is_never_read.Code(),
|
||||
diagnostics.All_imports_in_import_declaration_are_unused.Code(),
|
||||
diagnostics.Unreachable_code_detected.Code(),
|
||||
diagnostics.Unused_label.Code(),
|
||||
diagnostics.Fallthrough_case_in_switch.Code(),
|
||||
diagnostics.Not_all_code_paths_return_a_value.Code(),
|
||||
)
|
||||
|
||||
func diagnosticToLSP(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, opts diagnosticOptions) *lsproto.Diagnostic {
|
||||
locale := locale.FromContext(ctx)
|
||||
var severity lsproto.DiagnosticSeverity
|
||||
switch diagnostic.Category() {
|
||||
case diagnostics.CategorySuggestion:
|
||||
severity = lsproto.DiagnosticSeverityHint
|
||||
case diagnostics.CategoryMessage:
|
||||
severity = lsproto.DiagnosticSeverityInformation
|
||||
case diagnostics.CategoryWarning:
|
||||
severity = lsproto.DiagnosticSeverityWarning
|
||||
default:
|
||||
severity = lsproto.DiagnosticSeverityError
|
||||
}
|
||||
|
||||
if opts.reportStyleChecksAsWarnings && severity == lsproto.DiagnosticSeverityError && styleCheckDiagnostics.Has(diagnostic.Code()) {
|
||||
severity = lsproto.DiagnosticSeverityWarning
|
||||
}
|
||||
|
||||
var relatedInformation []*lsproto.DiagnosticRelatedInformation
|
||||
if opts.relatedInformation {
|
||||
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
|
||||
for _, related := range diagnostic.RelatedInformation() {
|
||||
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
|
||||
Location: lsproto.Location{
|
||||
Uri: FileNameToDocumentURI(related.File().FileName()),
|
||||
Range: converters.ToLSPRange(related.File(), related.Loc()),
|
||||
},
|
||||
Message: related.Localize(locale),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var tags []lsproto.DiagnosticTag
|
||||
if len(opts.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
|
||||
tags = make([]lsproto.DiagnosticTag, 0, 2)
|
||||
if diagnostic.ReportsUnnecessary() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
|
||||
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
|
||||
}
|
||||
if diagnostic.ReportsDeprecated() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagDeprecated) {
|
||||
tags = append(tags, lsproto.DiagnosticTagDeprecated)
|
||||
}
|
||||
}
|
||||
|
||||
// For diagnostics without a file (e.g., program diagnostics), use a zero range
|
||||
var lspRange lsproto.Range
|
||||
if diagnostic.File() != nil {
|
||||
lspRange = converters.ToLSPRange(diagnostic.File(), diagnostic.Loc())
|
||||
}
|
||||
|
||||
var code *lsproto.IntegerOrString
|
||||
var source *string
|
||||
if opts.visualStudio {
|
||||
code = &lsproto.IntegerOrString{
|
||||
String: new(fmt.Sprintf("TS%d", diagnostic.Code())),
|
||||
}
|
||||
} else {
|
||||
code = &lsproto.IntegerOrString{
|
||||
Integer: new(diagnostic.Code()),
|
||||
}
|
||||
source = new("ts")
|
||||
}
|
||||
|
||||
return &lsproto.Diagnostic{
|
||||
Range: lspRange,
|
||||
Code: code,
|
||||
Severity: &severity,
|
||||
Message: lsproto.StringOrMarkupContent{String: new(messageChainToString(diagnostic, locale))},
|
||||
Source: source,
|
||||
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
|
||||
Tags: ptrToSliceIfNonEmpty(tags),
|
||||
}
|
||||
}
|
||||
|
||||
func messageChainToString(diagnostic *ast.Diagnostic, locale locale.Locale) string {
|
||||
if len(diagnostic.MessageChain()) == 0 {
|
||||
return diagnostic.Localize(locale)
|
||||
}
|
||||
var b strings.Builder
|
||||
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n", locale)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
Reference in New Issue
Block a user