Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,14 @@ func (f *FourslashTest) nextID() int32 {
}

func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCapabilities) {
initOptions := map[string]any{
// Hack: disable push diagnostics entirely, since the fourslash runner does not
// yet gracefully handle non-request messages.
"disablePushDiagnostics": true,
}
params := &lsproto.InitializeParams{
Locale: ptrTo("en-US"),
Locale: ptrTo("en-US"),
InitializationOptions: ptrTo[any](initOptions),
}
params.Capabilities = getCapabilitiesWithDefaults(capabilities)
// !!! check for errors?
Expand Down
74 changes: 1 addition & 73 deletions internal/ls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package ls

import (
"context"
"slices"
"strings"

"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
Expand Down Expand Up @@ -41,76 +37,8 @@ func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[
lspDiagnostics := make([]*lsproto.Diagnostic, 0, size)
for _, diagSlice := range diagnostics {
for _, diag := range diagSlice {
lspDiagnostics = append(lspDiagnostics, l.toLSPDiagnostic(ctx, diag))
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPull(ctx, l.converters, diag))
}
}
return lspDiagnostics
}

func (l *LanguageService) toLSPDiagnostic(ctx context.Context, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientOptions := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
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
}

var relatedInformation []*lsproto.DiagnosticRelatedInformation
if clientOptions.RelatedInformation {
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
for _, related := range diagnostic.RelatedInformation() {
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
Location: lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(related.File().FileName()),
Range: l.converters.ToLSPRange(related.File(), related.Loc()),
},
Message: related.Message(),
})
}
}

var tags []lsproto.DiagnosticTag
if len(clientOptions.TagSupport.ValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagUnnecessary) {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() && slices.Contains(clientOptions.TagSupport.ValueSet, lsproto.DiagnosticTagDeprecated) {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}

return &lsproto.Diagnostic{
Range: l.converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()),
Code: &lsproto.IntegerOrString{
Integer: ptrTo(diagnostic.Code()),
},
Severity: &severity,
Message: messageChainToString(diagnostic),
Source: ptrTo("ts"),
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}

func messageChainToString(diagnostic *ast.Diagnostic) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Message()
}
var b strings.Builder
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
return b.String()
}

func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}
100 changes: 100 additions & 0 deletions internal/ls/lsconv/converters.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/tspath"
)
Expand Down Expand Up @@ -199,3 +203,99 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex
func ptrTo[T any](v T) *T {
return &v
}

type diagnosticCapabilities struct {
relatedInformation bool
tagValueSet []lsproto.DiagnosticTag
}

// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
clientCaps := lsproto.GetClientCapabilities(ctx).TextDocument.Diagnostic
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
relatedInformation: clientCaps.RelatedInformation,
tagValueSet: clientCaps.TagSupport.ValueSet,
})
}

// 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).TextDocument.PublishDiagnostics
return diagnosticToLSP(converters, diagnostic, diagnosticCapabilities{
relatedInformation: clientCaps.RelatedInformation,
tagValueSet: clientCaps.TagSupport.ValueSet,
})
}

func diagnosticToLSP(converters *Converters, diagnostic *ast.Diagnostic, caps diagnosticCapabilities) *lsproto.Diagnostic {
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
}

var relatedInformation []*lsproto.DiagnosticRelatedInformation
if caps.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.Message(),
})
}
}

var tags []lsproto.DiagnosticTag
if len(caps.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() && slices.Contains(caps.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() && slices.Contains(caps.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())
}

return &lsproto.Diagnostic{
Range: lspRange,
Code: &lsproto.IntegerOrString{
Integer: ptrTo(diagnostic.Code()),
},
Severity: &severity,
Message: messageChainToString(diagnostic),
Source: ptrTo("ts"),
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}

func messageChainToString(diagnostic *ast.Diagnostic) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Message()
}
var b strings.Builder
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n")
return b.String()
}

func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}
76 changes: 43 additions & 33 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ func (s *Server) RefreshDiagnostics(ctx context.Context) error {
return nil
}

// PublishDiagnostics implements project.Client.
func (s *Server) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
notification := lsproto.TextDocumentPublishDiagnosticsInfo.NewNotificationMessage(params)
s.outgoingQueue <- notification.Message()
return nil
}

func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferences, error) {
caps := lsproto.GetClientCapabilities(ctx)
if !caps.Workspace.Configuration {
Expand Down Expand Up @@ -716,6 +723,16 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
cwd = s.cwd
}

var disablePushDiagnostics bool
if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
// Check for disablePushDiagnostics option
if initOpts, ok := (*s.initializeParams.InitializationOptions).(map[string]any); ok {
if disable, ok := initOpts["disablePushDiagnostics"].(bool); ok {
disablePushDiagnostics = disable
}
}
}

s.session = project.NewSession(&project.SessionInit{
Options: &project.SessionOptions{
CurrentDirectory: cwd,
Expand All @@ -726,43 +743,36 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
LoggingEnabled: true,
DebounceDelay: 500 * time.Millisecond,
},
FS: s.fs,
Logger: s.logger,
Client: s,
NpmExecutor: s,
ParseCache: s.parseCache,
FS: s.fs,
Logger: s.logger,
Client: s,
NpmExecutor: s,
ParseCache: s.parseCache,
DisablePushDiagnostics: disablePushDiagnostics,
})

if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && *s.initializeParams.InitializationOptions != nil {
// handle userPreferences from initializationOptions
userPreferences := s.session.NewUserPreferences()
userPreferences.Parse(*s.initializeParams.InitializationOptions)
s.session.InitializeWithConfig(userPreferences)
Comment on lines -736 to -740
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code was unused

} else {
// request userPreferences if not provided at initialization
userPreferences, err := s.RequestConfiguration(ctx)
if err != nil {
return err
}
s.session.InitializeWithConfig(userPreferences)
userPreferences, err := s.RequestConfiguration(ctx)
if err != nil {
return err
}
s.session.InitializeWithConfig(userPreferences)

_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
Registrations: []*lsproto.Registration{
{
Id: "typescript-config-watch-id",
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
Section: &lsproto.StringOrStrings{
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
Strings: &[]string{"typescript"},
},
})),
},
_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
Registrations: []*lsproto.Registration{
{
Id: "typescript-config-watch-id",
Method: string(lsproto.MethodWorkspaceDidChangeConfiguration),
RegisterOptions: ptrTo(any(lsproto.DidChangeConfigurationRegistrationOptions{
Section: &lsproto.StringOrStrings{
// !!! Both the 'javascript' and 'js/ts' scopes need to be watched for settings as well.
Strings: &[]string{"typescript"},
},
})),
},
})
if err != nil {
return fmt.Errorf("failed to register configuration change watcher: %w", err)
}
},
})
if err != nil {
return fmt.Errorf("failed to register configuration change watcher: %w", err)
}

// !!! temporary.
Expand Down
1 change: 1 addition & 0 deletions internal/project/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ type Client interface {
WatchFiles(ctx context.Context, id WatcherID, watchers []*lsproto.FileSystemWatcher) error
UnwatchFiles(ctx context.Context, id WatcherID) error
RefreshDiagnostics(ctx context.Context) error
PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error
}
Loading
Loading