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