@@ -530,23 +530,116 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
533−// WaitForDiagnostics waits until diagnostics change or the timeout is reached.
533+// NotifyWorkspaceChange sends a workspace-level file change notification to
534+// trigger re-analysis of all files. This is useful when the overall project
535+// state may have changed (e.g., after a project-wide refactoring) and
536+// diagnostics for files not currently being edited may be stale.
534−func (c *Client) WaitForDiagnostics(ctx context.Context, d time.Duration) {
537+func (c *Client) NotifyWorkspaceChange(ctx context.Context) error {
541+ return c.client.NotifyDidChangeWatchedFiles(ctx, []protocol.FileEvent{
542+ {URI: protocol.DocumentURI(protocol.URIFromPath(c.cwd)), Type: protocol.Changed},
546+// RefreshOpenFiles re-notifies the LSP server about all currently open files,
547+// which triggers re-analysis and fresh diagnostics for the entire project.
548+func (c *Client) RefreshOpenFiles(ctx context.Context) {
535 if c == nil {
549 if c == nil {
552+ for uri, info := range c.openFiles.Seq2() {
553+ path, err := protocol.DocumentURI(uri).Path()
555+ slog.Warn("Failed to convert URI to path", "uri", uri, "error", err)
558+ content, err := os.ReadFile(path)
560+ slog.Warn("Failed to read file for refresh", "path", path, "error", err)
564+ changes := []protocol.TextDocumentContentChangeEvent{
566+ Value: protocol.TextDocumentContentChangeWholeDocument{
567+ Text: string(content),
571+ if err := c.client.NotifyDidChangeTextDocument(ctx, uri, int(info.Version), changes); err != nil {
572+ slog.Warn("Failed to notify file change", "uri", uri, "error", err)
577+// WaitForDiagnostics waits until diagnostics stop changing for a settling
578+// period, indicating the LSP server has finished processing. If no
579+// diagnostics change within firstChangeDuration, it returns early since the
580+// server likely isn't going to republish.
581+func (c *Client) WaitForDiagnostics(ctx context.Context, timeout time.Duration) {
587+ firstChangeDuration = 1 * time.Second
588+ settleDuration = 300 * time.Millisecond
591+ deadline := time.NewTimer(timeout)
592+ defer deadline.Stop()
593+ firstChangeTimer := time.NewTimer(min(timeout, firstChangeDuration))
594+ defer firstChangeTimer.Stop()
595+ previousVersion := c.diagnostics.Version()
538− ticker := time.NewTicker(200 * time.Millisecond)
596+ ticker := time.NewTicker(100 * time.Millisecond)
539 defer ticker.Stop()
597 defer ticker.Stop()
540− timeout := time.After(d)
598+
541− pv := c.diagnostics.Version()
544 case <-ctx.Done():
601 case <-ctx.Done():
546− case <-timeout:
603+ case <-deadline.C:
605+ case <-firstChangeTimer.C:
606+ // No change arrived quickly — server isn't republishing.
548 case <-ticker.C:
608 case <-ticker.C:
549− if pv != c.diagnostics.Version() {
609+ currentVersion := c.diagnostics.Version()
610+ if currentVersion != previousVersion {
611+ // Diagnostics changed — now wait for them to settle.
612+ c.waitForDiagnosticsToSettle(ctx, deadline.C, settleDuration)
619+// waitForDiagnosticsToSettle waits until diagnostics version stays the same
620+// for settleDuration, indicating the LSP server has finished publishing.
621+func (c *Client) waitForDiagnosticsToSettle(ctx context.Context, deadline <-chan time.Time, settleDuration time.Duration) {
622+ lastVersion := c.diagnostics.Version()
623+ settleTicker := time.NewTicker(50 * time.Millisecond)
624+ defer settleTicker.Stop()
626+ // Track how long the version has been stable.
627+ stableStart := time.Now()
635+ case <-settleTicker.C:
636+ currentVersion := c.diagnostics.Version()
637+ if currentVersion != lastVersion {
638+ // New change detected — reset the stable timer.
639+ lastVersion = currentVersion
640+ stableStart = time.Now()
641+ } else if time.Since(stableStart) >= settleDuration {
642+ // Diagnostics have been stable for the settle duration.