github.com/april1989/origin-go-tools@v0.0.32/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 "sync" 13 14 "github.com/april1989/origin-go-tools/internal/jsonrpc2" 15 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 16 "github.com/april1989/origin-go-tools/internal/lsp/source" 17 "github.com/april1989/origin-go-tools/internal/span" 18 errors "golang.org/x/xerrors" 19 ) 20 21 // ModificationSource identifies the originating cause of a file modification. 22 type ModificationSource int 23 24 const ( 25 // FromDidOpen is a file modification caused by opening a file. 26 FromDidOpen = ModificationSource(iota) 27 28 // FromDidChange is a file modification caused by changing a file. 29 FromDidChange 30 31 // FromDidChangeWatchedFiles is a file modification caused by a change to a 32 // watched file. 33 FromDidChangeWatchedFiles 34 35 // FromDidSave is a file modification caused by a file save. 36 FromDidSave 37 38 // FromDidClose is a file modification caused by closing a file. 39 FromDidClose 40 41 // FromRegenerateCgo refers to file modifications caused by regenerating 42 // the cgo sources for the workspace. 43 FromRegenerateCgo 44 45 // FromInitialWorkspaceLoad refers to the loading of all packages in the 46 // workspace when the view is first created. 47 FromInitialWorkspaceLoad 48 ) 49 50 func (m ModificationSource) String() string { 51 switch m { 52 case FromDidOpen: 53 return "opened files" 54 case FromDidChange: 55 return "changed files" 56 case FromDidChangeWatchedFiles: 57 return "files changed on disk" 58 case FromDidSave: 59 return "saved files" 60 case FromDidClose: 61 return "close files" 62 case FromRegenerateCgo: 63 return "regenerate cgo" 64 case FromInitialWorkspaceLoad: 65 return "initial workspace load" 66 default: 67 return "unknown file modification" 68 } 69 } 70 71 func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { 72 uri := params.TextDocument.URI.SpanURI() 73 if !uri.IsFile() { 74 return nil 75 } 76 // There may not be any matching view in the current session. If that's 77 // the case, try creating a new view based on the opened file path. 78 // 79 // TODO(rstambler): This seems like it would continuously add new 80 // views, but it won't because ViewOf only returns an error when there 81 // are no views in the session. I don't know if that logic should go 82 // here, or if we can continue to rely on that implementation detail. 83 if _, err := s.session.ViewOf(uri); err != nil { 84 dir := filepath.Dir(uri.Filename()) 85 if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{ 86 URI: string(protocol.URIFromPath(dir)), 87 Name: filepath.Base(dir), 88 }}); err != nil { 89 return err 90 } 91 } 92 93 return s.didModifyFiles(ctx, []source.FileModification{ 94 { 95 URI: uri, 96 Action: source.Open, 97 Version: params.TextDocument.Version, 98 Text: []byte(params.TextDocument.Text), 99 LanguageID: params.TextDocument.LanguageID, 100 }, 101 }, FromDidOpen) 102 } 103 104 func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 105 uri := params.TextDocument.URI.SpanURI() 106 if !uri.IsFile() { 107 return nil 108 } 109 110 text, err := s.changedText(ctx, uri, params.ContentChanges) 111 if err != nil { 112 return err 113 } 114 c := source.FileModification{ 115 URI: uri, 116 Action: source.Change, 117 Version: params.TextDocument.Version, 118 Text: text, 119 } 120 if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil { 121 return err 122 } 123 124 s.changedFilesMu.Lock() 125 defer s.changedFilesMu.Unlock() 126 127 s.changedFiles[uri] = struct{}{} 128 return nil 129 } 130 131 func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { 132 var modifications []source.FileModification 133 for _, change := range params.Changes { 134 uri := change.URI.SpanURI() 135 if !uri.IsFile() { 136 continue 137 } 138 action := changeTypeToFileAction(change.Type) 139 modifications = append(modifications, source.FileModification{ 140 URI: uri, 141 Action: action, 142 OnDisk: true, 143 }) 144 } 145 return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) 146 } 147 148 func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { 149 uri := params.TextDocument.URI.SpanURI() 150 if !uri.IsFile() { 151 return nil 152 } 153 c := source.FileModification{ 154 URI: uri, 155 Action: source.Save, 156 Version: params.TextDocument.Version, 157 } 158 if params.Text != nil { 159 c.Text = []byte(*params.Text) 160 } 161 return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave) 162 } 163 164 func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { 165 uri := params.TextDocument.URI.SpanURI() 166 if !uri.IsFile() { 167 return nil 168 } 169 return s.didModifyFiles(ctx, []source.FileModification{ 170 { 171 URI: uri, 172 Action: source.Close, 173 Version: -1, 174 Text: nil, 175 }, 176 }, FromDidClose) 177 } 178 179 func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error { 180 // diagnosticWG tracks outstanding diagnostic work as a result of this file 181 // modification. 182 var diagnosticWG sync.WaitGroup 183 if s.session.Options().VerboseWorkDoneProgress { 184 work := s.progress.start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) 185 defer func() { 186 go func() { 187 diagnosticWG.Wait() 188 work.end("Done.") 189 }() 190 }() 191 } 192 snapshots, releases, deletions, err := s.session.DidModifyFiles(ctx, modifications) 193 if err != nil { 194 return err 195 } 196 197 for _, uri := range deletions { 198 if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ 199 URI: protocol.URIFromSpanURI(uri), 200 Diagnostics: []protocol.Diagnostic{}, 201 Version: 0, 202 }); err != nil { 203 return err 204 } 205 } 206 snapshotByURI := make(map[span.URI]source.Snapshot) 207 for _, c := range modifications { 208 snapshotByURI[c.URI] = nil 209 } 210 // Avoid diagnosing the same snapshot twice. 211 snapshotSet := make(map[source.Snapshot][]span.URI) 212 for uri := range snapshotByURI { 213 view, err := s.session.ViewOf(uri) 214 if err != nil { 215 return err 216 } 217 var snapshot source.Snapshot 218 for _, s := range snapshots { 219 if s.View() == view { 220 if snapshot != nil { 221 return errors.Errorf("duplicate snapshots for the same view") 222 } 223 snapshot = s 224 } 225 } 226 // If the file isn't in any known views (for example, if it's in a dependency), 227 // we may not have a snapshot to map it to. As a result, we won't try to 228 // diagnose it. TODO(rstambler): Figure out how to handle this better. 229 if snapshot == nil { 230 continue 231 } 232 snapshotSet[snapshot] = append(snapshotSet[snapshot], uri) 233 snapshotByURI[uri] = snapshot 234 } 235 236 for _, mod := range modifications { 237 if mod.OnDisk || mod.Action != source.Change { 238 continue 239 } 240 snapshot, ok := snapshotByURI[mod.URI] 241 if !ok { 242 continue 243 } 244 // Ideally, we should be able to specify that a generated file should be opened as read-only. 245 // Tell the user that they should not be editing a generated file. 246 if s.wasFirstChange(mod.URI) && source.IsGenerated(ctx, snapshot, mod.URI) { 247 if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 248 Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", mod.URI.Filename()), 249 Type: protocol.Warning, 250 }); err != nil { 251 return err 252 } 253 } 254 } 255 256 for snapshot, uris := range snapshotSet { 257 // If a modification comes in for the view's go.mod file and the view 258 // was never properly initialized, or the view does not have 259 // a go.mod file, try to recreate the associated view. 260 if modfile := snapshot.View().ModFile(); modfile == "" { 261 for _, uri := range uris { 262 // Don't rebuild the view until the go.mod is on disk. 263 if !snapshot.IsSaved(uri) { 264 continue 265 } 266 fh, err := snapshot.GetFile(ctx, uri) 267 if err != nil { 268 return err 269 } 270 switch fh.Kind() { 271 case source.Mod: 272 newSnapshot, release, err := snapshot.View().Rebuild(ctx) 273 releases = append(releases, release) 274 if err != nil { 275 return err 276 } 277 // Update the snapshot to the rebuilt one. 278 snapshot = newSnapshot 279 } 280 } 281 } 282 diagnosticWG.Add(1) 283 go func(snapshot source.Snapshot) { 284 defer diagnosticWG.Done() 285 s.diagnoseSnapshot(snapshot) 286 }(snapshot) 287 } 288 289 go func() { 290 diagnosticWG.Wait() 291 for _, release := range releases { 292 release() 293 } 294 }() 295 // After any file modifications, we need to update our watched files, 296 // in case something changed. Compute the new set of directories to watch, 297 // and if it differs from the current set, send updated registrations. 298 if err := s.updateWatchedDirectories(ctx, snapshots); err != nil { 299 return err 300 } 301 return nil 302 } 303 304 // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a 305 // file change originating from the given cause. 306 func DiagnosticWorkTitle(cause ModificationSource) string { 307 return fmt.Sprintf("diagnosing %v", cause) 308 } 309 310 func (s *Server) wasFirstChange(uri span.URI) bool { 311 s.changedFilesMu.Lock() 312 defer s.changedFilesMu.Unlock() 313 314 if s.changedFiles == nil { 315 s.changedFiles = make(map[span.URI]struct{}) 316 } 317 _, ok := s.changedFiles[uri] 318 return !ok 319 } 320 321 func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 322 if len(changes) == 0 { 323 return nil, errors.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) 324 } 325 326 // Check if the client sent the full content of the file. 327 // We accept a full content change even if the server expected incremental changes. 328 if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { 329 return []byte(changes[0].Text), nil 330 } 331 return s.applyIncrementalChanges(ctx, uri, changes) 332 } 333 334 func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 335 fh, err := s.session.GetFile(ctx, uri) 336 if err != nil { 337 return nil, err 338 } 339 content, err := fh.Read() 340 if err != nil { 341 return nil, errors.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) 342 } 343 for _, change := range changes { 344 // Make sure to update column mapper along with the content. 345 converter := span.NewContentConverter(uri.Filename(), content) 346 m := &protocol.ColumnMapper{ 347 URI: uri, 348 Converter: converter, 349 Content: content, 350 } 351 if change.Range == nil { 352 return nil, errors.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) 353 } 354 spn, err := m.RangeSpan(*change.Range) 355 if err != nil { 356 return nil, err 357 } 358 if !spn.HasOffset() { 359 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 360 } 361 start, end := spn.Start().Offset(), spn.End().Offset() 362 if end < start { 363 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 364 } 365 var buf bytes.Buffer 366 buf.Write(content[:start]) 367 buf.WriteString(change.Text) 368 buf.Write(content[end:]) 369 content = buf.Bytes() 370 } 371 return content, nil 372 } 373 374 func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction { 375 switch ct { 376 case protocol.Changed: 377 return source.Change 378 case protocol.Created: 379 return source.Create 380 case protocol.Deleted: 381 return source.Delete 382 } 383 return source.UnknownFileAction 384 }