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.
14 "golang.org/x/tools/internal/event"
15 "golang.org/x/tools/internal/lsp/debug/tag"
16 "golang.org/x/tools/internal/lsp/protocol"
17 "golang.org/x/tools/internal/xcontext"
18 errors "golang.org/x/xerrors"
21 type progressTracker struct {
22 client protocol.Client
23 supportsWorkDoneProgress bool
26 inProgress map[protocol.ProgressToken]*workDone
29 func newProgressTracker(client protocol.Client) *progressTracker {
30 return &progressTracker{
32 inProgress: make(map[protocol.ProgressToken]*workDone),
36 // start notifies the client of work being done on the server. It uses either
37 // ShowMessage RPCs or $/progress messages, depending on the capabilities of
38 // the client. The returned WorkDone handle may be used to report incremental
39 // progress, and to report work completion. In particular, it is an error to
40 // call start and not call end(...) on the returned WorkDone handle.
42 // If token is empty, a token will be randomly generated.
44 // The progress item is considered cancellable if the given cancel func is
45 // non-nil. In this case, cancel is called when the work done
48 // func Generate(ctx) (err error) {
49 // ctx, cancel := context.WithCancel(ctx)
51 // work := s.progress.start(ctx, "generate", "running go generate", cancel)
54 // work.end(ctx, fmt.Sprintf("generate failed: %v", err))
56 // work.end(ctx, "done")
62 func (t *progressTracker) start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *workDone {
64 ctx: xcontext.Detach(ctx),
69 if !t.supportsWorkDoneProgress {
70 // Previous iterations of this fallback attempted to retain cancellation
71 // support by using ShowMessageCommand with a 'Cancel' button, but this is
72 // not ideal as the 'Cancel' dialog stays open even after the command
75 // Just show a simple message. Clients can implement workDone progress
76 // reporting to get cancellation support.
77 if err := wd.client.ShowMessage(wd.ctx, &protocol.ShowMessageParams{
81 event.Error(ctx, "showing start message for "+title, err)
86 token = strconv.FormatInt(rand.Int63(), 10)
87 err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{
92 event.Error(ctx, "starting work for "+title, err)
97 // At this point we have a token that the client knows about. Store the token
98 // before starting work.
100 t.inProgress[wd.token] = wd
102 wd.cleanup = func() {
104 delete(t.inProgress, token)
107 err := wd.client.Progress(ctx, &protocol.ProgressParams{
109 Value: &protocol.WorkDoneProgressBegin{
111 Cancellable: wd.cancel != nil,
117 event.Error(ctx, "generate progress begin", err)
122 func (t *progressTracker) cancel(ctx context.Context, token protocol.ProgressToken) error {
125 wd, ok := t.inProgress[token]
127 return errors.Errorf("token %q not found in progress", token)
129 if wd.cancel == nil {
130 return errors.Errorf("work %q is not cancellable", token)
136 // workDone represents a unit of work that is reported to the client via the
138 type workDone struct {
139 // ctx is detached, for sending $/progress updates.
141 client protocol.Client
142 // If token is nil, this workDone object uses the ShowMessage API, rather
144 token protocol.ProgressToken
145 // err is set if progress reporting is broken for some reason (for example,
146 // if there was an initial error creating a token).
156 func (wd *workDone) doCancel() {
158 defer wd.cancelMu.Unlock()
164 // report reports an update on WorkDone report back to the client.
165 func (wd *workDone) report(message string, percentage float64) {
170 cancelled := wd.cancelled
175 if wd.err != nil || wd.token == nil {
176 // Not using the workDone API, so we do nothing. It would be far too spammy
177 // to send incremental messages.
180 message = strings.TrimSuffix(message, "\n")
181 err := wd.client.Progress(wd.ctx, &protocol.ProgressParams{
183 Value: &protocol.WorkDoneProgressReport{
185 // Note that in the LSP spec, the value of Cancellable may be changed to
186 // control whether the cancel button in the UI is enabled. Since we don't
187 // yet use this feature, the value is kept constant here.
188 Cancellable: wd.cancel != nil,
190 Percentage: percentage,
194 event.Error(wd.ctx, "reporting progress", err)
198 // end reports a workdone completion back to the client.
199 func (wd *workDone) end(message string) {
206 // There is a prior error.
207 case wd.token == nil:
208 // We're falling back to message-based reporting.
209 err = wd.client.ShowMessage(wd.ctx, &protocol.ShowMessageParams{
214 err = wd.client.Progress(wd.ctx, &protocol.ProgressParams{
216 Value: &protocol.WorkDoneProgressEnd{
223 event.Error(wd.ctx, "ending work", err)
225 if wd.cleanup != nil {
230 // eventWriter writes every incoming []byte to
231 // event.Print with the operation=generate tag
232 // to distinguish its logs from others.
233 type eventWriter struct {
238 func (ew *eventWriter) Write(p []byte) (n int, err error) {
239 event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation))
243 // workDoneWriter wraps a workDone handle to provide a Writer interface,
244 // so that workDone reporting can more easily be hooked into commands.
245 type workDoneWriter struct {
249 func (wdw workDoneWriter) Write(p []byte) (n int, err error) {
250 wdw.wd.report(string(p), 0)
251 // Don't fail just because of a failure to report progress.