mirror of
https://github.com/golang/go
synced 2024-11-18 12:34:42 -07:00
978e77c455
Progress reporting has gotten complicated, and has had a couple bugs. Factor out progress-related behavior to a new progressTracker type, and use this to implement some unit tests. Also rename some methods to remove stuttering, and reorganize the code to be more logical. Fixes golang/go#40527 Change-Id: I93d53a67982460e7171f892021e99f4523fe3e5d Reviewed-on: https://go-review.googlesource.com/c/tools/+/247407 Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
248 lines
6.7 KiB
Go
248 lines
6.7 KiB
Go
// 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")
|
|
}
|