github.com/jd-ly/tools@v0.5.7/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/jd-ly/tools/internal/jsonrpc2" 15 "github.com/jd-ly/tools/internal/lsp/protocol" 16 "github.com/jd-ly/tools/internal/lsp/source" 17 "github.com/jd-ly/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 return s.didModifyFiles(ctx, []source.FileModification{{ 93 URI: uri, 94 Action: source.Open, 95 Version: params.TextDocument.Version, 96 Text: []byte(params.TextDocument.Text), 97 LanguageID: params.TextDocument.LanguageID, 98 }}, FromDidOpen) 99 } 100 101 func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 102 uri := params.TextDocument.URI.SpanURI() 103 if !uri.IsFile() { 104 return nil 105 } 106 107 text, err := s.changedText(ctx, uri, params.ContentChanges) 108 if err != nil { 109 return err 110 } 111 c := source.FileModification{ 112 URI: uri, 113 Action: source.Change, 114 Version: params.TextDocument.Version, 115 Text: text, 116 } 117 if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil { 118 return err 119 } 120 return s.warnAboutModifyingGeneratedFiles(ctx, uri) 121 } 122 123 // warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a 124 // generated file for the first time. 125 func (s *Server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri span.URI) error { 126 s.changedFilesMu.Lock() 127 _, ok := s.changedFiles[uri] 128 if !ok { 129 s.changedFiles[uri] = struct{}{} 130 } 131 s.changedFilesMu.Unlock() 132 133 // This file has already been edited before. 134 if ok { 135 return nil 136 } 137 138 // Ideally, we should be able to specify that a generated file should 139 // be opened as read-only. Tell the user that they should not be 140 // editing a generated file. 141 view, err := s.session.ViewOf(uri) 142 if err != nil { 143 return err 144 } 145 snapshot, release := view.Snapshot(ctx) 146 isGenerated := source.IsGenerated(ctx, snapshot, uri) 147 release() 148 149 if !isGenerated { 150 return nil 151 } 152 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 153 Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()), 154 Type: protocol.Warning, 155 }) 156 } 157 158 func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { 159 var modifications []source.FileModification 160 for _, change := range params.Changes { 161 uri := change.URI.SpanURI() 162 if !uri.IsFile() { 163 continue 164 } 165 action := changeTypeToFileAction(change.Type) 166 modifications = append(modifications, source.FileModification{ 167 URI: uri, 168 Action: action, 169 OnDisk: true, 170 }) 171 } 172 return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) 173 } 174 175 func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { 176 uri := params.TextDocument.URI.SpanURI() 177 if !uri.IsFile() { 178 return nil 179 } 180 c := source.FileModification{ 181 URI: uri, 182 Action: source.Save, 183 Version: params.TextDocument.Version, 184 } 185 if params.Text != nil { 186 c.Text = []byte(*params.Text) 187 } 188 return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave) 189 } 190 191 func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { 192 uri := params.TextDocument.URI.SpanURI() 193 if !uri.IsFile() { 194 return nil 195 } 196 return s.didModifyFiles(ctx, []source.FileModification{ 197 { 198 URI: uri, 199 Action: source.Close, 200 Version: -1, 201 Text: nil, 202 }, 203 }, FromDidClose) 204 } 205 206 func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error { 207 // diagnosticWG tracks outstanding diagnostic work as a result of this file 208 // modification. 209 var diagnosticWG sync.WaitGroup 210 if s.session.Options().VerboseWorkDoneProgress { 211 work := s.progress.start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) 212 defer func() { 213 go func() { 214 diagnosticWG.Wait() 215 work.end("Done.") 216 }() 217 }() 218 } 219 220 // If the set of changes included directories, expand those directories 221 // to their files. 222 modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) 223 224 views, snapshots, releases, err := s.session.DidModifyFiles(ctx, modifications) 225 if err != nil { 226 return err 227 } 228 229 // Group files by best view and diagnose them. 230 viewURIs := map[source.View][]span.URI{} 231 for uri, view := range views { 232 viewURIs[view] = append(viewURIs[view], uri) 233 } 234 for view, uris := range viewURIs { 235 snapshot := snapshots[view] 236 if snapshot == nil { 237 panic(fmt.Sprintf("no snapshot assigned for files %v", uris)) 238 } 239 diagnosticWG.Add(1) 240 go func(snapshot source.Snapshot, uris []span.URI) { 241 defer diagnosticWG.Done() 242 s.diagnoseSnapshot(snapshot, uris, cause == FromDidChangeWatchedFiles) 243 }(snapshot, uris) 244 } 245 246 go func() { 247 diagnosticWG.Wait() 248 for _, release := range releases { 249 release() 250 } 251 }() 252 253 // After any file modifications, we need to update our watched files, 254 // in case something changed. Compute the new set of directories to watch, 255 // and if it differs from the current set, send updated registrations. 256 return s.updateWatchedDirectories(ctx) 257 } 258 259 // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a 260 // file change originating from the given cause. 261 func DiagnosticWorkTitle(cause ModificationSource) string { 262 return fmt.Sprintf("diagnosing %v", cause) 263 } 264 265 func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 266 if len(changes) == 0 { 267 return nil, errors.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) 268 } 269 270 // Check if the client sent the full content of the file. 271 // We accept a full content change even if the server expected incremental changes. 272 if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { 273 return []byte(changes[0].Text), nil 274 } 275 return s.applyIncrementalChanges(ctx, uri, changes) 276 } 277 278 func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { 279 fh, err := s.session.GetFile(ctx, uri) 280 if err != nil { 281 return nil, err 282 } 283 content, err := fh.Read() 284 if err != nil { 285 return nil, errors.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) 286 } 287 for _, change := range changes { 288 // Make sure to update column mapper along with the content. 289 converter := span.NewContentConverter(uri.Filename(), content) 290 m := &protocol.ColumnMapper{ 291 URI: uri, 292 Converter: converter, 293 Content: content, 294 } 295 if change.Range == nil { 296 return nil, errors.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) 297 } 298 spn, err := m.RangeSpan(*change.Range) 299 if err != nil { 300 return nil, err 301 } 302 if !spn.HasOffset() { 303 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 304 } 305 start, end := spn.Start().Offset(), spn.End().Offset() 306 if end < start { 307 return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) 308 } 309 var buf bytes.Buffer 310 buf.Write(content[:start]) 311 buf.WriteString(change.Text) 312 buf.Write(content[end:]) 313 content = buf.Bytes() 314 } 315 return content, nil 316 } 317 318 func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction { 319 switch ct { 320 case protocol.Changed: 321 return source.Change 322 case protocol.Created: 323 return source.Create 324 case protocol.Deleted: 325 return source.Delete 326 } 327 return source.UnknownFileAction 328 }