golang.org/x/tools/gopls@v0.15.3/internal/progress/progress.go (about) 1 // Copyright 2020 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 // The progress package defines utilities for reporting the progress 6 // of long-running operations using features of the LSP client 7 // interface such as Progress and ShowMessage. 8 package progress 9 10 import ( 11 "context" 12 "fmt" 13 "io" 14 "math/rand" 15 "strconv" 16 "strings" 17 "sync" 18 19 "golang.org/x/tools/gopls/internal/protocol" 20 "golang.org/x/tools/internal/event" 21 "golang.org/x/tools/internal/event/tag" 22 "golang.org/x/tools/internal/xcontext" 23 ) 24 25 // NewTracker returns a new Tracker that reports progress to the 26 // specified client. 27 func NewTracker(client protocol.Client) *Tracker { 28 return &Tracker{ 29 client: client, 30 inProgress: make(map[protocol.ProgressToken]*WorkDone), 31 } 32 } 33 34 // A Tracker reports the progress of a long-running operation to an LSP client. 35 type Tracker struct { 36 client protocol.Client 37 supportsWorkDoneProgress bool 38 39 mu sync.Mutex 40 inProgress map[protocol.ProgressToken]*WorkDone 41 } 42 43 // SetSupportsWorkDoneProgress sets whether the client supports "work done" 44 // progress reporting. It must be set before using the tracker. 45 // 46 // TODO(rfindley): fix this broken initialization pattern. 47 // Also: do we actually need the fall-back progress behavior using ShowMessage? 48 // Surely ShowMessage notifications are too noisy to be worthwhile. 49 func (t *Tracker) SetSupportsWorkDoneProgress(b bool) { 50 t.supportsWorkDoneProgress = b 51 } 52 53 // SupportsWorkDoneProgress reports whether the tracker supports work done 54 // progress reporting. 55 func (t *Tracker) SupportsWorkDoneProgress() bool { 56 return t.supportsWorkDoneProgress 57 } 58 59 // Start notifies the client of work being done on the server. It uses either 60 // ShowMessage RPCs or $/progress messages, depending on the capabilities of 61 // the client. The returned WorkDone handle may be used to report incremental 62 // progress, and to report work completion. In particular, it is an error to 63 // call start and not call end(...) on the returned WorkDone handle. 64 // 65 // If token is empty, a token will be randomly generated. 66 // 67 // The progress item is considered cancellable if the given cancel func is 68 // non-nil. In this case, cancel is called when the work done 69 // 70 // Example: 71 // 72 // func Generate(ctx) (err error) { 73 // ctx, cancel := context.WithCancel(ctx) 74 // defer cancel() 75 // work := s.progress.start(ctx, "generate", "running go generate", cancel) 76 // defer func() { 77 // if err != nil { 78 // work.end(ctx, fmt.Sprintf("generate failed: %v", err)) 79 // } else { 80 // work.end(ctx, "done") 81 // } 82 // }() 83 // // Do the work... 84 // } 85 func (t *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone { 86 ctx = xcontext.Detach(ctx) // progress messages should not be cancelled 87 wd := &WorkDone{ 88 client: t.client, 89 token: token, 90 cancel: cancel, 91 } 92 if !t.supportsWorkDoneProgress { 93 // Previous iterations of this fallback attempted to retain cancellation 94 // support by using ShowMessageCommand with a 'Cancel' button, but this is 95 // not ideal as the 'Cancel' dialog stays open even after the command 96 // completes. 97 // 98 // Just show a simple message. Clients can implement workDone progress 99 // reporting to get cancellation support. 100 if err := wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 101 Type: protocol.Log, 102 Message: message, 103 }); err != nil { 104 event.Error(ctx, "showing start message for "+title, err) 105 } 106 return wd 107 } 108 if wd.token == nil { 109 token = strconv.FormatInt(rand.Int63(), 10) 110 err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ 111 Token: token, 112 }) 113 if err != nil { 114 wd.err = err 115 event.Error(ctx, "starting work for "+title, err) 116 return wd 117 } 118 wd.token = token 119 } 120 // At this point we have a token that the client knows about. Store the token 121 // before starting work. 122 t.mu.Lock() 123 t.inProgress[wd.token] = wd 124 t.mu.Unlock() 125 wd.cleanup = func() { 126 t.mu.Lock() 127 delete(t.inProgress, token) 128 t.mu.Unlock() 129 } 130 err := wd.client.Progress(ctx, &protocol.ProgressParams{ 131 Token: wd.token, 132 Value: &protocol.WorkDoneProgressBegin{ 133 Kind: "begin", 134 Cancellable: wd.cancel != nil, 135 Message: message, 136 Title: title, 137 }, 138 }) 139 if err != nil { 140 event.Error(ctx, "progress begin", err) 141 } 142 return wd 143 } 144 145 func (t *Tracker) Cancel(token protocol.ProgressToken) error { 146 t.mu.Lock() 147 defer t.mu.Unlock() 148 wd, ok := t.inProgress[token] 149 if !ok { 150 return fmt.Errorf("token %q not found in progress", token) 151 } 152 if wd.cancel == nil { 153 return fmt.Errorf("work %q is not cancellable", token) 154 } 155 wd.doCancel() 156 return nil 157 } 158 159 // WorkDone represents a unit of work that is reported to the client via the 160 // progress API. 161 type WorkDone struct { 162 client protocol.Client 163 // If token is nil, this workDone object uses the ShowMessage API, rather 164 // than $/progress. 165 token protocol.ProgressToken 166 // err is set if progress reporting is broken for some reason (for example, 167 // if there was an initial error creating a token). 168 err error 169 170 cancelMu sync.Mutex 171 cancelled bool 172 cancel func() 173 174 cleanup func() 175 } 176 177 func (wd *WorkDone) Token() protocol.ProgressToken { 178 return wd.token 179 } 180 181 func (wd *WorkDone) doCancel() { 182 wd.cancelMu.Lock() 183 defer wd.cancelMu.Unlock() 184 if !wd.cancelled { 185 wd.cancel() 186 } 187 } 188 189 // Report reports an update on WorkDone report back to the client. 190 func (wd *WorkDone) Report(ctx context.Context, message string, percentage float64) { 191 ctx = xcontext.Detach(ctx) // progress messages should not be cancelled 192 if wd == nil { 193 return 194 } 195 wd.cancelMu.Lock() 196 cancelled := wd.cancelled 197 wd.cancelMu.Unlock() 198 if cancelled { 199 return 200 } 201 if wd.err != nil || wd.token == nil { 202 // Not using the workDone API, so we do nothing. It would be far too spammy 203 // to send incremental messages. 204 return 205 } 206 message = strings.TrimSuffix(message, "\n") 207 err := wd.client.Progress(ctx, &protocol.ProgressParams{ 208 Token: wd.token, 209 Value: &protocol.WorkDoneProgressReport{ 210 Kind: "report", 211 // Note that in the LSP spec, the value of Cancellable may be changed to 212 // control whether the cancel button in the UI is enabled. Since we don't 213 // yet use this feature, the value is kept constant here. 214 Cancellable: wd.cancel != nil, 215 Message: message, 216 Percentage: uint32(percentage), 217 }, 218 }) 219 if err != nil { 220 event.Error(ctx, "reporting progress", err) 221 } 222 } 223 224 // End reports a workdone completion back to the client. 225 func (wd *WorkDone) End(ctx context.Context, message string) { 226 ctx = xcontext.Detach(ctx) // progress messages should not be cancelled 227 if wd == nil { 228 return 229 } 230 var err error 231 switch { 232 case wd.err != nil: 233 // There is a prior error. 234 case wd.token == nil: 235 // We're falling back to message-based reporting. 236 err = wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 237 Type: protocol.Info, 238 Message: message, 239 }) 240 default: 241 err = wd.client.Progress(ctx, &protocol.ProgressParams{ 242 Token: wd.token, 243 Value: &protocol.WorkDoneProgressEnd{ 244 Kind: "end", 245 Message: message, 246 }, 247 }) 248 } 249 if err != nil { 250 event.Error(ctx, "ending work", err) 251 } 252 if wd.cleanup != nil { 253 wd.cleanup() 254 } 255 } 256 257 // NewEventWriter returns an [io.Writer] that calls the context's 258 // event printer for each data payload, wrapping it with the 259 // operation=generate tag to distinguish its logs from others. 260 func NewEventWriter(ctx context.Context, operation string) io.Writer { 261 return &eventWriter{ctx: ctx, operation: operation} 262 } 263 264 type eventWriter struct { 265 ctx context.Context 266 operation string 267 } 268 269 func (ew *eventWriter) Write(p []byte) (n int, err error) { 270 event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation)) 271 return len(p), nil 272 } 273 274 // NewWorkDoneWriter wraps a WorkDone handle to provide a Writer interface, 275 // so that workDone reporting can more easily be hooked into commands. 276 func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) io.Writer { 277 return &workDoneWriter{ctx: ctx, wd: wd} 278 } 279 280 // workDoneWriter wraps a workDone handle to provide a Writer interface, 281 // so that workDone reporting can more easily be hooked into commands. 282 type workDoneWriter struct { 283 // In order to implement the io.Writer interface, we must close over ctx. 284 ctx context.Context 285 wd *WorkDone 286 } 287 288 func (wdw *workDoneWriter) Write(p []byte) (n int, err error) { 289 wdw.wd.Report(wdw.ctx, string(p), 0) 290 // Don't fail just because of a failure to report progress. 291 return len(p), nil 292 }