Skip to content
Merged
1 change: 1 addition & 0 deletions internal/ls/lsutil/userpreferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ type UserPreferences struct {
DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!!
DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!!
ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"`
Locale string `config:"locale"`

// ------- ATA -------

Expand Down
15 changes: 15 additions & 0 deletions internal/ls/lsutil/userpreferences_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,21 @@ func TestUserPreferencesParseUnstable(t *testing.T) {
}
}

func TestUserPreferencesLocale(t *testing.T) {
t.Parallel()

prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"locale": "de",
},
"js/ts": map[string]any{
"locale": "fr",
},
})

assert.Equal(t, prefs.Locale, "fr")
}

func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
t.Parallel()

Expand Down
30 changes: 28 additions & 2 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ type Server struct {
initializationOptions *lsproto.InitializationOptions
clientCapabilities lsproto.ResolvedClientCapabilities
positionEncoding lsproto.PositionEncodingKind
localeMu sync.RWMutex
locale locale.Locale
// initLocale is the locale resolved from the initialize request; it is
// used as the fallback when the user's locale preference is "auto".
initLocale locale.Locale

watchEnabled bool
telemetryEnabled bool
Expand Down Expand Up @@ -362,6 +366,28 @@ func (s *Server) ProgressFinish(message *diagnostics.Message, args ...any) {
}
}

// GetLocale implements project.Client.
func (s *Server) GetLocale() locale.Locale {
s.localeMu.RLock()
defer s.localeMu.RUnlock()
return s.locale
}

// SetLocale implements project.Client.
func (s *Server) SetLocale(localeString string) {
newLocale := s.initLocale
if localeString != "auto" {
parsed, ok := locale.Parse(localeString)
if !ok {
return
}
newLocale = parsed
}
s.localeMu.Lock()
s.locale = newLocale
s.localeMu.Unlock()
}

