From a6efbe10e43b14ae4d3639ffd39114cbc07f0c31 Mon Sep 17 00:00:00 2001 From: Christian Vuerings Date: Fri, 12 Jun 2026 14:11:10 -0700 Subject: [PATCH 1/3] Push per-file diagnostics for clients without pull diagnostics support Clients that do not advertise the textDocument.diagnostic capability (e.g. eglot before Dec 2025, Nova, Claude Code) previously received no file diagnostics at all, since the server only supports pull diagnostics and only pushes project-level diagnostics to the tsconfig URI. When the client advertises textDocument.publishDiagnostics but not textDocument.diagnostic, push per-file diagnostics for open files: - didOpen publishes initial diagnostics for the opened file - didChange schedules a debounced snapshot update that rebuilds dirty programs and republishes diagnostics for affected open files - didClose clears diagnostics for the closed file - workspace/diagnostic/refresh triggers (watched files, config changes, ATA updates) schedule a snapshot update instead of a refresh request, since push-only clients cannot re-pull Clients that support pull diagnostics see no behavior change, and the existing disablePushDiagnostics initialization option also disables the new path. --- internal/ls/diagnostics.go | 17 +++++ internal/lsp/server.go | 29 ++++++--- internal/project/project_test.go | 105 +++++++++++++++++++++++++++++++ internal/project/session.go | 85 +++++++++++++++++++++++-- internal/project/snapshot.go | 4 ++ 5 files changed, 225 insertions(+), 15 deletions(-) diff --git a/internal/ls/diagnostics.go b/internal/ls/diagnostics.go index 87ace009f9d..98facc493db 100644 --- a/internal/ls/diagnostics.go +++ b/internal/ls/diagnostics.go @@ -34,6 +34,23 @@ func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.Do }, nil } +// ProvidePushDiagnostics computes diagnostics for a file in the format used by +// textDocument/publishDiagnostics. +func (l *LanguageService) ProvidePushDiagnostics(ctx context.Context, uri lsproto.DocumentUri) []*lsproto.Diagnostic { + program, file := l.tryGetProgramAndFile(uri.FileName()) + if file == nil { + return nil + } + + diagnostics := getAllDiagnostics(ctx, program, file) + + lspDiagnostics := make([]*lsproto.Diagnostic, 0, len(diagnostics)) + for _, diag := range diagnostics { + lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPush(ctx, l.converters, diag)) + } + return lspDiagnostics +} + func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic { size := 0 for _, diagSlice := range diagnostics { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 7aac5f263d5..cfcc1efcf29 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1193,19 +1193,28 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali } s.telemetryEnabled = enableTelemetry + var clientSupportsPullDiagnostics, clientSupportsPublishDiagnostics bool + if capabilities := s.initializeParams.Capabilities; capabilities != nil && capabilities.TextDocument != nil { + textDocumentCapabilities := capabilities.TextDocument + clientSupportsPullDiagnostics = textDocumentCapabilities.Diagnostic != nil + clientSupportsPublishDiagnostics = textDocumentCapabilities.PublishDiagnostics != nil + } + pushFileDiagnostics := !disablePushDiagnostics && !clientSupportsPullDiagnostics && clientSupportsPublishDiagnostics + s.session = project.NewSession(&project.SessionInit{ BackgroundCtx: lsproto.WithClientCapabilities(s.backgroundCtx, &s.clientCapabilities), Options: &project.SessionOptions{ - CurrentDirectory: cwd, - DefaultLibraryPath: s.defaultLibraryPath, - TypingsLocation: s.typingsLocation, - PositionEncoding: s.positionEncoding, - WatchEnabled: s.watchEnabled, - LoggingEnabled: true, - TelemetryEnabled: enableTelemetry, - DebounceDelay: 500 * time.Millisecond, - PushDiagnosticsEnabled: !disablePushDiagnostics, - Locale: s.locale, + CurrentDirectory: cwd, + DefaultLibraryPath: s.defaultLibraryPath, + TypingsLocation: s.typingsLocation, + PositionEncoding: s.positionEncoding, + WatchEnabled: s.watchEnabled, + LoggingEnabled: true, + TelemetryEnabled: enableTelemetry, + DebounceDelay: 500 * time.Millisecond, + PushDiagnosticsEnabled: !disablePushDiagnostics, + PushFileDiagnosticsEnabled: pushFileDiagnostics, + Locale: s.locale, }, FS: s.fs, Logger: s.logger, diff --git a/internal/project/project_test.go b/internal/project/project_test.go index b819f860d7c..274fc889a4d 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -659,3 +659,108 @@ func TestProgressNotifications(t *testing.T) { assert.Equal(t, starts, finishes, "ProgressStart and ProgressFinish calls for Project_0 should be balanced") }) } + +func TestPushFileDiagnostics(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + files := map[string]any{ + "/src/tsconfig.json": `{}`, + "/src/index.ts": `const x: number = "";`, + } + options := &project.SessionOptions{ + CurrentDirectory: "/", + DefaultLibraryPath: bundled.LibPath(), + PositionEncoding: lsproto.PositionEncodingKindUTF8, + WatchEnabled: true, + LoggingEnabled: true, + PushDiagnosticsEnabled: true, + PushFileDiagnosticsEnabled: true, + } + setup := func() (*project.Session, *projecttestutil.SessionUtils) { + init, utils := projecttestutil.GetSessionInitOptions(files, options, &projecttestutil.TypingsInstallerOptions{}) + versionSupport := true + caps := (&lsproto.ClientCapabilities{ + TextDocument: &lsproto.TextDocumentClientCapabilities{ + PublishDiagnostics: &lsproto.PublishDiagnosticsClientCapabilities{VersionSupport: &versionSupport}, + }, + }).Resolve() + init.BackgroundCtx = lsproto.WithClientCapabilities(context.Background(), &caps) + return project.NewSession(init), utils + } + uri := lsproto.DocumentUri("file:///src/index.ts") + + t.Run("publishes file diagnostics on open", func(t *testing.T) { + t.Parallel() + session, utils := setup() + session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Assert(t, len(calls) > 0, "expected PublishDiagnostics call for index.ts") + last := calls[len(calls)-1] + assert.Equal(t, len(last.Params.Diagnostics), 1) + assert.Equal(t, *last.Params.Version, int32(1)) + }) + + t.Run("publishes updated file diagnostics on change", func(t *testing.T) { + t.Parallel() + session, utils := setup() + session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + session.WaitForBackgroundTasks() + + session.DidChangeFile(context.Background(), uri, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ + {WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: `const x: number = 1;`}}, + }) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Assert(t, len(calls) > 1, "expected PublishDiagnostics call after change") + last := calls[len(calls)-1] + assert.Equal(t, len(last.Params.Diagnostics), 0) + assert.Equal(t, *last.Params.Version, int32(2)) + }) + + t.Run("clears file diagnostics on close", func(t *testing.T) { + t.Parallel() + session, utils := setup() + session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + session.WaitForBackgroundTasks() + + session.DidCloseFile(context.Background(), uri) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Assert(t, len(calls) > 1, "expected PublishDiagnostics call after close") + last := calls[len(calls)-1] + assert.Equal(t, len(last.Params.Diagnostics), 0) + assert.Assert(t, last.Params.Version == nil) + }) + + t.Run("omits version when client does not support it", func(t *testing.T) { + t.Parallel() + session, utils := projecttestutil.SetupWithOptions(files, options) + session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Assert(t, len(calls) > 0, "expected PublishDiagnostics call for index.ts") + last := calls[len(calls)-1] + assert.Equal(t, len(last.Params.Diagnostics), 1) + assert.Assert(t, last.Params.Version == nil) + }) + + t.Run("does not publish file diagnostics when disabled", func(t *testing.T) { + t.Parallel() + session, utils := projecttestutil.Setup(files) + session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageService(context.Background(), uri) + assert.NilError(t, err) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Equal(t, len(calls), 0) + }) +} diff --git a/internal/project/session.go b/internal/project/session.go index 4fb513460ba..df8e75554fe 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -36,6 +36,8 @@ const ( UpdateReasonUnknown UpdateReason = iota UpdateReasonDidOpenFile UpdateReasonDidCloseFile + UpdateReasonDidChangeFile + UpdateReasonDiagnosticsRefresh UpdateReasonDidChangeCompilerOptionsForInferredProjects UpdateReasonRequestedLanguageServicePendingChanges UpdateReasonRequestedLanguageServiceProjectNotLoaded @@ -61,8 +63,11 @@ type SessionOptions struct { LoggingEnabled bool TelemetryEnabled bool PushDiagnosticsEnabled bool - DebounceDelay time.Duration - Locale locale.Locale + // PushFileDiagnosticsEnabled pushes per-file diagnostics for open files, + // for clients that do not support pull diagnostics. + PushFileDiagnosticsEnabled bool + DebounceDelay time.Duration + Locale locale.Locale } type SessionInit struct { @@ -333,13 +338,16 @@ func (s *Session) DidChangeFile(ctx context.Context, uri lsproto.DocumentUri, ve s.cancelWarmAutoImportCache() s.scheduleIdleCacheClean() s.pendingFileChangesMu.Lock() - defer s.pendingFileChangesMu.Unlock() s.pendingFileChanges = append(s.pendingFileChanges, FileChange{ Kind: FileChangeKindChange, URI: uri, Version: version, Changes: changes, }) + s.pendingFileChangesMu.Unlock() + if s.options.PushFileDiagnosticsEnabled { + s.ScheduleSnapshotUpdate(UpdateReasonDidChangeFile) + } } func (s *Session) DidSaveFile(ctx context.Context, uri lsproto.DocumentUri) { @@ -391,6 +399,12 @@ func (s *Session) DidChangeCompilerOptionsForInferredProjects(ctx context.Contex } func (s *Session) ScheduleDiagnosticsRefresh() { + if s.options.PushFileDiagnosticsEnabled { + // Push-only clients can't re-pull in response to workspace/diagnostic/refresh. + s.ScheduleSnapshotUpdate(UpdateReasonDiagnosticsRefresh) + return + } + s.diagnosticsRefreshMu.Lock() defer s.diagnosticsRefreshMu.Unlock() @@ -502,12 +516,22 @@ func (s *Session) ScheduleSnapshotUpdate(reason UpdateReason) { return } - s.UpdateSnapshot(ctx, overlays, SnapshotChange{ + change := SnapshotChange{ reason: reason, fileChanges: fileChanges, ataChanges: ataChanges, newConfig: newConfig, - }) + } + if s.options.PushFileDiagnosticsEnabled { + // Request open documents so dirty programs are rebuilt eagerly; + // push-only clients send no requests that would otherwise do it. + documents := make([]lsproto.DocumentUri, 0, len(overlays)) + for _, overlay := range overlays { + documents = append(documents, lsconv.FileNameToDocumentURI(overlay.FileName())) + } + change.ResourceRequest = ResourceRequest{Documents: documents} + } + s.UpdateSnapshot(ctx, overlays, change) }) } @@ -1236,6 +1260,7 @@ func (s *Session) updateSnapshot(ctx context.Context, overlays map[tspath.Path]* } } s.publishProgramDiagnostics(oldSnapshot, newSnapshot) + s.publishOpenFileDiagnostics(oldSnapshot, newSnapshot) s.sendProjectInfoTelemetryForNewProjects(oldSnapshot, newSnapshot) s.warmAutoImportCache(ctx, change, oldSnapshot, newSnapshot) }) @@ -1689,6 +1714,56 @@ func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath } } +// publishOpenFileDiagnostics pushes diagnostics for open files after a +// snapshot update, for clients that do not support pull diagnostics. +func (s *Session) publishOpenFileDiagnostics(oldSnapshot *Snapshot, newSnapshot *Snapshot) { + if !s.options.PushFileDiagnosticsEnabled { + return + } + + ctx := s.backgroundCtx + for path, overlay := range oldSnapshot.fs.overlays { + if _, stillOpen := newSnapshot.fs.overlays[path]; !stillOpen { + s.publishFileDiagnostics(ctx, lsconv.FileNameToDocumentURI(overlay.FileName()), nil, nil) + } + } + + for path, overlay := range newSnapshot.fs.overlays { + uri := lsconv.FileNameToDocumentURI(overlay.FileName()) + project := newSnapshot.GetDefaultProject(uri) + if project == nil { + continue + } + program := project.GetProgram() + if program == nil { + continue + } + _, wasOpen := oldSnapshot.fs.overlays[path] + if wasOpen && project.ProgramLastUpdate != newSnapshot.ID() { + continue + } + languageService := ls.NewLanguageService(project.configFilePath, program, newSnapshot, overlay.FileName()) + version := overlay.Version() + s.publishFileDiagnostics(ctx, uri, &version, languageService.ProvidePushDiagnostics(ctx, uri)) + } +} + +func (s *Session) publishFileDiagnostics(ctx context.Context, uri lsproto.DocumentUri, version *int32, diagnostics []*lsproto.Diagnostic) { + if diagnostics == nil { + diagnostics = []*lsproto.Diagnostic{} + } + if !lsproto.GetClientCapabilities(ctx).TextDocument.PublishDiagnostics.VersionSupport { + version = nil + } + if err := s.client.PublishDiagnostics(ctx, &lsproto.PublishDiagnosticsParams{ + Uri: uri, + Version: version, + Diagnostics: diagnostics, + }); err != nil && s.options.LoggingEnabled { + s.logger.Logf("Error publishing diagnostics: %v", err) + } +} + // EnqueuePublishGlobalDiagnostics schedules a background check for new accumulated // global diagnostics from checker pools, re-publishing tsconfig diagnostics if changed. // Multiple calls are coalesced into a single background task. diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index b3e5fc6b1aa..e3092d06bda 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -266,6 +266,10 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma logger.Logf("Reason: DidOpenFile - %s", change.fileChanges.Opened) case UpdateReasonDidCloseFile: logger.Logf("Reason: DidCloseFile - %v", change.fileChanges.Closed) + case UpdateReasonDidChangeFile: + logger.Logf("Reason: DidChangeFile - %v", change.fileChanges.Changed) + case UpdateReasonDiagnosticsRefresh: + logger.Logf("Reason: DiagnosticsRefresh") case UpdateReasonDidChangeCompilerOptionsForInferredProjects: logger.Logf("Reason: DidChangeCompilerOptionsForInferredProjects") case UpdateReasonRequestedLanguageServicePendingChanges: From f47339a1f612d2011eb95da45594a810e204544e Mon Sep 17 00:00:00 2001 From: Christian Vuerings Date: Fri, 12 Jun 2026 14:39:12 -0700 Subject: [PATCH 2/3] Address review feedback - Do not gate push-file diagnostics on the textDocument.publishDiagnostics capability; it is a baseline notification and the capability only advertises extensions - Gate the per-file push path on PushDiagnosticsEnabled as well, so the global option always wins - Skip publishing empty diagnostics for files that never had any, and skip the clear on close for files with nothing published --- internal/lsp/server.go | 10 +++++----- internal/project/project_test.go | 16 ++++++++++++++++ internal/project/session.go | 30 ++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index cfcc1efcf29..fdcab99f13b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1193,13 +1193,13 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali } s.telemetryEnabled = enableTelemetry - var clientSupportsPullDiagnostics, clientSupportsPublishDiagnostics bool + // textDocument/publishDiagnostics is a baseline notification; its capability + // only advertises extensions like versionSupport, so it is not checked here. + var clientSupportsPullDiagnostics bool if capabilities := s.initializeParams.Capabilities; capabilities != nil && capabilities.TextDocument != nil { - textDocumentCapabilities := capabilities.TextDocument - clientSupportsPullDiagnostics = textDocumentCapabilities.Diagnostic != nil - clientSupportsPublishDiagnostics = textDocumentCapabilities.PublishDiagnostics != nil + clientSupportsPullDiagnostics = capabilities.TextDocument.Diagnostic != nil } - pushFileDiagnostics := !disablePushDiagnostics && !clientSupportsPullDiagnostics && clientSupportsPublishDiagnostics + pushFileDiagnostics := !disablePushDiagnostics && !clientSupportsPullDiagnostics s.session = project.NewSession(&project.SessionInit{ BackgroundCtx: lsproto.WithClientCapabilities(s.backgroundCtx, &s.clientCapabilities), diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 274fc889a4d..93f34ba4821 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -752,6 +752,22 @@ func TestPushFileDiagnostics(t *testing.T) { assert.Assert(t, last.Params.Version == nil) }) + t.Run("does not publish empty diagnostics for files that never had any", func(t *testing.T) { + t.Parallel() + cleanFiles := map[string]any{ + "/src/tsconfig.json": `{}`, + "/src/index.ts": `const x: number = 1;`, + } + session, utils := projecttestutil.SetupWithOptions(cleanFiles, options) + session.DidOpenFile(context.Background(), uri, 1, cleanFiles["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + session.WaitForBackgroundTasks() + session.DidCloseFile(context.Background(), uri) + session.WaitForBackgroundTasks() + + calls := filterDiagnosticsByURI(utils.Client().PublishDiagnosticsCalls(), uri, 0) + assert.Equal(t, len(calls), 0) + }) + t.Run("does not publish file diagnostics when disabled", func(t *testing.T) { t.Parallel() session, utils := projecttestutil.Setup(files) diff --git a/internal/project/session.go b/internal/project/session.go index df8e75554fe..996b8ad0b7c 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -179,6 +179,14 @@ type Session struct { // task should be enqueued. It is reset when the task runs, coalescing multiple // requests into a single background task. globalDiagPublishPending atomic.Bool + + // pushedDiagnosticsFiles tracks open files whose last published diagnostics + // were non-empty, so redundant empty publishes can be skipped. + pushedDiagnosticsFiles collections.SyncSet[tspath.Path] +} + +func (s *Session) pushFileDiagnosticsEnabled() bool { + return s.options.PushDiagnosticsEnabled && s.options.PushFileDiagnosticsEnabled } func NewSession(init *SessionInit) *Session { @@ -345,7 +353,7 @@ func (s *Session) DidChangeFile(ctx context.Context, uri lsproto.DocumentUri, ve Changes: changes, }) s.pendingFileChangesMu.Unlock() - if s.options.PushFileDiagnosticsEnabled { + if s.pushFileDiagnosticsEnabled() { s.ScheduleSnapshotUpdate(UpdateReasonDidChangeFile) } } @@ -399,7 +407,7 @@ func (s *Session) DidChangeCompilerOptionsForInferredProjects(ctx context.Contex } func (s *Session) ScheduleDiagnosticsRefresh() { - if s.options.PushFileDiagnosticsEnabled { + if s.pushFileDiagnosticsEnabled() { // Push-only clients can't re-pull in response to workspace/diagnostic/refresh. s.ScheduleSnapshotUpdate(UpdateReasonDiagnosticsRefresh) return @@ -522,7 +530,7 @@ func (s *Session) ScheduleSnapshotUpdate(reason UpdateReason) { ataChanges: ataChanges, newConfig: newConfig, } - if s.options.PushFileDiagnosticsEnabled { + if s.pushFileDiagnosticsEnabled() { // Request open documents so dirty programs are rebuilt eagerly; // push-only clients send no requests that would otherwise do it. documents := make([]lsproto.DocumentUri, 0, len(overlays)) @@ -1717,13 +1725,14 @@ func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath // publishOpenFileDiagnostics pushes diagnostics for open files after a // snapshot update, for clients that do not support pull diagnostics. func (s *Session) publishOpenFileDiagnostics(oldSnapshot *Snapshot, newSnapshot *Snapshot) { - if !s.options.PushFileDiagnosticsEnabled { + if !s.pushFileDiagnosticsEnabled() { return } ctx := s.backgroundCtx for path, overlay := range oldSnapshot.fs.overlays { - if _, stillOpen := newSnapshot.fs.overlays[path]; !stillOpen { + if _, stillOpen := newSnapshot.fs.overlays[path]; !stillOpen && s.pushedDiagnosticsFiles.Has(path) { + s.pushedDiagnosticsFiles.Delete(path) s.publishFileDiagnostics(ctx, lsconv.FileNameToDocumentURI(overlay.FileName()), nil, nil) } } @@ -1743,8 +1752,17 @@ func (s *Session) publishOpenFileDiagnostics(oldSnapshot *Snapshot, newSnapshot continue } languageService := ls.NewLanguageService(project.configFilePath, program, newSnapshot, overlay.FileName()) + diagnostics := languageService.ProvidePushDiagnostics(ctx, uri) + if len(diagnostics) == 0 { + if !s.pushedDiagnosticsFiles.Has(path) { + continue + } + s.pushedDiagnosticsFiles.Delete(path) + } else { + s.pushedDiagnosticsFiles.Add(path) + } version := overlay.Version() - s.publishFileDiagnostics(ctx, uri, &version, languageService.ProvidePushDiagnostics(ctx, uri)) + s.publishFileDiagnostics(ctx, uri, &version, diagnostics) } } From 15117c6ccc876424a7aad55eb900ef0d434bcf87 Mon Sep 17 00:00:00 2001 From: Christian Vuerings Date: Fri, 12 Jun 2026 15:07:50 -0700 Subject: [PATCH 3/3] Sort requested documents for deterministic ordering --- internal/project/session.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/project/session.go b/internal/project/session.go index 996b8ad0b7c..15786698e52 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -537,6 +537,7 @@ func (s *Session) ScheduleSnapshotUpdate(reason UpdateReason) { for _, overlay := range overlays { documents = append(documents, lsconv.FileNameToDocumentURI(overlay.FileName())) } + slices.Sort(documents) change.ResourceRequest = ResourceRequest{Documents: documents} } s.UpdateSnapshot(ctx, overlays, change)