Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/tsgo/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"os/signal"
"syscall"
"time"

"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/core"
Expand Down Expand Up @@ -56,6 +57,7 @@ func runLSP(args []string) int {
cmd.Dir = cwd
return cmd.Output()
},
ProgressDelay: 250 * time.Millisecond,
})

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
Expand Down
159 changes: 120 additions & 39 deletions internal/lsp/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lsp

import (
"fmt"
"time"

"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
Expand All @@ -15,22 +16,72 @@ type progressEvent struct {
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) {
_, _ = sendClientRequest(r.server.backgroundCtx, 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 {
server *Server
ch chan progressEvent
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 newProjectLoadingProgress(server *Server) *projectLoadingProgress {
func newProjectLoadingProgressFromReporter(reporter progressReporter, delay time.Duration) *projectLoadingProgress {
p := &projectLoadingProgress{
server: server,
ch: make(chan progressEvent, 64),
reporter: reporter,
ch: make(chan progressEvent, 64),
delay: delay,
}
go p.run()
return p
Expand All @@ -40,7 +91,7 @@ func (p *projectLoadingProgress) start(message *diagnostics.Message, args ...any
select {
case p.ch <- progressEvent{message: message, args: args}:
// Sent successfully.
case <-p.server.backgroundCtx.Done():
case <-p.reporter.done():
// Server shutting down; drop the event.
}
}
Expand All @@ -49,7 +100,7 @@ func (p *projectLoadingProgress) finish(message *diagnostics.Message, args ...an
select {
case p.ch <- progressEvent{message: message, args: args, finish: true}:
// Sent successfully.
case <-p.server.backgroundCtx.Done():
case <-p.reporter.done():
// Server shutting down; drop the event.
}
}
Expand All @@ -62,38 +113,44 @@ func (p *projectLoadingProgress) run() {
token string // current token; empty if no progress active
tokenID int
begun bool // whether "begin" has been sent for the current token
title = diagnostics.Loading.Localize(p.server.locale)
)

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 := ev.message.Localize(p.server.locale, ev.args...)
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
_, _ = sendClientRequest(p.server.backgroundCtx, p.server, lsproto.WindowWorkDoneProgressCreateInfo, &lsproto.WorkDoneProgressCreateParams{
Token: lsproto.IntegerOrString{String: &token},
})
if p.delay <= 0 {
delayFired = true
p.reporter.createWorkDoneProgress(token)
} else {
delayFired = false
delay = time.NewTimer(p.delay)
}
}
if !begun {
begun = true
p.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Begin: &lsproto.WorkDoneProgressBegin{
Title: title,
Message: &text,
},
})
} else {
p.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Report: &lsproto.WorkDoneProgressReport{
Message: &text,
},
})
if delayFired {
begun = p.beginOrReport(token, text, begun)
}
} else {
count := loading.GetOrZero(text)
Expand All @@ -106,31 +163,55 @@ func (p *projectLoadingProgress) run() {
continue
}
if loading.Size() == 0 {
p.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
End: &lsproto.WorkDoneProgressEnd{},
})
if begun {
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
End: &lsproto.WorkDoneProgressEnd{},
})
}
stopDelay()
token = ""
} else {
} else if delayFired {
first := core.FirstOrNilSeq(loading.Keys())
p.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
p.reporter.sendProgress(token, lsproto.WorkDoneProgressBeginOrReportOrEnd{
Report: &lsproto.WorkDoneProgressReport{
Message: &first,
},
})
}
}

case <-p.server.backgroundCtx.Done():
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
}
}
}

// sendProgress sends a $/progress notification with a snapshot of the token
// string, so deferred serialization in the write loop won't see a mutated value.
func (p *projectLoadingProgress) sendProgress(token string, value lsproto.WorkDoneProgressBeginOrReportOrEnd) {
_ = sendNotification(p.server, lsproto.ProgressInfo, &lsproto.ProgressParams{
Token: lsproto.IntegerOrString{String: &token},
Value: value,
})
// 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
}
Loading
Loading