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
54 changes: 54 additions & 0 deletions internal/lsp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type Client struct {
diagnostics map[protocol.DocumentUri][]protocol.Diagnostic
diagnosticsMu sync.RWMutex

// Per-URI waiters fired when textDocument/publishDiagnostics arrives.
// Used by WaitForDiagnostics to replace the old 3s sleep.
diagnosticWaiters map[protocol.DocumentUri][]chan struct{}
diagnosticWaitersMu sync.Mutex

// Files are currently opened by the LSP
openFiles map[string]*OpenFileInfo
openFilesMu sync.RWMutex
Expand Down Expand Up @@ -75,6 +80,7 @@ func NewClient(command string, args ...string) (*Client, error) {
notificationHandlers: make(map[string]NotificationHandler),
serverRequestHandlers: make(map[string]ServerRequestHandler),
diagnostics: make(map[protocol.DocumentUri][]protocol.Diagnostic),
diagnosticWaiters: make(map[protocol.DocumentUri][]chan struct{}),
openFiles: make(map[string]*OpenFileInfo),
}

Expand Down Expand Up @@ -281,6 +287,54 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
return nil
}

// WaitForDiagnostics blocks until the LSP publishes diagnostics for uri or
// timeout expires, whichever comes first. After the first publish, sleeps
// settle to coalesce follow-up updates (e.g. project-wide rescans). The
// caller reads from the diagnostic cache after this returns.
func (c *Client) WaitForDiagnostics(ctx context.Context, uri protocol.DocumentUri, timeout, settle time.Duration) {
ch := make(chan struct{}, 1)

c.diagnosticWaitersMu.Lock()
c.diagnosticWaiters[uri] = append(c.diagnosticWaiters[uri], ch)
c.diagnosticWaitersMu.Unlock()

defer func() {
c.diagnosticWaitersMu.Lock()
waiters := c.diagnosticWaiters[uri]
for i, w := range waiters {
if w == ch {
c.diagnosticWaiters[uri] = append(waiters[:i], waiters[i+1:]...)
break
}
}
if len(c.diagnosticWaiters[uri]) == 0 {
delete(c.diagnosticWaiters, uri)
}
c.diagnosticWaitersMu.Unlock()
}()

// If diagnostics already cached (file was opened earlier), fire immediately.
c.diagnosticsMu.RLock()
_, hasExisting := c.diagnostics[uri]
c.diagnosticsMu.RUnlock()
if hasExisting {
return
}

select {
case <-ch:
// First publish arrived. Sleep settle to absorb redundant follow-ups
// (Civet republishes after a ~100ms project-wide propagation pass).
select {
case <-time.After(settle):
case <-ctx.Done():
}
case <-time.After(timeout):
lspLogger.Debug("WaitForDiagnostics timed out for %s after %s", uri, timeout)
case <-ctx.Done():
}
}

type OpenFileInfo struct {
Version int32
URI protocol.DocumentUri
Expand Down
11 changes: 11 additions & 0 deletions internal/lsp/server-request-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,16 @@ func HandleDiagnostics(client *Client, params json.RawMessage) {
client.diagnostics[diagParams.URI] = diagParams.Diagnostics
client.diagnosticsMu.Unlock()

// Signal any WaitForDiagnostics callers blocked on this URI.
client.diagnosticWaitersMu.Lock()
waiters := client.diagnosticWaiters[diagParams.URI]
client.diagnosticWaitersMu.Unlock()
for _, w := range waiters {
select {
case w <- struct{}{}:
default:
}
}

lspLogger.Info("Received diagnostics for %s: %d items", diagParams.URI, len(diagParams.Diagnostics))
}
19 changes: 11 additions & 8 deletions internal/tools/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,23 @@ func GetDiagnosticsForFile(ctx context.Context, client *lsp.Client, filePath str
return "", fmt.Errorf("could not open file: %v", err)
}

// Wait for diagnostics
// TODO: wait for notification
time.Sleep(time.Second * 3)

// Convert the file path to URI format
uri := protocol.DocumentUri("file://" + filePath)

// Request fresh diagnostics
// Wait for the LSP to publish diagnostics for this URI (push mode).
// 3s upper bound matches the previous hardcoded sleep; settle window
// (150ms) absorbs follow-up republishes some servers send after a
// project-wide rescan.
client.WaitForDiagnostics(ctx, uri, 3*time.Second, 150*time.Millisecond)

// Also attempt pull-mode (textDocument/diagnostic) for servers that
// support it. Push-only servers (e.g. Civet) return -32601; that's
// fine since the cache is already populated from publishDiagnostics.
diagParams := protocol.DocumentDiagnosticParams{
TextDocument: protocol.TextDocumentIdentifier{URI: uri},
}
_, err = client.Diagnostic(ctx, diagParams)
if err != nil {
toolsLogger.Error("Failed to get diagnostics: %v", err)
if _, err = client.Diagnostic(ctx, diagParams); err != nil {
toolsLogger.Debug("Pull-mode diagnostic unavailable (server likely push-only): %v", err)
}

// Get diagnostics from the cache
Expand Down