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