golang.org/x/tools/gopls@v0.15.3/internal/progress/progress.go (about)

     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.
     4  
     5  // The progress package defines utilities for reporting the progress
     6  // of long-running operations using features of the LSP client
     7  // interface such as Progress and ShowMessage.
     8  package progress
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"io"
    14  	"math/rand"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  
    19  	"golang.org/x/tools/gopls/internal/protocol"
    20  	"golang.org/x/tools/internal/event"
    21  	"golang.org/x/tools/internal/event/tag"
    22  	"golang.org/x/tools/internal/xcontext"
    23  )
    24  
    25  // NewTracker returns a new Tracker that reports progress to the
    26  // specified client.
    27  func NewTracker(client protocol.Client) *Tracker {
    28  	return &Tracker{
    29  		client:     client,
    30  		inProgress: make(map[protocol.ProgressToken]*WorkDone),
    31  	}
    32  }
    33  
    34  // A Tracker reports the progress of a long-running operation to an LSP client.
    35  type Tracker struct {
    36  	client                   protocol.Client
    37  	supportsWorkDoneProgress bool
    38  
    39  	mu         sync.Mutex
    40  	inProgress map[protocol.ProgressToken]*WorkDone
    41  }
    42  
    43  // SetSupportsWorkDoneProgress sets whether the client supports "work done"
    44  // progress reporting. It must be set before using the tracker.
    45  //
    46  // TODO(rfindley): fix this broken initialization pattern.
    47  // Also: do we actually need the fall-back progress behavior using ShowMessage?
    48  // Surely ShowMessage notifications are too noisy to be worthwhile.
    49  func (t *Tracker) SetSupportsWorkDoneProgress(b bool) {
    50  	t.supportsWorkDoneProgress = b
    51  }
    52  
    53  // SupportsWorkDoneProgress reports whether the tracker supports work done
    54  // progress reporting.
    55  func (t *Tracker) SupportsWorkDoneProgress() bool {
    56  	return t.supportsWorkDoneProgress
    57  }
    58  
    59  // Start notifies the client of work being done on the server. It uses either
    60  // ShowMessage RPCs or $/progress messages, depending on the capabilities of
    61  // the client.  The returned WorkDone handle may be used to report incremental
    62  // progress, and to report work completion. In particular, it is an error to
    63  // call start and not call end(...) on the returned WorkDone handle.
    64  //
    65  // If token is empty, a token will be randomly generated.
    66  //
    67  // The progress item is considered cancellable if the given cancel func is
    68  // non-nil. In this case, cancel is called when the work done
    69  //
    70  // Example:
    71  //
    72  //	func Generate(ctx) (err error) {
    73  //	  ctx, cancel := context.WithCancel(ctx)
    74  //	  defer cancel()
    75  //	  work := s.progress.start(ctx, "generate", "running go generate", cancel)
    76  //	  defer func() {
    77  //	    if err != nil {
    78  //	      work.end(ctx, fmt.Sprintf("generate failed: %v", err))
    79  //	    } else {
    80  //	      work.end(ctx, "done")
    81  //	    }
    82  //	  }()
    83  //	  // Do the work...
    84  //	}
    85  func (t *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone {
    86  	ctx = xcontext.Detach(ctx) // progress messages should not be cancelled
    87  	wd := &WorkDone{
    88  		client: t.client,
    89  		token:  token,
    90  		cancel: cancel,
    91  	}
    92  	if !t.supportsWorkDoneProgress {
    93  		// Previous iterations of this fallback attempted to retain cancellation
    94  		// support by using ShowMessageCommand with a 'Cancel' button, but this is
    95  		// not ideal as the 'Cancel' dialog stays open even after the command
    96  		// completes.
    97  		//
    98  		// Just show a simple message. Clients can implement workDone progress
    99  		// reporting to get cancellation support.
   100  		if err := wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{
   101  			Type:    protocol.Log,
   102  			Message: message,
   103  		}); err != nil {
   104  			event.Error(ctx, "showing start message for "+title, err)
   105  		}
   106  		return wd
   107  	}
   108  	if wd.token == nil {
   109  		token = strconv.FormatInt(rand.Int63(), 10)
   110  		err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{
   111  			Token: token,
   112  		})
   113  		if err != nil {
   114  			wd.err = err
   115  			event.Error(ctx, "starting work for "+title, err)
   116  			return wd
   117  		}
   118  		wd.token = token
   119  	}
   120  	// At this point we have a token that the client knows about. Store the token
   121  	// before starting work.
   122  	t.mu.Lock()
   123  	t.inProgress[wd.token] = wd
   124  	t.mu.Unlock()
   125  	wd.cleanup = func() {
   126  		t.mu.Lock()
   127  		delete(t.inProgress, token)
   128  		t.mu.Unlock()
   129  	}
   130  	err := wd.client.Progress(ctx, &protocol.ProgressParams{
   131  		Token: wd.token,
   132  		Value: &protocol.WorkDoneProgressBegin{
   133  			Kind:        "begin",
   134  			Cancellable: wd.cancel != nil,
   135  			Message:     message,
   136  			Title:       title,
   137  		},
   138  	})
   139  	if err != nil {
   140  		event.Error(ctx, "progress begin", err)
   141  	}
   142  	return wd
   143  }
   144  
   145  func (t *Tracker) Cancel(token protocol.ProgressToken) error {
   146  	t.mu.Lock()
   147  	defer t.mu.Unlock()
   148  	wd, ok := t.inProgress[token]
   149  	if !ok {
   150  		return fmt.Errorf("token %q not found in progress", token)
   151  	}
   152  	if wd.cancel == nil {
   153  		return fmt.Errorf("work %q is not cancellable", token)
   154  	}
   155  	wd.doCancel()
   156  	return nil
   157  }
   158  
   159  // WorkDone represents a unit of work that is reported to the client via the
   160  // progress API.
   161  type WorkDone struct {
   162  	client protocol.Client
   163  	// If token is nil, this workDone object uses the ShowMessage API, rather
   164  	// than $/progress.
   165  	token protocol.ProgressToken
   166  	// err is set if progress reporting is broken for some reason (for example,
   167  	// if there was an initial error creating a token).
   168  	err error
   169  
   170  	cancelMu  sync.Mutex
   171  	cancelled bool
   172  	cancel    func()
   173  
   174  	cleanup func()
   175  }
   176  
   177  func (wd *WorkDone) Token() protocol.ProgressToken {
   178  	return wd.token
   179  }
   180  
   181  func (wd *WorkDone) doCancel() {
   182  	wd.cancelMu.Lock()
   183  	defer wd.cancelMu.Unlock()
   184  	if !wd.cancelled {
   185  		wd.cancel()
   186  	}
   187  }
   188  
   189  // Report reports an update on WorkDone report back to the client.
   190  func (wd *WorkDone) Report(ctx context.Context, message string, percentage float64) {
   191  	ctx = xcontext.Detach(ctx) // progress messages should not be cancelled
   192  	if wd == nil {
   193  		return
   194  	}
   195  	wd.cancelMu.Lock()
   196  	cancelled := wd.cancelled
   197  	wd.cancelMu.Unlock()
   198  	if cancelled {
   199  		return
   200  	}
   201  	if wd.err != nil || wd.token == nil {
   202  		// Not using the workDone API, so we do nothing. It would be far too spammy
   203  		// to send incremental messages.
   204  		return
   205  	}
   206  	message = strings.TrimSuffix(message, "\n")
   207  	err := wd.client.Progress(ctx, &protocol.ProgressParams{
   208  		Token: wd.token,
   209  		Value: &protocol.WorkDoneProgressReport{
   210  			Kind: "report",
   211  			// Note that in the LSP spec, the value of Cancellable may be changed to
   212  			// control whether the cancel button in the UI is enabled. Since we don't
   213  			// yet use this feature, the value is kept constant here.
   214  			Cancellable: wd.cancel != nil,
   215  			Message:     message,
   216  			Percentage:  uint32(percentage),
   217  		},
   218  	})
   219  	if err != nil {
   220  		event.Error(ctx, "reporting progress", err)
   221  	}
   222  }
   223  
   224  // End reports a workdone completion back to the client.
   225  func (wd *WorkDone) End(ctx context.Context, message string) {
   226  	ctx = xcontext.Detach(ctx) // progress messages should not be cancelled
   227  	if wd == nil {
   228  		return
   229  	}
   230  	var err error
   231  	switch {
   232  	case wd.err != nil:
   233  		// There is a prior error.
   234  	case wd.token == nil:
   235  		// We're falling back to message-based reporting.
   236  		err = wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{
   237  			Type:    protocol.Info,
   238  			Message: message,
   239  		})
   240  	default:
   241  		err = wd.client.Progress(ctx, &protocol.ProgressParams{
   242  			Token: wd.token,
   243  			Value: &protocol.WorkDoneProgressEnd{
   244  				Kind:    "end",
   245  				Message: message,
   246  			},
   247  		})
   248  	}
   249  	if err != nil {
   250  		event.Error(ctx, "ending work", err)
   251  	}
   252  	if wd.cleanup != nil {
   253  		wd.cleanup()
   254  	}
   255  }
   256  
   257  // NewEventWriter returns an [io.Writer] that calls the context's
   258  // event printer for each data payload, wrapping it with the
   259  // operation=generate tag to distinguish its logs from others.
   260  func NewEventWriter(ctx context.Context, operation string) io.Writer {
   261  	return &eventWriter{ctx: ctx, operation: operation}
   262  }
   263  
   264  type eventWriter struct {
   265  	ctx       context.Context
   266  	operation string
   267  }
   268  
   269  func (ew *eventWriter) Write(p []byte) (n int, err error) {
   270  	event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation))
   271  	return len(p), nil
   272  }
   273  
   274  // NewWorkDoneWriter wraps a WorkDone handle to provide a Writer interface,
   275  // so that workDone reporting can more easily be hooked into commands.
   276  func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) io.Writer {
   277  	return &workDoneWriter{ctx: ctx, wd: wd}
   278  }
   279  
   280  // workDoneWriter wraps a workDone handle to provide a Writer interface,
   281  // so that workDone reporting can more easily be hooked into commands.
   282  type workDoneWriter struct {
   283  	// In order to implement the io.Writer interface, we must close over ctx.
   284  	ctx context.Context
   285  	wd  *WorkDone
   286  }
   287  
   288  func (wdw *workDoneWriter) Write(p []byte) (n int, err error) {
   289  	wdw.wd.Report(wdw.ctx, string(p), 0)
   290  	// Don't fail just because of a failure to report progress.
   291  	return len(p), nil
   292  }