func (s *Server) RequestConfiguration(ctx context.Context) (lsutil.UserPreferences, error) {
caps := lsproto.GetClientCapabilities(ctx)
if !caps.Workspace.Configuration {
Expand Down Expand Up @@ -539,7 +565,7 @@ func (s *Server) dispatchLoop(ctx context.Context) error {
}

s.lastRequestTimeMs.Store(time.Now().UnixMilli())
requestCtx := locale.WithLocale(ctx, s.locale)
requestCtx := locale.WithLocale(ctx, s.GetLocale())
var cancel context.CancelFunc
if req.ID != nil {
requestCtx, cancel = context.WithCancel(core.WithRequestID(requestCtx, req.ID.String()))
Expand Down Expand Up @@ -1057,6 +1083,7 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ
if s.initializeParams.Locale != nil {
s.locale, _ = locale.Parse(*s.initializeParams.Locale)
}
s.initLocale = s.locale

if s.startWatchdog != nil && params.ProcessId.Integer != nil {
s.startWatchdog(int(*params.ProcessId.Integer))
Expand Down Expand Up @@ -1254,7 +1281,6 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
TelemetryEnabled: enableTelemetry,
DebounceDelay: 500 * time.Millisecond,
PushDiagnosticsEnabled: !disablePushDiagnostics,
Locale: s.locale,
},
FS: s.fs,
Logger: s.logger,
Expand Down
5 changes: 5 additions & 0 deletions internal/project/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)

Expand All @@ -18,4 +19,8 @@ type Client interface {
ProgressFinish(message *diagnostics.Message, args ...any)
SendTelemetry(ctx context.Context, telemetry lsproto.TelemetryEvent) error
IsActive() bool
// SetLocale updates the locale used for diagnostic messages.
SetLocale(locale string)
// GetLocale returns the current display locale for diagnostic messages.
GetLocale() locale.Locale
}
5 changes: 5 additions & 0 deletions internal/project/extendedconfigcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/project/logging"
"github.com/microsoft/typescript-go/internal/tsoptions"
Expand Down Expand Up @@ -44,6 +45,10 @@ func (noopClient) SendTelemetry(ctx context.Context, telemetry lsproto.Telemetry

func (noopClient) IsActive() bool { return true }

func (noopClient) GetLocale() locale.Locale { return locale.Default }

func (noopClient) SetLocale(locale string) {}

// TestExtendedConfigCacheOwnership tests the invariant that each ExtendedSourceFile
// of a config in the ConfigFileRegistry is owned exactly once per snapshot that
// references it, and released exactly once when that snapshot is removed.
Expand Down
47 changes: 31 additions & 16 deletions internal/project/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ type SessionOptions struct {
TelemetryEnabled bool
PushDiagnosticsEnabled bool
DebounceDelay time.Duration
Locale locale.Locale
CheckerPoolOptions CheckerPoolOptions
}

Expand Down Expand Up @@ -268,6 +267,17 @@ func (s *Session) Config() lsutil.UserPreferences {
return s.workspaceUserPreferences
}

func (s *Session) backgroundContext() context.Context {
return s.withCurrentLocale(s.backgroundCtx)
}

func (s *Session) withCurrentLocale(ctx context.Context) context.Context {
if s.client == nil {
return ctx
}
return locale.WithLocale(ctx, s.client.GetLocale())
}

// Trace implements module.ResolutionHost
func (s *Session) Trace(msg string) {
panic("ATA module resolution should not use tracing")
Expand All @@ -280,6 +290,10 @@ func (s *Session) Configure(config lsutil.UserPreferences) {
oldConfig := s.workspaceUserPreferences
s.workspaceUserPreferences = config

if config.Locale != "" {
s.client.SetLocale(config.Locale)
}

// Tell the client to re-request certain commands depending on user preference changes.
s.refreshInlayHintsIfNeeded(oldConfig, config)
s.refreshCodeLensIfNeeded(oldConfig, config)
Expand Down Expand Up @@ -404,7 +418,7 @@ func (s *Session) ScheduleDiagnosticsRefresh() {
}

// Create a new cancellable context for the debounce task
debounceCtx, cancel := context.WithCancel(s.backgroundCtx)
debounceCtx, cancel := context.WithCancel(s.backgroundContext())
s.diagnosticsRefreshGeneration++
generation := s.diagnosticsRefreshGeneration
s.diagnosticsRefreshCancel = cancel
Expand Down Expand Up @@ -433,7 +447,7 @@ func (s *Session) ScheduleDiagnosticsRefresh() {
if s.options.LoggingEnabled {
s.logger.Log("Running scheduled diagnostics refresh")
}
if err := s.client.RefreshDiagnostics(s.backgroundCtx); err != nil && s.options.LoggingEnabled {
if err := s.client.RefreshDiagnostics(s.backgroundContext()); err != nil && s.options.LoggingEnabled {
s.logger.Logf("Error refreshing diagnostics: %v", err)
}
})
Expand Down Expand Up @@ -465,7 +479,7 @@ func (s *Session) ScheduleSnapshotUpdate(reason UpdateReason) {
}

// Create a new cancellable context for the debounce task
debounceCtx, cancel := context.WithCancel(s.backgroundCtx)
debounceCtx, cancel := context.WithCancel(s.backgroundContext())
s.scheduledSnapshotUpdateGeneration++
generation := s.scheduledSnapshotUpdateGeneration
s.scheduledSnapshotUpdateCancel = cancel
Expand Down Expand Up @@ -553,7 +567,7 @@ func (s *Session) scheduleIdleCacheClean() {
defer s.snapshotUpdateMu.Unlock()
s.cancelScheduledSnapshotUpdate()

ctx := s.backgroundCtx
ctx := s.backgroundContext()
fileChanges, overlays, ataChanges, newConfig := s.flushChanges(ctx)
s.UpdateSnapshot(ctx, overlays, SnapshotChange{
reason: UpdateReasonIdleCleanDiskCache,
Expand Down Expand Up @@ -584,7 +598,7 @@ func (s *Session) StartPerformanceTelemetry() {
if !s.options.TelemetryEnabled {
return
}
ctx, cancel := context.WithCancel(s.backgroundCtx)
ctx, cancel := context.WithCancel(s.backgroundContext())
s.performanceTelemetryCancel = cancel
s.backgroundQueue.Enqueue(ctx, func(ctx context.Context) {
ticker := time.NewTicker(performanceTelemetryInterval)
Expand Down Expand Up @@ -736,7 +750,7 @@ func (s *Session) sendProjectInfoTelemetryForNewProjects(oldSnapshot *Snapshot,
if !s.options.TelemetryEnabled {
return
}
ctx := s.backgroundCtx
ctx := s.backgroundContext()
collections.DiffOrderedMaps(
oldSnapshot.ProjectCollection.ProjectsByPath(),
newSnapshot.ProjectCollection.ProjectsByPath(),
Expand Down Expand Up @@ -1170,7 +1184,7 @@ func (s *Session) adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot *Sna
// The clone's initial ref (1) is transferred to adoptSnapshotChange,
// which will either promote it as the session's current snapshot or
// release it if the session has moved on.
s.backgroundQueue.Enqueue(s.backgroundCtx, func(ctx context.Context) {
s.backgroundQueue.Enqueue(s.backgroundContext(), func(ctx context.Context) {
s.adoptSnapshotChange(baseSnapshot, newSnapshot)
})
}
Expand Down Expand Up @@ -1245,7 +1259,7 @@ func (s *Session) updateSnapshot(ctx context.Context, overlays map[tspath.Path]*

// Enqueue logging, watch updates, and diagnostic refresh tasks
// !!! userPreferences/configuration updates
s.backgroundQueue.Enqueue(s.backgroundCtx, func(ctx context.Context) {
s.backgroundQueue.Enqueue(s.backgroundContext(), func(ctx context.Context) {
if s.options.LoggingEnabled {
s.logger.Logf("Adopted snapshot %d (parent %d) as current session snapshot (replacing %d)", newSnapshot.id, newSnapshot.parentId, oldSnapshot.id)
s.logger.Log(newSnapshot.builderLogs.String())
Expand Down Expand Up @@ -1355,7 +1369,7 @@ func updateWatch[T any](ctx context.Context, session *Session, logger logging.Lo
func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) error {
var errors []error
start := time.Now()
ctx := s.backgroundCtx
ctx := s.backgroundContext()
core.DiffMapsFunc(
oldSnapshot.ConfigFileRegistry.configs,
newSnapshot.ConfigFileRegistry.configs,
Expand Down Expand Up @@ -1577,15 +1591,15 @@ func (s *Session) NpmInstall(cwd string, npmInstallArgs []string) ([]byte, error

func (s *Session) refreshInlayHintsIfNeeded(oldPrefs lsutil.UserPreferences, newPrefs lsutil.UserPreferences) {
if oldPrefs.InlayHints != newPrefs.InlayHints {
if err := s.client.RefreshInlayHints(s.backgroundCtx); err != nil && s.options.LoggingEnabled {
if err := s.client.RefreshInlayHints(s.backgroundContext()); err != nil && s.options.LoggingEnabled {
s.logger.Logf("Error refreshing inlay hints: %v", err)
}
}
}

func (s *Session) refreshCodeLensIfNeeded(oldPrefs lsutil.UserPreferences, newPrefs lsutil.UserPreferences) {
if oldPrefs.CodeLens != newPrefs.CodeLens {
if err := s.client.RefreshCodeLens(s.backgroundCtx); err != nil && s.options.LoggingEnabled {
if err := s.client.RefreshCodeLens(s.backgroundContext()); err != nil && s.options.LoggingEnabled {
s.logger.Logf("Error refreshing code lens: %v", err)
}
}
Expand Down Expand Up @@ -1617,13 +1631,13 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot *
}
for configFilePath, oldProject := range oldSnapshot.ProjectCollection.ProjectsByPath().Entries() {
if oldProject.Kind == KindConfigured && oldSnapshot.ProjectCollection.GetOpenConfiguredProjects().Has(configFilePath) {
s.publishProjectDiagnostics(s.backgroundCtx, string(configFilePath), nil, oldSnapshot.converters)
s.publishProjectDiagnostics(s.backgroundContext(), string(configFilePath), nil, oldSnapshot.converters)
}
}
return
}

ctx := s.backgroundCtx
ctx := s.backgroundContext()
oldProjects := oldSnapshot.ProjectCollection.ProjectsByPath()
newProjects := newSnapshot.ProjectCollection.ProjectsByPath()
oldOpenProjects := oldSnapshot.ProjectCollection.GetOpenConfiguredProjects()
Expand Down Expand Up @@ -1683,6 +1697,7 @@ func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath
if s.Config().EnableValidation.IsFalse() {
diagnostics = nil
}
ctx = s.withCurrentLocale(ctx)
lspDiagnostics := make([]*lsproto.Diagnostic, 0, len(diagnostics))
for _, diag := range diagnostics {
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPush(ctx, converters, diag))
Expand All @@ -1704,7 +1719,7 @@ func (s *Session) EnqueuePublishGlobalDiagnostics() {
return
}
if s.globalDiagPublishPending.CompareAndSwap(false, true) {
s.backgroundQueue.Enqueue(s.backgroundCtx, s.publishGlobalDiagnostics)
s.backgroundQueue.Enqueue(s.backgroundContext(), s.publishGlobalDiagnostics)
}
}

Expand All @@ -1730,7 +1745,7 @@ func (s *Session) publishGlobalDiagnostics(ctx context.Context) {
func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) {
for _, project := range newSnapshot.ProjectCollection.Projects() {
if project.ShouldTriggerATA(newSnapshot.ID()) {
s.backgroundQueue.Enqueue(s.backgroundCtx, func(ctx context.Context) {
s.backgroundQueue.Enqueue(s.backgroundContext(), func(ctx context.Context) {
var logTree *logging.LogTree
if s.options.LoggingEnabled {
logTree = logging.NewLogTree("Triggering ATA for project " + project.Name())
Expand Down
34 changes: 34 additions & 0 deletions internal/project/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/project"
Expand Down Expand Up @@ -1406,6 +1407,39 @@ export const value = content;`,
assert.Equal(t, len(inlayHintsRefreshCalls), 1, "expected one RefreshInlayHints call after inlay hints preference change")
})

t.Run("sets locale when configured", func(t *testing.T) {
t.Parallel()
session, utils := projecttestutil.Setup(map[string]any{})
prefs := lsutil.NewDefaultUserPreferences()
prefs.Locale = "fr"

session.Configure(prefs)

setLocaleCalls := utils.Client().SetLocaleCalls()
assert.Equal(t, len(setLocaleCalls), 1)
assert.Equal(t, setLocaleCalls[0].LocaleMoqParam, "fr")
})

t.Run("adds locale to background contexts", func(t *testing.T) {
t.Parallel()
session, utils := projecttestutil.Setup(map[string]any{})
fr, ok := locale.Parse("fr")
assert.Assert(t, ok)
utils.Client().GetLocaleFunc = func() locale.Locale {
return fr
}
utils.Client().RefreshCodeLensFunc = func(ctx context.Context) error {
assert.Equal(t, locale.FromContext(ctx), fr)
return nil
}
prefs := lsutil.NewDefaultUserPreferences()
prefs.CodeLens.ReferencesCodeLensEnabled = core.TSTrue

session.Configure(prefs)

assert.Equal(t, len(utils.Client().RefreshCodeLensCalls()), 1)
})

t.Run("schedules diagnostics refresh when reportStyleChecksAsWarnings changes", func(t *testing.T) {
t.Parallel()
files := map[string]any{
Expand Down
Loading