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