Files
kjol/tools/tsgo/internal/lsp/progress.go
2026-07-09 16:50:43 -04:00

218 lines
6.3 KiB
Go

package lsp
import (
"fmt"
"time"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
)
type progressEvent struct {
message *diagnostics.Message
args []any
finish bool
}
// progressReporter abstracts the LSP transport operations needed by
// projectLoadingProgress so the progress logic can be tested without a
// full Server instance.
type progressReporter interface {
// done returns a channel that is closed when the server is shutting down.
done() <-chan struct{}
// localize converts a diagnostic message to a display string.
localize(msg *diagnostics.Message, args ...any) string
// createWorkDoneProgress asks the client to create a progress token.
createWorkDoneProgress(token string)
// sendProgress sends a $/progress notification.
sendProgress(token string, value lsproto.WorkDoneProgressBeginOrReportOrEnd)
}
// serverProgressReporter adapts *Server to the progressReporter interface.
type serverProgressReporter struct {
server *Server
}
func (r *serverProgressReporter) done() <-chan struct{} {
return r.server.backgroundCtx.Done()
}
func (r *serverProgressReporter) localize(msg *diagnostics.Message, args ...any) string {
return msg.Localize(r.server.locale, args...)
}
func (r *serverProgressReporter) createWorkDoneProgress(token string) {
_ = sendClientRequestFireAndForget(r.server, lsproto.WindowWorkDoneProgressCreateInfo, &lsproto.WorkDoneProgressCreateParams{
Token: lsproto.IntegerOrString{String: &token},
})
}
func (r *serverProgressReporter) sendProgress(token string, value lsproto.WorkDoneProgressBeginOrReportOrEnd) {
_ = sendNotification(r.server, lsproto.ProgressInfo, &lsproto.ProgressParams{
Token: lsproto.IntegerOrString{String: &token},
Value: value,
})
}
// projectLoadingProgress manages LSP WorkDoneProgress indicators for
// long-running operations. A single persistent goroutine processes
// start/finish events, maintains a ref-counted map of active operations,
// and sends progress messages in order.
//
// To avoid flickering on fast operations, the indicator is not shown
// until progressDelay has elapsed since the first start event. If all
// operations complete before then, no progress UI is displayed.
//
// start/finish may block if the internal buffer (64 events) is full,
// but will bail out if the server's background context is cancelled.
type projectLoadingProgress struct {
reporter progressReporter
ch chan progressEvent
delay time.Duration // time to wait before showing progress UI
}
func newProjectLoadingProgress(server *Server, delay time.Duration) *projectLoadingProgress {
return newProjectLoadingProgressFromReporter(&serverProgressReporter{server: server}, delay)
}
func newProjectLoadingProgressFromReporter(reporter progressReporter, delay time.Duration) *projectLoadingProgress {
p := &projectLoadingProgress{
reporter: reporter,
ch: make(chan progressEvent, 64),
delay: delay,
}
go p.run()
return p
}
func (p *projectLoadingProgress) start(message *diagnostics.Message, args ...any) {
select {
case p.ch <- progressEvent{message: message, args: args}:
// Sent successfully.
case <-p.reporter.done():
// Server shutting down; drop the event.
}
}
func (p *projectLoadingProgress) finish(message *diagnostics.Message, args ...any) {
select {
case p.ch <- progressEvent{message: message, args: args, finish: true}:
// Sent successfully.
case <-p.reporter.done():
// Server shutting down; drop the event.
}
}
// run is the persistent goroutine that processes all progress events.
// It owns all mutable state: no external synchronization needed.
func (p *projectLoadingProgress) run() {
var (
loading collections.OrderedMap[string, int]
token string // current token; empty if no progress active
tokenID int
begun bool // whether "begin" has been sent for the current token
)
var delay *time.Timer
delayC := func() <-chan time.Time {
if delay == nil {
return nil
}
return delay.C
}
stopDelay := func() {
if delay != nil {
delay.Stop()
delay = nil
}
}
delayFired := false // true after the delay timer fires
for {
select {
case ev := <-p.ch:
text := p.reporter.localize(ev.message, ev.args...)
if !ev.finish {
count := loading.GetOrZero(text)
loading.Set(text, count+1)
if token == "" {
tokenID++
token = fmt.Sprintf("tsgo-loading-%d", tokenID)
begun = false
if p.delay <= 0 {
delayFired = true
p.reporter.createWorkDoneProgress(token)
} else {
delayFired = false
delay = time.NewTimer(p.delay)
}
}
if delayFired {
begun = p.beginOrReport(token, text, begun)
}
} else {
count := loading.GetOrZero(text)
if count <= 1 {
loading.Delete(text)
} else {
loading.Set(text, count-1)
}
if token == "" {
continue
}
if loading.Size() == 0 {
if begun {
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
End: &lsproto.WorkDoneProgressEnd{},
})
}
stopDelay()
token = ""
} else if delayFired {
first := core.FirstOrNilSeq(loading.Keys())
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Report: &lsproto.WorkDoneProgressReport{
Message: &first,
},
})
}
}
case <-delayC():
delayFired = true
if token != "" && loading.Size() > 0 {
p.reporter.createWorkDoneProgress(token)
first := core.FirstOrNilSeq(loading.Keys())
begun = p.beginOrReport(token, first, begun)
}
case <-p.reporter.done():
stopDelay()
return
}
}
}
// beginOrReport sends WorkDoneProgressBegin if not yet begun, otherwise
// sends WorkDoneProgressReport. Returns true to indicate begun state.
func (p *projectLoadingProgress) beginOrReport(token, text string, begun bool) bool {
if !begun {
title := p.reporter.localize(diagnostics.Loading)
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Begin: &lsproto.WorkDoneProgressBegin{
Title: title,
Message: &text,
},
})
} else {
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Report: &lsproto.WorkDoneProgressReport{
Message: &text,
},
})
}
return true
}