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