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