|  | // Copyright 2020 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package lsp | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "io" | 
|  | "math/rand" | 
|  | "strconv" | 
|  | "sync" | 
|  |  | 
|  | "golang.org/x/tools/internal/event" | 
|  | "golang.org/x/tools/internal/lsp/debug/tag" | 
|  | "golang.org/x/tools/internal/lsp/protocol" | 
|  | errors "golang.org/x/xerrors" | 
|  | ) | 
|  |  | 
|  | type progressTracker struct { | 
|  | client                   protocol.Client | 
|  | supportsWorkDoneProgress bool | 
|  |  | 
|  | mu         sync.Mutex | 
|  | inProgress map[protocol.ProgressToken]*workDone | 
|  | } | 
|  |  | 
|  | func newProgressTracker(client protocol.Client) *progressTracker { | 
|  | return &progressTracker{ | 
|  | client:     client, | 
|  | inProgress: make(map[protocol.ProgressToken]*workDone), | 
|  | } | 
|  | } | 
|  |  | 
|  | // start issues a $/progress notification to begin a unit of work on the | 
|  | // server. The returned WorkDone handle may be used to report incremental | 
|  | // progress, and to report work completion. In particular, it is an error to | 
|  | // call start and not call end(...) on the returned WorkDone handle. | 
|  | // | 
|  | // If token is empty, a token will be randomly generated. | 
|  | // | 
|  | // The progress item is considered cancellable if the given cancel func is | 
|  | // non-nil. | 
|  | // | 
|  | // Example: | 
|  | //  func Generate(ctx) (err error) { | 
|  | //    ctx, cancel := context.WithCancel(ctx) | 
|  | //    defer cancel() | 
|  | //    work := s.progress.start(ctx, "generate", "running go generate", cancel) | 
|  | //    defer func() { | 
|  | //      if err != nil { | 
|  | //        work.end(ctx, fmt.Sprintf("generate failed: %v", err)) | 
|  | //      } else { | 
|  | //        work.end(ctx, "done") | 
|  | //      } | 
|  | //    }() | 
|  | //    // Do the work... | 
|  | //  } | 
|  | // | 
|  | func (t *progressTracker) start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *workDone { | 
|  | wd := &workDone{ | 
|  | client: t.client, | 
|  | token:  token, | 
|  | cancel: cancel, | 
|  | } | 
|  | if !t.supportsWorkDoneProgress { | 
|  | wd.startErr = errors.New("workdone reporting is not supported") | 
|  | return wd | 
|  | } | 
|  | if wd.token == nil { | 
|  | wd.token = strconv.FormatInt(rand.Int63(), 10) | 
|  | err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ | 
|  | Token: wd.token, | 
|  | }) | 
|  | if err != nil { | 
|  | wd.startErr = err | 
|  | event.Error(ctx, "starting work for "+title, err) | 
|  | return wd | 
|  | } | 
|  | } | 
|  | t.mu.Lock() | 
|  | t.inProgress[wd.token] = wd | 
|  | t.mu.Unlock() | 
|  | wd.cleanup = func() { | 
|  | t.mu.Lock() | 
|  | delete(t.inProgress, token) | 
|  | t.mu.Unlock() | 
|  | } | 
|  | err := wd.client.Progress(ctx, &protocol.ProgressParams{ | 
|  | Token: wd.token, | 
|  | Value: &protocol.WorkDoneProgressBegin{ | 
|  | Kind:        "begin", | 
|  | Cancellable: wd.cancel != nil, | 
|  | Message:     message, | 
|  | Title:       title, | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | event.Error(ctx, "generate progress begin", err) | 
|  | } | 
|  | return wd | 
|  | } | 
|  |  | 
|  | func (t *progressTracker) cancel(ctx context.Context, token protocol.ProgressToken) error { | 
|  | t.mu.Lock() | 
|  | defer t.mu.Unlock() | 
|  | wd, ok := t.inProgress[token] | 
|  | if !ok { | 
|  | return errors.Errorf("token %q not found in progress", token) | 
|  | } | 
|  | if wd.cancel == nil { | 
|  | return errors.Errorf("work %q is not cancellable", token) | 
|  | } | 
|  | wd.cancel() | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // newProgressWriter returns an io.WriterCloser that can be used | 
|  | // to report progress on a command based on the client capabilities. | 
|  | func (t *progressTracker) newWriter(ctx context.Context, title, beginMsg, msg string, token protocol.ProgressToken, cancel func()) io.WriteCloser { | 
|  | if t.supportsWorkDoneProgress { | 
|  | wd := t.start(ctx, title, beginMsg, token, cancel) | 
|  | return &workDoneWriter{ctx, wd} | 
|  | } | 
|  | mw := &messageWriter{ctx, cancel, t.client} | 
|  | mw.start(msg) | 
|  | return mw | 
|  | } | 
|  |  | 
|  | // workDone represents a unit of work that is reported to the client via the | 
|  | // progress API. | 
|  | type workDone struct { | 
|  | client   protocol.Client | 
|  | startErr error | 
|  | token    protocol.ProgressToken | 
|  | cancel   func() | 
|  | cleanup  func() | 
|  | } | 
|  |  | 
|  | // report reports an update on WorkDone report back to the client. | 
|  | func (wd *workDone) report(ctx context.Context, message string, percentage float64) error { | 
|  | if wd.startErr != nil { | 
|  | return wd.startErr | 
|  | } | 
|  | return wd.client.Progress(ctx, &protocol.ProgressParams{ | 
|  | Token: wd.token, | 
|  | Value: &protocol.WorkDoneProgressReport{ | 
|  | Kind: "report", | 
|  | // Note that in the LSP spec, the value of Cancellable may be changed to | 
|  | // control whether the cancel button in the UI is enabled. Since we don't | 
|  | // yet use this feature, the value is kept constant here. | 
|  | Cancellable: wd.cancel != nil, | 
|  | Message:     message, | 
|  | Percentage:  percentage, | 
|  | }, | 
|  | }) | 
|  | } | 
|  |  | 
|  | // end reports a workdone completion back to the client. | 
|  | func (wd *workDone) end(ctx context.Context, message string) error { | 
|  | if wd.startErr != nil { | 
|  | return wd.startErr | 
|  | } | 
|  | err := wd.client.Progress(ctx, &protocol.ProgressParams{ | 
|  | Token: wd.token, | 
|  | Value: &protocol.WorkDoneProgressEnd{ | 
|  | Kind:    "end", | 
|  | Message: message, | 
|  | }, | 
|  | }) | 
|  | if wd.cleanup != nil { | 
|  | wd.cleanup() | 
|  | } | 
|  | return err | 
|  | } | 
|  |  | 
|  | // eventWriter writes every incoming []byte to | 
|  | // event.Print with the operation=generate tag | 
|  | // to distinguish its logs from others. | 
|  | type eventWriter struct { | 
|  | ctx       context.Context | 
|  | operation string | 
|  | } | 
|  |  | 
|  | func (ew *eventWriter) Write(p []byte) (n int, err error) { | 
|  | event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation)) | 
|  | return len(p), nil | 
|  | } | 
|  |  | 
|  | // messageWriter implements progressWriter and only tells the user that | 
|  | // a command has started through window/showMessage, but does not report | 
|  | // anything afterwards. This is because each log shows up as a separate window | 
|  | // and therefore would be obnoxious to show every incoming line. Request | 
|  | // cancellation happens synchronously through the ShowMessageRequest response. | 
|  | type messageWriter struct { | 
|  | ctx    context.Context | 
|  | cancel func() | 
|  | client protocol.Client | 
|  | } | 
|  |  | 
|  | func (lw *messageWriter) Write(p []byte) (n int, err error) { | 
|  | return len(p), nil | 
|  | } | 
|  |  | 
|  | func (lw *messageWriter) start(msg string) { | 
|  | go func() { | 
|  | const cancel = "Cancel" | 
|  | item, err := lw.client.ShowMessageRequest(lw.ctx, &protocol.ShowMessageRequestParams{ | 
|  | Type:    protocol.Log, | 
|  | Message: msg, | 
|  | Actions: []protocol.MessageActionItem{{ | 
|  | Title: "Cancel", | 
|  | }}, | 
|  | }) | 
|  | if err != nil { | 
|  | event.Error(lw.ctx, "error sending message request", err) | 
|  | return | 
|  | } | 
|  | if item != nil && item.Title == "Cancel" { | 
|  | lw.cancel() | 
|  | } | 
|  | }() | 
|  | } | 
|  |  | 
|  | func (lw *messageWriter) Close() error { | 
|  | return lw.client.ShowMessage(lw.ctx, &protocol.ShowMessageParams{ | 
|  | Type:    protocol.Info, | 
|  | Message: "go generate has finished", | 
|  | }) | 
|  | } | 
|  |  | 
|  | // workDoneWriter implements progressWriter by sending $/progress notifications | 
|  | // to the client. Request cancellations happens separately through the | 
|  | // window/workDoneProgress/cancel request, in which case the given context will | 
|  | // be rendered done. | 
|  | type workDoneWriter struct { | 
|  | ctx context.Context | 
|  | wd  *workDone | 
|  | } | 
|  |  | 
|  | func (wdw *workDoneWriter) Write(p []byte) (n int, err error) { | 
|  | return len(p), wdw.wd.report(wdw.ctx, string(p), 0) | 
|  | } | 
|  |  | 
|  | func (wdw *workDoneWriter) Close() error { | 
|  | return wdw.wd.end(wdw.ctx, "finished") | 
|  | } |