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 }