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