cuelang.org/go@v0.13.0/internal/golangorgx/gopls/server/text_synchronization.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package server 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "maps" 14 "sync" 15 16 "cuelang.org/go/internal/golangorgx/gopls/cache" 17 "cuelang.org/go/internal/golangorgx/gopls/cache/metadata" 18 "cuelang.org/go/internal/golangorgx/gopls/file" 19 "cuelang.org/go/internal/golangorgx/gopls/protocol" 20 "cuelang.org/go/internal/golangorgx/tools/event" 21 "cuelang.org/go/internal/golangorgx/tools/event/tag" 22 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2" 23 "cuelang.org/go/internal/golangorgx/tools/xcontext" 24 ) 25 26 // ModificationSource identifies the origin of a change. 27 type ModificationSource int 28 29 const ( 30 // FromDidOpen is from a didOpen notification. 31 FromDidOpen = ModificationSource(iota) 32 33 // FromDidChange is from a didChange notification. 34 FromDidChange 35 36 // FromDidChangeWatchedFiles is from didChangeWatchedFiles notification. 37 FromDidChangeWatchedFiles 38 39 // FromDidSave is from a didSave notification. 40 FromDidSave 41 42 // FromDidClose is from a didClose notification. 43 FromDidClose 44 45 // FromDidChangeConfiguration is from a didChangeConfiguration notification. 46 FromDidChangeConfiguration 47 48 // FromRegenerateCgo refers to file modifications caused by regenerating 49 // the cgo sources for the workspace. 50 FromRegenerateCgo 51 52 // FromInitialWorkspaceLoad refers to the loading of all packages in the 53 // workspace when the view is first created. 54 FromInitialWorkspaceLoad 55 56 // FromCheckUpgrades refers to state changes resulting from the CheckUpgrades 57 // command, which queries module upgrades. 58 FromCheckUpgrades 59 60 // FromResetGoModDiagnostics refers to state changes resulting from the 61 // ResetGoModDiagnostics command. 62 FromResetGoModDiagnostics 63 64 // FromToggleGCDetails refers to state changes resulting from toggling 65 // gc_details on or off for a package. 66 FromToggleGCDetails 67 ) 68 69 func (m ModificationSource) String() string { 70 switch m { 71 case FromDidOpen: 72 return "opened files" 73 case FromDidChange: 74 return "changed files" 75 case FromDidChangeWatchedFiles: 76 return "files changed on disk" 77 case FromDidSave: 78 return "saved files" 79 case FromDidClose: 80 return "close files" 81 case FromRegenerateCgo: 82 return "regenerate cgo" 83 case FromInitialWorkspaceLoad: 84 return "initial workspace load" 85 case FromCheckUpgrades: 86 return "from check upgrades" 87 case FromResetGoModDiagnostics: 88 return "from resetting go.mod diagnostics" 89 default: 90 return "unknown file modification" 91 } 92 } 93 94 func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 95 ctx, done := event.Start(ctx, "lsp.Server.didOpen", tag.URI.Of(params.TextDocument.URI)) 96 defer done() 97 98 uri := params.TextDocument.URI 99 100 // TODO(myitcv): we need to report an error/problem/something in case the user opens a file 101 // that is not part of the CUE module. For now we will not support that, because it massively 102 // opens up a can of worms in terms of single-file support, ad hoc workspaces etc. 103 104 modifications := []file.Modification{{ 105 URI: uri, 106 Action: file.Open, 107 Version: params.TextDocument.Version, 108 Text: []byte(params.TextDocument.Text), 109 LanguageID: params.TextDocument.LanguageID, 110 }} 111 return s.didModifyFiles(ctx, modifications, FromDidOpen) 112 } 113 114 func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 115 ctx, done := event.Start(ctx, "lsp.Server.didChange", tag.URI.Of(params.TextDocument.URI)) 116 defer done() 117 118 uri := params.TextDocument.URI 119 text, err := s.changedText(ctx, uri, params.ContentChanges) 120 if err != nil { 121 return err 122 } 123 c := file.Modification{ 124 URI: uri, 125 Action: file.Change, 126 Version: params.TextDocument.Version, 127 Text: text, 128 } 129 if err := s.didModifyFiles(ctx, []file.Modification{c}, FromDidChange); err != nil { 130 return err 131 } 132 return nil 133 } 134 135 func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { 136 ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles") 137 defer done() 138 139 var modifications []file.Modification 140 for _, change := range params.Changes { 141 action := changeTypeToFileAction(change.Type) 142 modifications = append(modifications, file.Modification{ 143 URI: change.URI, 144 Action: action, 145 OnDisk: true, 146 }) 147 } 148 return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) 149 } 150 151 func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { 152 ctx, done := event.Start(ctx, "lsp.Server.didSave", tag.URI.Of(params.TextDocument.URI)) 153 defer done() 154 155 c := file.Modification{ 156 URI: params.TextDocument.URI, 157 Action: file.Save, 158 } 159 if params.Text != nil { 160 c.Text = []byte(*params.Text) 161 } 162 return s.didModifyFiles(ctx, []file.Modification{c}, FromDidSave) 163 } 164 165 func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { 166 ctx, done := event.Start(ctx, "lsp.Server.didClose", tag.URI.Of(params.TextDocument.URI)) 167 defer done() 168 169 return s.didModifyFiles(ctx, []file.Modification{ 170 { 171 URI: params.TextDocument.URI, 172 Action: file.Close, 173 Version: -1, 174 Text: nil, 175 }, 176 }, FromDidClose) 177 } 178 179 // This exists temporarily for facilitating integration tests. TODO(ms): remove when possible. 180 func logFilesToPackage(ctx context.Context, s *server, modifications []file.Modification) { 181 uriToSnapshot := make(map[protocol.DocumentURI]map[protocol.DocumentURI][]metadata.ImportPath) 182 for _, mod := range modifications { 183 snapshot, release, err := s.session.SnapshotOf(ctx, mod.URI) 184 if err != nil { 185 continue 186 } 187 uriToSnapshot[mod.URI] = maps.Clone(snapshot.MetadataGraph().FilesToPackage) 188 release() 189 } 190 bs, err := json.Marshal(uriToSnapshot) 191 if err != nil { 192 return 193 } 194 s.client.LogMessage(ctx, &protocol.LogMessageParams{ 195 Type: protocol.Debug, 196 Message: string(bs), 197 }) 198 } 199 200 func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modification, cause ModificationSource) error { 201 // wg guards two conditions: 202 // 1. didModifyFiles is complete 203 // 2. the goroutine diagnosing changes on behalf of didModifyFiles is 204 // complete, if it was started 205 // 206 // Both conditions must be satisfied for the purpose of testing: we don't 207 // want to observe the completion of change processing until we have received 208 // all diagnostics as well as all server->client notifications done on behalf 209 // of this function. 210 var wg sync.WaitGroup 211 wg.Add(1) 212 defer wg.Done() 213 214 if s.Options().VerboseWorkDoneProgress { 215 work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) 216 go func() { 217 wg.Wait() 218 work.End(ctx, "Done.") 219 }() 220 } 221 222 s.stateMu.Lock() 223 if s.state >= serverShutDown { 224 // This state check does not prevent races below, and exists only to 225 // produce a better error message. The actual race to the cache should be 226 // guarded by Session.viewMu. 227 s.stateMu.Unlock() 228 return errors.New("server is shut down") 229 } 230 s.stateMu.Unlock() 231 232 // If the set of changes included directories, expand those directories 233 // to their files. 234 modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) 235 236 viewsToDiagnose, err := s.session.DidModifyFiles(ctx, modifications) 237 if err != nil { 238 return err 239 } 240 241 // golang/go#50267: diagnostics should be re-sent after each change. 242 for _, mod := range modifications { 243 s.mustPublishDiagnostics(mod.URI) 244 } 245 246 modCtx, modID := s.needsDiagnosis(ctx, viewsToDiagnose) 247 248 wg.Add(1) 249 go func() { 250 s.diagnoseChangedViews(modCtx, modID, viewsToDiagnose, cause) 251 wg.Done() 252 // Temporary exposure of internal behaviour for testing only. TODO(ms): remove when possible. 253 logFilesToPackage(ctx, s, modifications) 254 }() 255 256 // After any file modifications, we need to update our watched files, 257 // in case something changed. Compute the new set of directories to watch, 258 // and if it differs from the current set, send updated registrations. 259 return s.updateWatchedDirectories(ctx) 260 } 261 262 // needsDiagnosis records the given views as needing diagnosis, returning the 263 // context and modification id to use for said diagnosis. 264 // 265 // Only the keys of viewsToDiagnose are used; the changed files are irrelevant. 266 func (s *server) needsDiagnosis(ctx context.Context, viewsToDiagnose map[*cache.View][]protocol.DocumentURI) (context.Context, uint64) { 267 s.modificationMu.Lock() 268 defer s.modificationMu.Unlock() 269 if s.cancelPrevDiagnostics != nil { 270 s.cancelPrevDiagnostics() 271 } 272 modCtx := xcontext.Detach(ctx) 273 modCtx, s.cancelPrevDiagnostics = context.WithCancel(modCtx) 274 s.lastModificationID++ 275 modID := s.lastModificationID 276 277 for v := range viewsToDiagnose { 278 if needs, ok := s.viewsToDiagnose[v]; !ok || needs < modID { 279 s.viewsToDiagnose[v] = modID 280 } 281 } 282 return modCtx, modID 283 } 284 285 // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a 286 // file change originating from the given cause. 287 func DiagnosticWorkTitle(cause ModificationSource) string { 288 return fmt.Sprintf("diagnosing %v", cause) 289 } 290 291 func (s *server) changedText(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 292 if len(changes) == 0 { 293 return nil, fmt.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) 294 } 295 296 // Check if the client sent the full content of the file. 297 // We accept a full content change even if the server expected incremental changes. 298 if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { 299 return []byte(changes[0].Text), nil 300 } 301 return s.applyIncrementalChanges(ctx, uri, changes) 302 } 303 304 func (s *server) applyIncrementalChanges(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 305 fh, err := s.session.ReadFile(ctx, uri) 306 if err != nil { 307 return nil, err 308 } 309 content, err := fh.Content() 310 if err != nil { 311 return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) 312 } 313 for _, change := range changes { 314 // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. 315 // out-of-order or overlapping changes---and much more efficient. 316 317 // Make sure to update mapper along with the content. 318 m := protocol.NewMapper(uri, content) 319 if change.Range == nil { 320 return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) 321 } 322 start, end, err := m.RangeOffsets(*change.Range) 323 if err != nil { 324 return nil, err 325 } 326 if end < start { 327 return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 328 } 329 var buf bytes.Buffer 330 buf.Write(content[:start]) 331 buf.WriteString(change.Text) 332 buf.Write(content[end:]) 333 content = buf.Bytes() 334 } 335 return content, nil 336 } 337 338 func changeTypeToFileAction(ct protocol.FileChangeType) file.Action { 339 switch ct { 340 case protocol.Changed: 341 return file.Change 342 case protocol.Created: 343 return file.Create 344 case protocol.Deleted: 345 return file.Delete 346 } 347 return file.UnknownAction 348 }