internal/lsp: move the progress tracker to the session

Sometimes, we may want to report progress from functions inside of the
cache package, so move the progress tracker to the session to allow for
that.

Change-Id: I15409577a7a5080e7f0224a95d159de42856ffa7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/319330
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 88596d6..fcadc48 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -55,6 +55,15 @@
 // load calls packages.Load for the given scopes, updating package metadata,
 // import graph, and mapped files with the result.
 func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) error {
+	if s.view.Options().VerboseWorkDoneProgress {
+		work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading scopes %s", scopes), nil, nil)
+		defer func() {
+			go func() {
+				work.End("Done.")
+			}()
+		}()
+	}
+
 	var query []string
 	var containsDir bool // for logging
 	for _, scope := range scopes {
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 4e3531d..c2e08eb 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -14,6 +14,7 @@
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/imports"
+	"golang.org/x/tools/internal/lsp/progress"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/xcontext"
@@ -36,6 +37,8 @@
 
 	// gocmdRunner guards go command calls from concurrency errors.
 	gocmdRunner *gocommand.Runner
+
+	progress *progress.Tracker
 }
 
 type overlay struct {
@@ -131,6 +134,11 @@
 	s.options = options
 }
 
+func (s *Session) SetProgressTracker(tracker *progress.Tracker) {
+	// The progress tracker should be set before any view is initialized.
+	s.progress = tracker
+}
+
 func (s *Session) Shutdown(ctx context.Context) {
 	s.viewMu.Lock()
 	defer s.viewMu.Unlock()
diff --git a/internal/lsp/cmd/info.go b/internal/lsp/cmd/info.go
index fd53d8a..87ba428 100644
--- a/internal/lsp/cmd/info.go
+++ b/internal/lsp/cmd/info.go
@@ -178,6 +178,6 @@
 	} else {
 		txt += opts.LicensesText
 	}
-	fmt.Fprintf(os.Stdout, txt)
+	fmt.Fprint(os.Stdout, txt)
 	return nil
 }
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 47a3577..d810735 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -20,6 +20,7 @@
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/debug"
+	"golang.org/x/tools/internal/lsp/progress"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -65,7 +66,7 @@
 type commandDeps struct {
 	snapshot source.Snapshot            // present if cfg.forURI was set
 	fh       source.VersionedFileHandle // present if cfg.forURI was set
-	work     *workDone                  // present cfg.progress was set
+	work     *progress.WorkDone         // present cfg.progress was set
 }
 
 type commandFunc func(context.Context, commandDeps) error
@@ -90,19 +91,21 @@
 	}
 	ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
 	if cfg.progress != "" {
-		deps.work = c.s.progress.start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel)
+		deps.work = c.s.progress.Start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel)
 	}
 	runcmd := func() error {
 		defer cancel()
 		err := run(ctx, deps)
-		switch {
-		case errors.Is(err, context.Canceled):
-			deps.work.end("canceled")
-		case err != nil:
-			event.Error(ctx, "command error", err)
-			deps.work.end("failed")
-		default:
-			deps.work.end("completed")
+		if deps.work != nil {
+			switch {
+			case errors.Is(err, context.Canceled):
+				deps.work.End("canceled")
+			case err != nil:
+				event.Error(ctx, "command error", err)
+				deps.work.End("failed")
+			default:
+				deps.work.End("completed")
+			}
 		}
 		return err
 	}
@@ -349,7 +352,7 @@
 	})
 }
 
