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