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
5package lsp
6
7import (
8	"context"
9	"errors"
10	"io"
11	"math/rand"
12	"strconv"
13
14	"golang.org/x/tools/internal/event"
15	"golang.org/x/tools/internal/lsp/debug/tag"
16	"golang.org/x/tools/internal/lsp/protocol"
17)
18
19// WorkDone represents a unit of work that is reported to the client via the
20// progress API.
21type WorkDone struct {
22	client   protocol.Client
23	startErr error
24	token    string
25	cancel   func()
26	cleanup  func()
27}
28
29// StartWork creates a unique token and issues a $/progress notification to
30// begin a unit of work on the server. The returned WorkDone handle may be used
31// to report incremental progress, and to report work completion. In
32// particular, it is an error to call StartWork and not call End(...) on the
33// returned WorkDone handle.
34//
35// The progress item is considered cancellable if the given cancel func is
36// non-nil.
37//
38// Example:
39//  func Generate(ctx) (err error) {
40//    ctx, cancel := context.WithCancel(ctx)
41//    defer cancel()
42//    work := s.StartWork(ctx, "generate", "running go generate", cancel)
43//    defer func() {
44//      if err != nil {
45//        work.End(ctx, fmt.Sprintf("generate failed: %v", err))
46//      } else {
47//        work.End(ctx, "done")
48//      }
49//    }()
50//    // Do the work...
51//  }
52//
53func (s *Server) StartWork(ctx context.Context, title, message string, cancel func()) *WorkDone {
54	wd := &WorkDone{
55		client: s.client,
56		token:  strconv.FormatInt(rand.Int63(), 10),
57		cancel: cancel,
58	}
59	if !s.supportsWorkDoneProgress {
60		wd.startErr = errors.New("workdone reporting is not supported")
61		return wd
62	}
63	err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{
64		Token: wd.token,
65	})
66	if err != nil {
67		wd.startErr = err
68		event.Error(ctx, "starting work for "+title, err)
69		return wd
70	}
71	s.addInProgress(wd)
72	wd.cleanup = func() {
73		s.removeInProgress(wd.token)
74	}
75	err = wd.client.Progress(ctx, &protocol.ProgressParams{
76		Token: wd.token,
77		Value: &protocol.WorkDoneProgressBegin{
78			Kind:        "begin",
79			Cancellable: wd.cancel != nil,
80			Message:     message,
81			Title:       title,
82		},
83	})
84	if err != nil {
85		event.Error(ctx, "generate progress begin", err)
86	}
87	return wd
88}
89
90// Progress reports an update on WorkDone progress back to the client.
91func (wd *WorkDone) Progress(ctx context.Context, message string, percentage float64) error {
92	if wd.startErr != nil {
93		return wd.startErr
94	}
95	return wd.client.Progress(ctx, &protocol.ProgressParams{
96		Token: wd.token,
97		Value: &protocol.WorkDoneProgressReport{
98			Kind: "report",
99			// Note that in the LSP spec, the value of Cancellable may be changed to
100			// control whether the cancel button in the UI is enabled. Since we don't
101			// yet use this feature, the value is kept constant here.
102			Cancellable: wd.cancel != nil,
103			Message:     message,
104			Percentage:  percentage,
105		},
106	})
107}
108
109// End reports a workdone completion back to the client.
110func (wd *WorkDone) End(ctx context.Context, message string) error {
111	if wd.startErr != nil {
112		return wd.startErr
113	}
114	err := wd.client.Progress(ctx, &protocol.ProgressParams{
115		Token: wd.token,
116		Value: protocol.WorkDoneProgressEnd{
117			Kind:    "end",
118			Message: message,
119		},
120	})
121	if wd.cleanup != nil {
122		wd.cleanup()
123	}
124	return err
125}
126
127// eventWriter writes every incoming []byte to
128// event.Print with the operation=generate tag
129// to distinguish its logs from others.
130type eventWriter struct {
131	ctx       context.Context
132	operation string
133}
134
135func (ew *eventWriter) Write(p []byte) (n int, err error) {
136	event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation))
137	return len(p), nil
138}
139
140// newProgressWriter returns an io.WriterCloser that can be used
141// to report progress on a command based on the client capabilities.
142func (s *Server) newProgressWriter(ctx context.Context, title, beginMsg, msg string, cancel func()) io.WriteCloser {
143	if s.supportsWorkDoneProgress {
144		wd := s.StartWork(ctx, title, beginMsg, cancel)
145		return &workDoneWriter{ctx, wd}
146	}
147	mw := &messageWriter{ctx, cancel, s.client}
148	mw.start(msg)
149	return mw
150}
151
152// messageWriter implements progressWriter and only tells the user that
153// a command has started through window/showMessage, but does not report
154// anything afterwards. This is because each log shows up as a separate window
155// and therefore would be obnoxious to show every incoming line. Request
156// cancellation happens synchronously through the ShowMessageRequest response.
157type messageWriter struct {
158	ctx    context.Context
159	cancel func()
160	client protocol.Client
161}
162
163func (lw *messageWriter) Write(p []byte) (n int, err error) {
164	return len(p), nil
165}
166
167func (lw *messageWriter) start(msg string) {
168	go func() {
169		const cancel = "Cancel"
170		item, err := lw.client.ShowMessageRequest(lw.ctx, &protocol.ShowMessageRequestParams{
171			Type:    protocol.Log,
172			Message: msg,
173			Actions: []protocol.MessageActionItem{{
174				Title: "Cancel",
175			}},
176		})
177		if err != nil {
178			event.Error(lw.ctx, "error sending message request", err)
179			return
180		}
181		if item != nil && item.Title == "Cancel" {
182			lw.cancel()
183		}
184	}()
185}
186
187func (lw *messageWriter) Close() error {
188	return lw.client.ShowMessage(lw.ctx, &protocol.ShowMessageParams{
189		Type:    protocol.Info,
190		Message: "go generate has finished",
191	})
192}
193
194// workDoneWriter implements progressWriter by sending $/progress notifications
195// to the client. Request cancellations happens separately through the
196// window/workDoneProgress/cancel request, in which case the given context will
197// be rendered done.
198type workDoneWriter struct {
199	ctx context.Context
200	wd  *WorkDone
201}
202
203func (wdw *workDoneWriter) Write(p []byte) (n int, err error) {
204	return len(p), wdw.wd.Progress(wdw.ctx, string(p), 0)
205}
206
207func (wdw *workDoneWriter) Close() error {
208	return wdw.wd.End(wdw.ctx, "finished")
209}
210