-func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *workDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
+func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
 	// TODO: fix the error reporting when this runs async.
 	pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
 	if err != nil {
@@ -362,8 +365,8 @@
 
 	// create output
 	buf := &bytes.Buffer{}
-	ew := &eventWriter{ctx: ctx, operation: "test"}
-	out := io.MultiWriter(ew, workDoneWriter{work}, buf)
+	ew := progress.NewEventWriter(ctx, "test")
+	out := io.MultiWriter(ew, progress.NewWorkDoneWriter(work), buf)
 
 	// Run `go test -run Func` on each test.
 	var failedTests int
@@ -435,7 +438,7 @@
 		progress:    title,
 		forURI:      args.Dir,
 	}, func(ctx context.Context, deps commandDeps) error {
-		er := &eventWriter{ctx: ctx, operation: "generate"}
+		er := progress.NewEventWriter(ctx, "generate")
 
 		pattern := "."
 		if args.Recursive {
@@ -446,7 +449,7 @@
 			Args:       []string{"-x", pattern},
 			WorkingDir: args.Dir.SpanURI().Filename(),
 		}
-		stderr := io.MultiWriter(er, workDoneWriter{deps.work})
+		stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(deps.work))
 		if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
 			return err
 		}
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 8d559e1..953c95c 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -384,7 +384,7 @@
 
 	if s.criticalErrorStatus == nil {
 		if errMsg != "" {
-			s.criticalErrorStatus = s.progress.start(ctx, WorkspaceLoadFailure, errMsg, nil, nil)
+			s.criticalErrorStatus = s.progress.Start(ctx, WorkspaceLoadFailure, errMsg, nil, nil)
 		}
 		return
 	}
@@ -392,10 +392,10 @@
 	// If an error is already shown to the user, update it or mark it as
 	// resolved.
 	if errMsg == "" {
-		s.criticalErrorStatus.end("Done.")
+		s.criticalErrorStatus.End("Done.")
 		s.criticalErrorStatus = nil
 	} else {
-		s.criticalErrorStatus.report(errMsg, 0)
+		s.criticalErrorStatus.Report(errMsg, 0)
 	}
 }
 
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 3c7bbfe..3c409d3 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -46,7 +46,7 @@
 		event.Error(ctx, "creating temp dir", err)
 		s.tempDir = ""
 	}
-	s.progress.supportsWorkDoneProgress = params.Capabilities.Window.WorkDoneProgress
+	s.progress.SetSupportsWorkDoneProgress(params.Capabilities.Window.WorkDoneProgress)
 
 	options := s.session.Options()
 	defer func() { s.session.SetOptions(options) }()
@@ -217,11 +217,11 @@
 
 	var wg sync.WaitGroup
 	if s.session.Options().VerboseWorkDoneProgress {
-		work := s.progress.start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil)
+		work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil)
 		defer func() {
 			go func() {
 				wg.Wait()
-				work.end("Done.")
+				work.End("Done.")
 			}()
 		}()
 	}
@@ -233,11 +233,11 @@
 		if !uri.IsFile() {
 			continue
 		}
-		work := s.progress.start(ctx, "Setting up workspace", "Loading packages...", nil, nil)
+		work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil)
 		snapshot, release, err := s.addView(ctx, folder.Name, uri)
 		if err != nil {
 			viewErrors[uri] = err
-			work.end(fmt.Sprintf("Error loading packages: %s", err))
+			work.End(fmt.Sprintf("Error loading packages: %s", err))
 			continue
 		}
 		var swg sync.WaitGroup
@@ -247,7 +247,7 @@
 			defer swg.Done()
 			defer allFoldersWg.Done()
 			snapshot.AwaitInitialized(ctx)
-			work.end("Finished loading packages.")
+			work.End("Finished loading packages.")
 		}()
 
 		// Print each view's environment.
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index a349a50..68c83f6 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -92,6 +92,7 @@
 		normalizers: tests.CollectNormalizers(datum.Exported),
 		editRecv:    make(chan map[span.URI]string, 1),
 	}
+
 	r.server = NewServer(session, testClient{runner: r})
 	tests.Run(t, r, datum)
 }
diff --git a/internal/lsp/progress.go b/internal/lsp/progress/progress.go
similarity index 79%
rename from internal/lsp/progress.go
rename to internal/lsp/progress/progress.go
index 719e9c3..18e1bd0 100644
--- a/internal/lsp/progress.go
+++ b/internal/lsp/progress/progress.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package lsp
+package progress
 
 import (
 	"context"
@@ -18,22 +18,26 @@
 	errors "golang.org/x/xerrors"
 )
 
