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