diff options
Diffstat (limited to 'internal/lsp/progress')
-rw-r--r-- | internal/lsp/progress/progress.go | 269 | ||||
-rw-r--r-- | internal/lsp/progress/progress_test.go | 161 |
2 files changed, 0 insertions, 430 deletions
diff --git a/internal/lsp/progress/progress.go b/internal/lsp/progress/progress.go deleted file mode 100644 index 18e1bd0f1..000000000 --- a/internal/lsp/progress/progress.go +++ /dev/null @@ -1,269 +0,0 @@ -// 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 progress - -import ( - "context" - "math/rand" - "strconv" - "strings" - "sync" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/xcontext" - errors "golang.org/x/xerrors" -) - -type Tracker struct { - client protocol.Client - supportsWorkDoneProgress bool - - mu sync.Mutex - inProgress map[protocol.ProgressToken]*WorkDone -} - -func NewTracker(client protocol.Client) *Tracker { - return &Tracker{ - client: client, - inProgress: make(map[protocol.ProgressToken]*WorkDone), - } -} - -func (tracker *Tracker) SetSupportsWorkDoneProgress(b bool) { - tracker.supportsWorkDoneProgress = b -} - -// Start notifies the client of work being done on the server. It uses either -// ShowMessage RPCs or $/progress messages, depending on the capabilities of -// the client. 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. In this case, cancel is called when the work done -// -// 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 *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone { - wd := &WorkDone{ - ctx: xcontext.Detach(ctx), - client: t.client, - token: token, - cancel: cancel, - } - if !t.supportsWorkDoneProgress { - // Previous iterations of this fallback attempted to retain cancellation - // support by using ShowMessageCommand with a 'Cancel' button, but this is - // not ideal as the 'Cancel' dialog stays open even after the command - // completes. - // - // Just show a simple message. Clients can implement workDone progress - // reporting to get cancellation support. - if err := wd.client.ShowMessage(wd.ctx, &protocol.ShowMessageParams{ - Type: protocol.Log, - Message: message, - }); err != nil { - event.Error(ctx, "showing start message for "+title, err) - } - return wd - } - if wd.token == nil { - token = strconv.FormatInt(rand.Int63(), 10) - err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ - Token: token, - }) - if err != nil { - wd.err = err - event.Error(ctx, "starting work for "+title, err) - return wd - } - wd.token = token - } - // At this point we have a token that the client knows about. Store the token - // before starting work. - 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 *Tracker) 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.doCancel() - return nil -} - -// WorkDone represents a unit of work that is reported to the client via the -// progress API. -type WorkDone struct { - // ctx is detached, for sending $/progress updates. - ctx context.Context - client protocol.Client - // If token is nil, this workDone object uses the ShowMessage API, rather - // than $/progress. - token protocol.ProgressToken - // err is set if progress reporting is broken for some reason (for example, - // if there was an initial error creating a token). - err error - - cancelMu sync.Mutex - cancelled bool - cancel func() - - cleanup func() -} - -func (wd *WorkDone) Token() protocol.ProgressToken { - return wd.token -} - -func (wd *WorkDone) doCancel() { - wd.cancelMu.Lock() - defer wd.cancelMu.Unlock() - if !wd.cancelled { - wd.cancel() - } -} - -// report reports an update on WorkDone report back to the client. -func (wd *WorkDone) Report(message string, percentage float64) { - if wd == nil { - return - } - wd.cancelMu.Lock() - cancelled := wd.cancelled - wd.cancelMu.Unlock() - if cancelled { - return - } - if wd.err != nil || wd.token == nil { - // Not using the workDone API, so we do nothing. It would be far too spammy - // to send incremental messages. - return - } - message = strings.TrimSuffix(message, "\n") - err := wd.client.Progress(wd.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: uint32(percentage), - }, - }) - if err != nil { - event.Error(wd.ctx, "reporting progress", err) - } -} - -// end reports a workdone completion back to the client. -func (wd *WorkDone) End(message string) { - if wd == nil { - return - } - var err error - switch { - case wd.err != nil: - // There is a prior error. - case wd.token == nil: - // We're falling back to message-based reporting. - err = wd.client.ShowMessage(wd.ctx, &protocol.ShowMessageParams{ - Type: protocol.Info, - Message: message, - }) - default: - err = wd.client.Progress(wd.ctx, &protocol.ProgressParams{ - Token: wd.token, - Value: &protocol.WorkDoneProgressEnd{ - Kind: "end", - Message: message, - }, - }) - } - if err != nil { - event.Error(wd.ctx, "ending work", err) - } - if wd.cleanup != nil { - wd.cleanup() - } -} - -// 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 NewEventWriter(ctx context.Context, operation string) *EventWriter { - return &EventWriter{ctx: ctx, operation: operation} -} - -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 -} - -// WorkDoneWriter wraps a workDone handle to provide a Writer interface, -// so that workDone reporting can more easily be hooked into commands. -type WorkDoneWriter struct { - wd *WorkDone -} - -func NewWorkDoneWriter(wd *WorkDone) *WorkDoneWriter { - return &WorkDoneWriter{wd: wd} -} - -func (wdw WorkDoneWriter) Write(p []byte) (n int, err error) { - wdw.wd.Report(string(p), 0) - // Don't fail just because of a failure to report progress. - return len(p), nil -} diff --git a/internal/lsp/progress/progress_test.go b/internal/lsp/progress/progress_test.go deleted file mode 100644 index b3c821938..000000000 --- a/internal/lsp/progress/progress_test.go +++ /dev/null @@ -1,161 +0,0 @@ -// 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 progress - -import ( - "context" - "fmt" - "sync" - "testing" - - "golang.org/x/tools/internal/lsp/protocol" -) - -type fakeClient struct { - protocol.Client - - token protocol.ProgressToken - - mu sync.Mutex - created, begun, reported, messages, ended int -} - -func (c *fakeClient) checkToken(token protocol.ProgressToken) { - if token == nil { - panic("nil token in progress message") - } - if c.token != nil && c.token != token { - panic(fmt.Errorf("invalid token in progress message: got %v, want %v", token, c.token)) - } -} - -func (c *fakeClient) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { - c.mu.Lock() - defer c.mu.Unlock() - c.checkToken(params.Token) - c.created++ - return nil -} - -func (c *fakeClient) Progress(ctx context.Context, params *protocol.ProgressParams) error { - c.mu.Lock() - defer c.mu.Unlock() - c.checkToken(params.Token) - switch params.Value.(type) { - case *protocol.WorkDoneProgressBegin: - c.begun++ - case *protocol.WorkDoneProgressReport: - c.reported++ - case *protocol.WorkDoneProgressEnd: - c.ended++ - default: - panic(fmt.Errorf("unknown progress value %T", params.Value)) - } - return nil -} - -func (c *fakeClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { - c.mu.Lock() - defer c.mu.Unlock() - c.messages++ - return nil -} - -func setup(token protocol.ProgressToken) (context.Context, *Tracker, *fakeClient) { - c := &fakeClient{} - tracker := NewTracker(c) - tracker.SetSupportsWorkDoneProgress(true) - return context.Background(), tracker, c -} - -func TestProgressTracker_Reporting(t *testing.T) { - for _, test := range []struct { - name string - supported bool - token protocol.ProgressToken - wantReported, wantCreated, wantBegun, wantEnded int - wantMessages int - }{ - { - name: "unsupported", - wantMessages: 2, - }, - { - name: "random token", - supported: true, - wantCreated: 1, - wantBegun: 1, - wantReported: 1, - wantEnded: 1, - }, - { - name: "string token", - supported: true, - token: "token", - wantBegun: 1, - wantReported: 1, - wantEnded: 1, - }, - { - name: "numeric token", - supported: true, - token: 1, - wantReported: 1, - wantBegun: 1, - wantEnded: 1, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - ctx, tracker, client := setup(test.token) - ctx, cancel := context.WithCancel(ctx) - defer cancel() - tracker.supportsWorkDoneProgress = test.supported - work := tracker.Start(ctx, "work", "message", test.token, nil) - client.mu.Lock() - gotCreated, gotBegun := client.created, client.begun - client.mu.Unlock() - if gotCreated != test.wantCreated { - t.Errorf("got %d created tokens, want %d", gotCreated, test.wantCreated) - } - if gotBegun != test.wantBegun { - t.Errorf("got %d work begun, want %d", gotBegun, test.wantBegun) - } - // Ignore errors: this is just testing the reporting behavior. - work.Report("report", 50) - client.mu.Lock() - gotReported := client.reported - client.mu.Unlock() - if gotReported != test.wantReported { - t.Errorf("got %d progress reports, want %d", gotReported, test.wantCreated) - } - work.End("done") - client.mu.Lock() - gotEnded, gotMessages := client.ended, client.messages - client.mu.Unlock() - if gotEnded != test.wantEnded { - t.Errorf("got %d ended reports, want %d", gotEnded, test.wantEnded) - } - if gotMessages != test.wantMessages { - t.Errorf("got %d messages, want %d", gotMessages, test.wantMessages) - } - }) - } -} - -func TestProgressTracker_Cancellation(t *testing.T) { - for _, token := range []protocol.ProgressToken{nil, 1, "a"} { - ctx, tracker, _ := setup(token) - var canceled bool - cancel := func() { canceled = true } - work := tracker.Start(ctx, "work", "message", token, cancel) - if err := tracker.Cancel(ctx, work.Token()); err != nil { - t.Fatal(err) - } - if !canceled { - t.Errorf("tracker.cancel(...): cancel not called") - } - } -} |