-type progressTracker struct {
+type Tracker struct {
 	client                   protocol.Client
 	supportsWorkDoneProgress bool
 
 	mu         sync.Mutex
-	inProgress map[protocol.ProgressToken]*workDone
+	inProgress map[protocol.ProgressToken]*WorkDone
 }
 
-func newProgressTracker(client protocol.Client) *progressTracker {
-	return &progressTracker{
+func NewTracker(client protocol.Client) *Tracker {
+	return &Tracker{
 		client:     client,
-		inProgress: make(map[protocol.ProgressToken]*workDone),
+		inProgress: make(map[protocol.ProgressToken]*WorkDone),
 	}
 }
 
-// start notifies the client of work being done on the server. It uses either
+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
@@ -59,8 +63,8 @@
 //    // Do the work...
 //  }
 //
-func (t *progressTracker) start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *workDone {
-	wd := &workDone{
+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,
@@ -119,7 +123,7 @@
 	return wd
 }
 
-func (t *progressTracker) cancel(ctx context.Context, token protocol.ProgressToken) error {
+func (t *Tracker) Cancel(ctx context.Context, token protocol.ProgressToken) error {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 	wd, ok := t.inProgress[token]
@@ -133,9 +137,9 @@
 	return nil
 }
 
-// workDone represents a unit of work that is reported to the client via the
+// WorkDone represents a unit of work that is reported to the client via the
 // progress API.
-type workDone struct {
+type WorkDone struct {
 	// ctx is detached, for sending $/progress updates.
 	ctx    context.Context
 	client protocol.Client
@@ -153,7 +157,11 @@
 	cleanup func()
 }
 
-func (wd *workDone) doCancel() {
+func (wd *WorkDone) Token() protocol.ProgressToken {
+	return wd.token
+}
+
+func (wd *WorkDone) doCancel() {
 	wd.cancelMu.Lock()
 	defer wd.cancelMu.Unlock()
 	if !wd.cancelled {
@@ -162,7 +170,7 @@
 }
 
 // report reports an update on WorkDone report back to the client.
-func (wd *workDone) report(message string, percentage float64) {
+func (wd *WorkDone) Report(message string, percentage float64) {
 	if wd == nil {
 		return
 	}
@@ -196,7 +204,7 @@
 }
 
 // end reports a workdone completion back to the client.
-func (wd *workDone) end(message string) {
+func (wd *WorkDone) End(message string) {
 	if wd == nil {
 		return
 	}
@@ -227,27 +235,35 @@
 	}
 }
 
-// eventWriter writes every incoming []byte to
+// EventWriter writes every incoming []byte to
 // event.Print with the operation=generate tag
 // to distinguish its logs from others.
-type eventWriter struct {
+type EventWriter struct {
 	ctx       context.Context
 	operation string
 }
 
-func (ew *eventWriter) Write(p []byte) (n int, err error) {
+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,
+// 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
+type WorkDoneWriter struct {
+	wd *WorkDone
 }
 
-func (wdw workDoneWriter) Write(p []byte) (n int, err error) {
-	wdw.wd.report(string(p), 0)
+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_test.go b/internal/lsp/progress/progress_test.go
similarity index 90%
rename from internal/lsp/progress_test.go
rename to internal/lsp/progress/progress_test.go
index 40ca3d2..b3c8219 100644
--- a/internal/lsp/progress_test.go
+++ b/internal/lsp/progress/progress_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package lsp
+package progress
 
 import (
 	"context"
@@ -63,10 +63,10 @@
 	return nil
 }
 
-func setup(token protocol.ProgressToken) (context.Context, *progressTracker, *fakeClient) {
+func setup(token protocol.ProgressToken) (context.Context, *Tracker, *fakeClient) {
 	c := &fakeClient{}
-	tracker := newProgressTracker(c)
-	tracker.supportsWorkDoneProgress = true
+	tracker := NewTracker(c)
+	tracker.SetSupportsWorkDoneProgress(true)
 	return context.Background(), tracker, c
 }
 
@@ -113,7 +113,7 @@
 			ctx, cancel := context.WithCancel(ctx)
 			defer cancel()
 			tracker.supportsWorkDoneProgress = test.supported
-			work := tracker.start(ctx, "work", "message", test.token, nil)
+			work := tracker.Start(ctx, "work", "message", test.token, nil)
 			client.mu.Lock()
 			gotCreated, gotBegun := client.created, client.begun
 			client.mu.Unlock()
@@ -124,14 +124,14 @@
 				t.Errorf("got %d work begun, want %d", gotBegun, test.wantBegun)
 			}
 			// Ignore errors: this is just testing the reporting behavior.
-			work.report("report", 50)
+			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")
+			work.End("done")
 			client.mu.Lock()
 			gotEnded, gotMessages := client.ended, client.messages
 			client.mu.Unlock()
@@ -150,8 +150,8 @@
 		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 {
+		work := tracker.Start(ctx, "work", "message", token, cancel)
+		if err := tracker.Cancel(ctx, work.Token()); err != nil {
 			t.Fatal(err)
 		}
 		if !canceled {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 99786fe..16e050b 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -11,6 +11,7 @@
 	"sync"
 
 	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp/progress"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
@@ -22,6 +23,8 @@
 // NewServer creates an LSP server and binds it to handle incoming client
 // messages on on the supplied stream.
 func NewServer(session source.Session, client protocol.ClientCloser) *Server {
+	tracker := progress.NewTracker(client)
+	session.SetProgressTracker(tracker)
 	return &Server{
 		diagnostics:           map[span.URI]*fileReports{},
 		gcOptimizationDetails: make(map[string]struct{}),
@@ -30,7 +33,7 @@
 		session:               session,
 		client:                client,
 		diagnosticsSema:       make(chan struct{}, concurrentAnalyses),
-		progress:              newProgressTracker(client),
+		progress:              tracker,
 		debouncer:             newDebouncer(),
 	}
 }
@@ -99,7 +102,7 @@
 	// expensive.
 	diagnosticsSema chan struct{}
 
-	progress *progressTracker
+	progress *progress.Tracker
 
 	// debouncer is used for debouncing diagnostics.
 	debouncer *debouncer
@@ -107,11 +110,11 @@
 	// When the workspace fails to load, we show its status through a progress
 	// report with an error message.
 	criticalErrorStatusMu sync.Mutex
-	criticalErrorStatus   *workDone
+	criticalErrorStatus   *progress.WorkDone
 }
 
 func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {
-	return s.progress.cancel(ctx, params.Token)
+	return s.progress.Cancel(ctx, params.Token)
 }
 
 func (s *Server) nonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 3a6794c..748bdb1 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -20,6 +20,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/imports"
+	"golang.org/x/tools/internal/lsp/progress"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/span"
 	errors "golang.org/x/xerrors"
@@ -349,6 +350,9 @@
 	// known by the view. For views within a module, this is the module root,
 	// any directory in the module root, and any replace targets.
 	FileWatchingGlobPatterns(ctx context.Context) map[string]struct{}
+
+	// SetProgressTracker sets the progress tracker for the session.
+	SetProgressTracker(tracker *progress.Tracker)
 }
 
 // Overlay is the type for a file held in memory on a session.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 27b53b8..429f70e 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -207,11 +207,11 @@
 	// modification.
 	var diagnosticWG sync.WaitGroup
 	if s.session.Options().VerboseWorkDoneProgress {
-		work := s.progress.start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
+		work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
 		defer func() {
 			go func() {
 				diagnosticWG.Wait()
-				work.end("Done.")
+				work.End("Done.")
 			}()
 		}()
 	}