github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/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 lsp 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "path/filepath" 12 "time" 13 14 "github.com/jhump/golang-x-tools/internal/event" 15 "github.com/jhump/golang-x-tools/internal/jsonrpc2" 16 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 17 "github.com/jhump/golang-x-tools/internal/lsp/source" 18 "github.com/jhump/golang-x-tools/internal/span" 19 "github.com/jhump/golang-x-tools/internal/xcontext" 20 errors "golang.org/x/xerrors" 21 ) 22 23 // ModificationSource identifies the originating cause of a file modification. 24 type ModificationSource int 25 26 const ( 27 // FromDidOpen is a file modification caused by opening a file. 28 FromDidOpen = ModificationSource(iota) 29 30 // FromDidChange is a file modification caused by changing a file. 31 FromDidChange 32 33 // FromDidChangeWatchedFiles is a file modification caused by a change to a 34 // watched file. 35 FromDidChangeWatchedFiles 36 37 // FromDidSave is a file modification caused by a file save. 38 FromDidSave 39 40 // FromDidClose is a file modification caused by closing a file. 41 FromDidClose 42 43 // FromRegenerateCgo refers to file modifications caused by regenerating 44 // the cgo sources for the workspace. 45 FromRegenerateCgo 46 47 // FromInitialWorkspaceLoad refers to the loading of all packages in the 48 // workspace when the view is first created. 49 FromInitialWorkspaceLoad 50 ) 51 52 func (m ModificationSource) String() string { 53 switch m { 54 case FromDidOpen: 55 return "opened files" 56 case FromDidChange: 57 return "changed files" 58 case FromDidChangeWatchedFiles: 59 return "files changed on disk" 60 case FromDidSave: 61 return "saved files" 62 case FromDidClose: 63 return "close files" 64 case FromRegenerateCgo: 65 return "regenerate cgo" 66 case FromInitialWorkspaceLoad: 67 return "initial workspace load" 68 default: 69 return "unknown file modification" 70 } 71 } 72 73 func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 74 uri := params.TextDocument.URI.SpanURI() 75 if !uri.IsFile() { 76 return nil 77 } 78 // There may not be any matching view in the current session. If that's 79 // the case, try creating a new view based on the opened file path. 80 // 81 // TODO(rstambler): This seems like it would continuously add new 82 // views, but it won't because ViewOf only returns an error when there 83 // are no views in the session. I don't know if that logic should go 84 // here, or if we can continue to rely on that implementation detail. 85 if _, err := s.session.ViewOf(uri); err != nil { 86 dir := filepath.Dir(uri.Filename()) 87 if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{ 88 URI: string(protocol.URIFromPath(dir)), 89 Name: filepath.Base(dir), 90 }}); err != nil { 91 return err 92 } 93 } 94 return s.didModifyFiles(ctx, []source.FileModification{{ 95 URI: uri, 96 Action: source.Open, 97 Version: params.TextDocument.Version, 98 Text: []byte(params.TextDocument.Text), 99 LanguageID: params.TextDocument.LanguageID, 100 }}, FromDidOpen) 101 } 102 103 func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 104 uri := params.TextDocument.URI.SpanURI() 105 if !uri.IsFile() { 106 return nil 107 } 108 109 text, err := s.changedText(ctx, uri, params.ContentChanges) 110 if err != nil { 111 return err 112 } 113 c := source.FileModification{ 114 URI: uri, 115 Action: source.Change, 116 Version: params.TextDocument.Version, 117 Text: text, 118 } 119 if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil { 120 return err 121 } 122 return s.warnAboutModifyingGeneratedFiles(ctx, uri) 123 } 124 125 // warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a 126 // generated file for the first time. 127 func (s *Server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri span.URI) error { 128 s.changedFilesMu.Lock() 129 _, ok := s.changedFiles[uri] 130 if !ok { 131 s.changedFiles[uri] = struct{}{} 132 } 133 s.changedFilesMu.Unlock() 134 135 // This file has already been edited before. 136 if ok { 137 return nil 138 } 139 140 // Ideally, we should be able to specify that a generated file should 141 // be opened as read-only. Tell the user that they should not be 142 // editing a generated file. 143 view, err := s.session.ViewOf(uri) 144 if err != nil { 145 return err 146 } 147 snapshot, release := view.Snapshot(ctx) 148 isGenerated := source.IsGenerated(ctx, snapshot, uri) 149 release() 150 151 if !isGenerated { 152 return nil 153 } 154 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 155 Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()), 156 Type: protocol.Warning, 157 }) 158 } 159 160 func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { 161 var modifications []source.FileModification 162 for _, change := range params.Changes { 163 uri := change.URI.SpanURI() 164 if !uri.IsFile() { 165 continue 166 } 167 action := changeTypeToFileAction(change.Type) 168 modifications = append(modifications, source.FileModification{ 169 URI: uri, 170 Action: action, 171 OnDisk: true, 172 }) 173 } 174 return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) 175 } 176 177 func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { 178 uri := params.TextDocument.URI.SpanURI() 179 if !uri.IsFile() { 180 return nil 181 } 182 c := source.FileModification{ 183 URI: uri, 184 Action: source.Save, 185 } 186 if params.Text != nil { 187 c.Text = []byte(*params.Text) 188 } 189 return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave) 190 } 191 192 func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { 193 uri := params.TextDocument.URI.SpanURI() 194 if !uri.IsFile() { 195 return nil 196 } 197 return s.didModifyFiles(ctx, []source.FileModification{ 198 { 199 URI: uri, 200 Action: source.Close, 201 Version: -1, 202 Text: nil, 203 }, 204 }, FromDidClose) 205 } 206 207 func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error { 208 diagnoseDone := make(chan struct{}) 209 if s.session.Options().VerboseWorkDoneProgress { 210 work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) 211 defer func() { 212 go func() { 213 <-diagnoseDone 214 work.End("Done.") 215 }() 216 }() 217 } 218 219 onDisk := cause == FromDidChangeWatchedFiles 220 delay := s.session.Options().ExperimentalWatchedFileDelay 221 s.fileChangeMu.Lock() 222 defer s.fileChangeMu.Unlock() 223 if !onDisk || delay == 0 { 224 // No delay: process the modifications immediately. 225 return s.processModifications(ctx, modifications, onDisk, diagnoseDone) 226 } 227 // Debounce and batch up pending modifications from watched files. 228 pending := &pendingModificationSet{ 229 diagnoseDone: diagnoseDone, 230 changes: modifications, 231 } 232 // Invariant: changes appended to s.pendingOnDiskChanges are eventually 233 // handled in the order they arrive. This guarantee is only partially 234 // enforced here. Specifically: 235 // 1. s.fileChangesMu ensures that the append below happens in the order 236 // notifications were received, so that the changes within each batch are 237 // ordered properly. 238 // 2. The debounced func below holds s.fileChangesMu while processing all 239 // changes in s.pendingOnDiskChanges, ensuring that no batches are 240 // processed out of order. 241 // 3. Session.ExpandModificationsToDirectories and Session.DidModifyFiles 242 // process changes in order. 243 s.pendingOnDiskChanges = append(s.pendingOnDiskChanges, pending) 244 ctx = xcontext.Detach(ctx) 245 okc := s.watchedFileDebouncer.debounce("", 0, time.After(delay)) 246 go func() { 247 if ok := <-okc; !ok { 248 return 249 } 250 s.fileChangeMu.Lock() 251 var allChanges []source.FileModification 252 // For accurate progress notifications, we must notify all goroutines 253 // waiting for the diagnose pass following a didChangeWatchedFiles 254 // notification. This is necessary for regtest assertions. 255 var dones []chan struct{} 256 for _, pending := range s.pendingOnDiskChanges { 257 allChanges = append(allChanges, pending.changes...) 258 dones = append(dones, pending.diagnoseDone) 259 } 260 261 allDone := make(chan struct{}) 262 if err := s.processModifications(ctx, allChanges, onDisk, allDone); err != nil { 263 event.Error(ctx, "processing delayed file changes", err) 264 } 265 s.pendingOnDiskChanges = nil 266 s.fileChangeMu.Unlock() 267 <-allDone 268 for _, done := range dones { 269 close(done) 270 } 271 }() 272 return nil 273 } 274 275 // processModifications update server state to reflect file changes, and 276 // triggers diagnostics to run asynchronously. The diagnoseDone channel will be 277 // closed once diagnostics complete. 278 func (s *Server) processModifications(ctx context.Context, modifications []source.FileModification, onDisk bool, diagnoseDone chan struct{}) error { 279 s.stateMu.Lock() 280 if s.state >= serverShutDown { 281 // This state check does not prevent races below, and exists only to 282 // produce a better error message. The actual race to the cache should be 283 // guarded by Session.viewMu. 284 s.stateMu.Unlock() 285 close(diagnoseDone) 286 return errors.New("server is shut down") 287 } 288 s.stateMu.Unlock() 289 // If the set of changes included directories, expand those directories 290 // to their files. 291 modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) 292 293 snapshots, releases, err := s.session.DidModifyFiles(ctx, modifications) 294 if err != nil { 295 close(diagnoseDone) 296 return err 297 } 298 299 go func() { 300 s.diagnoseSnapshots(snapshots, onDisk) 301 for _, release := range releases { 302 release() 303 } 304 close(diagnoseDone) 305 }() 306 307 // After any file modifications, we need to update our watched files, 308 // in case something changed. Compute the new set of directories to watch, 309 // and if it differs from the current set, send updated registrations. 310 return s.updateWatchedDirectories(ctx) 311 } 312 313 // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a 314 // file change originating from the given cause. 315 func DiagnosticWorkTitle(cause ModificationSource) string { 316 return fmt.Sprintf("diagnosing %v", cause) 317 } 318 319 func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 320 if len(changes) == 0 { 321 return nil, errors.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) 322 } 323 324 // Check if the client sent the full content of the file. 325 // We accept a full content change even if the server expected incremental changes. 326 if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { 327 return []byte(changes[0].Text), nil 328 } 329 return s.applyIncrementalChanges(ctx, uri, changes) 330 } 331 332 func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 333 fh, err := s.session.GetFile(ctx, uri) 334 if err != nil { 335 return nil, err 336 } 337 content, err := fh.Read() 338 if err != nil { 339 return nil, errors.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) 340 } 341 for _, change := range changes { 342 // Make sure to update column mapper along with the content. 343 converter := span.NewContentConverter(uri.Filename(), content) 344 m := &protocol.ColumnMapper{ 345 URI: uri, 346 Converter: converter, 347 Content: content, 348 } 349 if change.Range == nil { 350 return nil, errors.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) 351 } 352 spn, err := m.RangeSpan(*change.Range) 353 if err != nil { 354 return nil, err 355 } 356 if !spn.HasOffset() { 357 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 358 } 359 start, end := spn.Start().Offset(), spn.End().Offset() 360 if end < start { 361 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 362 } 363 var buf bytes.Buffer 364 buf.Write(content[:start]) 365 buf.WriteString(change.Text) 366 buf.Write(content[end:]) 367 content = buf.Bytes() 368 } 369 return content, nil 370 } 371 372 func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction { 373 switch ct { 374 case protocol.Changed: 375 return source.Change 376 case protocol.Created: 377 return source.Create 378 case protocol.Deleted: 379 return source.Delete 380 } 381 return source.UnknownFileAction 382 }