@@ -530,23 +530,116 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
530530 }
531531}
532532
533โ// WaitForDiagnostics waits until diagnostics change or the timeout is reached.
534โfunc (c *Client) WaitForDiagnostics(ctx context.Context, d time.Duration) {
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.
537+func (c *Client) NotifyWorkspaceChange(ctx context.Context) error {
538+ if c == nil {
539+ return nil
540+ }
541+ return c.client.NotifyDidChangeWatchedFiles(ctx, []protocol.FileEvent{
542+ {URI: protocol.DocumentURI(protocol.URIFromPath(c.cwd)), Type: protocol.Changed},
543+ })
544+}
545+
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) {
535549 if c == nil {
536550 return
537551 }
538โ ticker := time.NewTicker(200 * time.Millisecond)
552+ for uri, info := range c.openFiles.Seq2() {
553+ path, err := protocol.DocumentURI(uri).Path()
554+ if err != nil {
555+ slog.Warn("Failed to convert URI to path", "uri", uri, "error", err)
556+ continue
557+ }
558+ content, err := os.ReadFile(path)
559+ if err != nil {
560+ slog.Warn("Failed to read file for refresh", "path", path, "error", err)
561+ continue
562+ }
563+ info.Version++
564+ changes := []protocol.TextDocumentContentChangeEvent{
565+ {
566+ Value: protocol.TextDocumentContentChangeWholeDocument{
567+ Text: string(content),
568+ },
569+ },
570+ }
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)
573+ }
574+ }
575+}
576+
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) {
582+ if c == nil {
583+ return
584+ }
585+
586+ const (
587+ firstChangeDuration = 1 * time.Second
588+ settleDuration = 300 * time.Millisecond
589+ )
590+
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()
596+ ticker := time.NewTicker(100 * time.Millisecond)
539597 defer ticker.Stop()
540โ timeout := time.After(d)
541โ pv := c.diagnostics.Version()
598+
542599 for {
543600 select {
544601 case <-ctx.Done():
545602 return
546โ case <-timeout:
603+ case <-deadline.C:
604+ return
605+ case <-firstChangeTimer.C:
606+ // No change arrived quickly โ server isn't republishing.
547607 return
548608 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)
613+ return
614+ }
615+ }
616+ }
617+}
618+
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()
625+
626+ // Track how long the version has been stable.
627+ stableStart := time.Now()
628+
629+ for {
630+ select {
631+ case <-ctx.Done():
632+ return
633+ case <-deadline:
634+ return
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.
550643 return
551644 }
552645 }