Skip to main content

fresh/app/
async_messages.rs

1//! Async message handlers for the Editor
2//!
3//! This module contains handlers for AsyncMessage variants, grouped by domain:
4//! - LSP diagnostics (push and pull models)
5//! - LSP feature responses (inlay hints, progress, status)
6//! - File system events
7//! - File explorer events
8//! - Plugin events
9
10use crate::model::buffer::Buffer;
11use crate::model::event::BufferId;
12use crate::services::async_bridge::{
13    LspMessageType, LspProgressValue, LspSemanticTokensResponse, LspServerStatus,
14};
15use crate::state::{SemanticTokenSpan, SemanticTokenStore};
16use crate::view::file_tree::{FileTreeView, NodeId};
17use lsp_types::{
18    Diagnostic, FoldingRange, InlayHint, SemanticToken, SemanticTokensEdit,
19    SemanticTokensFullDeltaResult, SemanticTokensLegend, SemanticTokensRangeResult,
20    SemanticTokensResult,
21};
22use rust_i18n::t;
23use serde_json::Value;
24use std::path::PathBuf;
25use std::time::{Duration, Instant};
26
27use super::types::{LspMessageEntry, LspProgressInfo};
28use super::Editor;
29
30// =============================================================================
31// Shared Helpers
32// =============================================================================
33
34impl Editor {
35    /// Find a buffer by its LSP URI
36    ///
37    /// This is a common pattern used by diagnostics, inlay hints, and other LSP handlers
38    pub(super) fn find_buffer_by_uri(&self, uri: &str) -> Option<BufferId> {
39        // The incoming URI string came over the LSP wire (e.g. a
40        // `publishDiagnostics` notification), so it's already in the
41        // server's coordinate space. `BufferMetadata.file_uri` is also
42        // wire-side ([`LspUri`]), so a string comparison is the right
43        // primitive here — both sides are translated identically and
44        // we never accidentally compare a host URI to a wire URI.
45        self.active_window()
46            .buffer_metadata
47            .iter()
48            .find(|(_, m)| m.file_uri().map(|u| u.as_str() == uri).unwrap_or(false))
49            .map(|(buffer_id, _)| *buffer_id)
50    }
51
52    /// Collect `(buffer_id, uri)` pairs for every open buffer whose stored
53    /// language matches `language`.
54    ///
55    /// This is the single correct way to enumerate buffers for sending a
56    /// per-URI LSP request (pull diagnostics, inlay hints, semantic tokens,
57    /// folding ranges, …) to a language-scoped server. Without the language
58    /// filter, a server configured only for e.g. "rust" ends up receiving
59    /// requests for every open URI regardless of type, and a responsible
60    /// server rejects unknown URIs with `file not found (code -32603)` —
61    /// polluting logs and wasting a round-trip per unrelated buffer.
62    ///
63    /// Callers that need richer per-buffer info (line counts, content, file
64    /// paths) can still iterate themselves, but should use the same
65    /// `state.language == language` predicate this helper encodes.
66    pub(crate) fn buffers_for_language(
67        &self,
68        language: &str,
69    ) -> Vec<(BufferId, crate::app::types::LspUri)> {
70        self.windows
71            .get(&self.active_window)
72            .map(|w| &w.buffers)
73            .expect("active window present")
74            .iter()
75            .filter_map(|(buffer_id, state)| {
76                if state.language != language {
77                    return None;
78                }
79                self.active_window()
80                    .buffer_metadata
81                    .get(buffer_id)
82                    .and_then(|m| m.file_uri().cloned())
83                    .map(|uri| (*buffer_id, uri))
84            })
85            .collect()
86    }
87
88    /// Apply diagnostics to a buffer identified by URI.
89    /// Returns `(buffer_id, actually_updated)` if buffer was found, None otherwise.
90    /// `actually_updated` is false when the DIAG CACHE determined no overlay changes were needed.
91    fn apply_diagnostics_to_buffer(
92        &mut self,
93        uri: &str,
94        diagnostics: &[Diagnostic],
95    ) -> Option<(BufferId, bool)> {
96        let buffer_id = self.find_buffer_by_uri(uri)?;
97        let state = self
98            .windows
99            .get_mut(&self.active_window)
100            .map(|w| &mut w.buffers)
101            .expect("active window present")
102            .get_mut(&buffer_id)?;
103        let updated = crate::services::lsp::diagnostics::apply_diagnostics_to_state_cached(
104            state,
105            diagnostics,
106            &*self.theme.read().unwrap(),
107        );
108        Some((buffer_id, updated))
109    }
110}
111
112// =============================================================================
113// LSP Diagnostics Handlers
114// =============================================================================
115
116impl Editor {
117    /// Merge push + pull diagnostics for a URI and apply the combined set
118    fn merge_and_apply_diagnostics(&mut self, uri: &str) {
119        // Merge diagnostics from all servers (push model) and pull model
120        let mut merged = Vec::new();
121        if let Some(server_map) = self.active_window_mut().stored_push_diagnostics.get(uri) {
122            for diagnostics in server_map.values() {
123                merged.extend(diagnostics.iter().cloned());
124            }
125        }
126        if let Some(pull) = self.active_window_mut().stored_pull_diagnostics.get(uri) {
127            merged.extend(pull.iter().cloned());
128        }
129
130        // Update the merged view
131        if merged.is_empty() {
132            self.stored_diagnostics_mut().remove(uri);
133        } else {
134            self.stored_diagnostics_mut()
135                .insert(uri.to_string(), merged.clone());
136        }
137
138        if let Some((buffer_id, updated)) = self.apply_diagnostics_to_buffer(uri, &merged) {
139            if updated {
140                tracing::info!(
141                    "Applied {} diagnostics to buffer {:?} (overlays updated)",
142                    merged.len(),
143                    buffer_id
144                );
145            } else {
146                tracing::debug!(
147                    "Diagnostics unchanged for buffer {:?} ({} diagnostics, cache hit)",
148                    buffer_id,
149                    merged.len()
150                );
151            }
152        } else {
153            tracing::debug!("No buffer found for diagnostic URI: {}", uri);
154        }
155
156        // Emit diagnostics_updated hook for plugins
157        let count = merged.len();
158        self.plugin_manager.read().unwrap().run_hook(
159            "diagnostics_updated",
160            crate::services::plugins::hooks::HookArgs::DiagnosticsUpdated {
161                uri: uri.to_string(),
162                count,
163            },
164        );
165    }
166
167    /// Handle LSP diagnostics (push model — publishDiagnostics from flycheck/cargo)
168    pub(super) fn handle_lsp_diagnostics(
169        &mut self,
170        uri: String,
171        diagnostics: Vec<Diagnostic>,
172        server_name: String,
173    ) {
174        // Discard diagnostics from servers that have been shut down.  The async
175        // bridge may still contain queued messages from a server that was stopped
176        // between the time it sent the notification and when we drain the channel.
177        if let Some(lsp) = self.lsp() {
178            if !lsp.has_server_named(&server_name) {
179                tracing::debug!(
180                    "Dropping diagnostics from stopped server '{}' for {}",
181                    server_name,
182                    uri
183                );
184                return;
185            }
186        }
187
188        tracing::debug!(
189            "Processing {} push diagnostics from '{}' for {}",
190            diagnostics.len(),
191            server_name,
192            uri
193        );
194
195        let server_map = self
196            .active_window_mut()
197            .stored_push_diagnostics
198            .entry(uri.clone())
199            .or_default();
200        if diagnostics.is_empty() {
201            server_map.remove(&server_name);
202            // Clean up empty outer entry
203            if server_map.is_empty() {
204                self.active_window_mut()
205                    .stored_push_diagnostics
206                    .remove(&uri);
207            }
208        } else {
209            server_map.insert(server_name, diagnostics);
210        }
211
212        self.merge_and_apply_diagnostics(&uri);
213    }
214
215    /// Handle LSP pulled diagnostics (pull model — native RA diagnostics, LSP 3.17+)
216    pub(super) fn handle_lsp_pulled_diagnostics(
217        &mut self,
218        uri: String,
219        result_id: Option<String>,
220        diagnostics: Vec<Diagnostic>,
221        unchanged: bool,
222    ) {
223        if unchanged {
224            tracing::debug!(
225                "Diagnostics unchanged for {} (result_id: {:?})",
226                uri,
227                result_id
228            );
229            return;
230        }
231
232        tracing::debug!(
233            "Processing {} pulled diagnostics for {} (result_id: {:?})",
234            diagnostics.len(),
235            uri,
236            result_id
237        );
238
239        // Store result_id for incremental updates
240        if let Some(result_id) = result_id {
241            self.active_window_mut()
242                .diagnostic_result_ids
243                .insert(uri.clone(), result_id);
244        }
245
246        if diagnostics.is_empty() {
247            self.active_window_mut()
248                .stored_pull_diagnostics
249                .remove(&uri);
250        } else {
251            self.active_window_mut()
252                .stored_pull_diagnostics
253                .insert(uri.clone(), diagnostics);
254        }
255
256        self.merge_and_apply_diagnostics(&uri);
257    }
258
259    /// Clear all diagnostics originating from a specific server.
260    ///
261    /// Removes the server's entries from `stored_push_diagnostics`, then
262    /// re-merges and re-applies diagnostics for every affected URI so that
263    /// overlays on screen are updated immediately.
264    pub(crate) fn clear_diagnostics_for_server(&mut self, server_name: &str) {
265        // Collect URIs that have diagnostics from this server.
266        let affected_uris: Vec<String> = self
267            .active_window()
268            .stored_push_diagnostics
269            .iter()
270            .filter_map(|(uri, server_map)| {
271                if server_map.contains_key(server_name) {
272                    Some(uri.clone())
273                } else {
274                    None
275                }
276            })
277            .collect();
278
279        if affected_uris.is_empty() {
280            return;
281        }
282
283        tracing::info!(
284            "Clearing diagnostics from server '{}' for {} URIs",
285            server_name,
286            affected_uris.len()
287        );
288
289        for uri in &affected_uris {
290            if let Some(server_map) = self
291                .active_window_mut()
292                .stored_push_diagnostics
293                .get_mut(uri)
294            {
295                server_map.remove(server_name);
296                if server_map.is_empty() {
297                    self.active_window_mut().stored_push_diagnostics.remove(uri);
298                }
299            }
300
301            // Invalidate the diagnostic overlay cache so the re-merge actually
302            // updates on-screen overlays even if the resulting hash happens to
303            // match a previous state.
304            crate::services::lsp::diagnostics::invalidate_cache_for_file(uri);
305
306            self.merge_and_apply_diagnostics(uri);
307        }
308    }
309}
310
311// =============================================================================
312// LSP Feature Handlers
313// =============================================================================
314
315impl Editor {
316    /// Handle LSP inlay hints response — thin shim over
317    /// [`Window::handle_lsp_inlay_hints`]. The body is purely
318    /// window-state mutation.
319    pub(super) fn handle_lsp_inlay_hints(
320        &mut self,
321        request_id: u64,
322        uri: String,
323        hints: Vec<InlayHint>,
324    ) {
325        self.active_window_mut()
326            .handle_lsp_inlay_hints(request_id, uri, hints);
327    }
328}
329
330impl crate::app::window::Window {
331    /// Handle LSP inlay hints response. Pure window-state
332    /// mutation — pulls the in-flight request from the per-window
333    /// pending map, version-checks against the current buffer
334    /// state, and applies hints as virtual text.
335    pub fn handle_lsp_inlay_hints(
336        &mut self,
337        request_id: u64,
338        uri: String,
339        hints: Vec<lsp_types::InlayHint>,
340    ) {
341        let Some(request) = self.pending_inlay_hints_requests.remove(&request_id) else {
342            tracing::debug!(
343                "Ignoring stale inlay hints response (request_id={})",
344                request_id
345            );
346            return;
347        };
348
349        // Drop responses that raced behind a local edit — the hint
350        // positions reference stale byte offsets and would render at
351        // the wrong place. A fresh request was (or will be) scheduled
352        // by the debounced inlay-hints timer on every didChange.
353        let state_version = match self.buffers.get(&request.buffer_id) {
354            Some(s) => s.buffer.version(),
355            None => return, // Buffer was closed before the response arrived.
356        };
357        if state_version != request.version {
358            tracing::debug!(
359                "Ignoring stale inlay hints for {} (request_id={}, version={}, current={})",
360                uri,
361                request_id,
362                request.version,
363                state_version
364            );
365            return;
366        }
367
368        tracing::info!(
369            "Received {} inlay hints for {} (request_id={})",
370            hints.len(),
371            uri,
372            request_id
373        );
374
375        if let Some(state) = self.buffers.get_mut(&request.buffer_id) {
376            super::Editor::apply_inlay_hints_to_state(state, &hints);
377            tracing::info!(
378                "Applied {} inlay hints as virtual text to buffer {:?}",
379                hints.len(),
380                request.buffer_id
381            );
382        }
383    }
384}
385
386impl Editor {
387    /// Handle LSP folding ranges response. The Editor wrapper
388    /// orchestrates the URI-keyed `stored_folding_ranges` map
389    /// (Editor-global because URIs can map to buffers in any
390    /// window) and delegates the per-window buffer-state mutation
391    /// to [`Window::apply_folding_ranges_response`].
392    pub(super) fn handle_lsp_folding_ranges(
393        &mut self,
394        request_id: u64,
395        uri: String,
396        ranges: Vec<FoldingRange>,
397    ) {
398        // First peek at the active window to check whether the
399        // request is still pending and whether the response is stale
400        // (buffer version moved on). Returns the buffer_id +
401        // up-to-date status so the editor-global stored_folding_ranges
402        // update can happen in this scope.
403        enum FoldingDispatch {
404            Stale { buffer_id: BufferId },
405            Apply { buffer_id: BufferId },
406            Skip,
407        }
408        let dispatch = {
409            let win = self.active_window_mut();
410            let Some(request) = win.pending_folding_range_requests.remove(&request_id) else {
411                tracing::debug!(
412                    "Ignoring folding ranges response without pending request (request_id={})",
413                    request_id
414                );
415                return;
416            };
417            win.folding_ranges_in_flight.remove(&request.buffer_id);
418            match win.buffers.get(&request.buffer_id) {
419                Some(state) if state.buffer.version() == request.version => {
420                    FoldingDispatch::Apply {
421                        buffer_id: request.buffer_id,
422                    }
423                }
424                Some(state) => {
425                    tracing::debug!(
426                        "Ignoring stale folding ranges for {} (request_id={}, version={}, current={})",
427                        uri,
428                        request_id,
429                        request.version,
430                        state.buffer.version()
431                    );
432                    FoldingDispatch::Stale {
433                        buffer_id: request.buffer_id,
434                    }
435                }
436                None => FoldingDispatch::Skip,
437            }
438        };
439        let buffer_id = match dispatch {
440            FoldingDispatch::Apply { buffer_id } => buffer_id,
441            FoldingDispatch::Stale { buffer_id } => {
442                self.active_window_mut()
443                    .schedule_folding_ranges_refresh(buffer_id);
444                return;
445            }
446            FoldingDispatch::Skip => return,
447        };
448
449        if ranges.is_empty() {
450            self.stored_folding_ranges_mut().remove(&uri);
451        } else {
452            self.stored_folding_ranges_mut().insert(uri.clone(), ranges);
453        }
454
455        let lsp_ranges = self
456            .active_window()
457            .stored_folding_ranges
458            .get(&uri)
459            .cloned()
460            .unwrap_or_default();
461        self.active_window_mut()
462            .apply_folding_ranges_response(buffer_id, lsp_ranges);
463    }
464
465    /// Handle LSP semantic tokens response
466    pub(super) fn handle_lsp_semantic_tokens(
467        &mut self,
468        request_id: u64,
469        uri: String,
470        response: LspSemanticTokensResponse,
471    ) {
472        let (
473            buffer_id,
474            target_version,
475            full_request_kind,
476            requested_range,
477            requested_start_line,
478            requested_end_line,
479        ) = if let Some(range_request) = self
480            .active_window_mut()
481            .take_pending_semantic_token_range_request(request_id)
482        {
483            (
484                range_request.buffer_id,
485                range_request.version,
486                None,
487                Some(range_request.range),
488                Some(range_request.start_line),
489                Some(range_request.end_line),
490            )
491        } else if let Some(full_request) = self
492            .active_window_mut()
493            .take_pending_semantic_token_request(request_id)
494        {
495            (
496                full_request.buffer_id,
497                full_request.version,
498                Some(full_request.kind),
499                None,
500                None,
501                None,
502            )
503        } else {
504            tracing::debug!(
505                "Semantic tokens response {} for {} without pending entry",
506                request_id,
507                uri
508            );
509            return;
510        };
511
512        // Get language from buffer's stored state
513        let Some(language) = self
514            .windows
515            .get(&self.active_window)
516            .map(|w| &w.buffers)
517            .expect("active window present")
518            .get(&buffer_id)
519            .map(|s| s.language.clone())
520        else {
521            return;
522        };
523
524        let legend = match self
525            .lsp()
526            .as_ref()
527            .and_then(|manager| manager.semantic_tokens_legend(&language).cloned())
528        {
529            Some(legend) => legend,
530            None => {
531                tracing::debug!("Semantic tokens legend missing for language {}", language);
532                return;
533            }
534        };
535
536        let Some(state) = self
537            .windows
538            .get_mut(&self.active_window)
539            .map(|w| &mut w.buffers)
540            .expect("active window present")
541            .get_mut(&buffer_id)
542        else {
543            return;
544        };
545
546        let current_version = state.buffer.version();
547        if current_version != target_version {
548            // Stale response - ignore; next render will request fresh tokens.
549            return;
550        }
551
552        match (requested_range, full_request_kind) {
553            (Some(range), None) => {
554                let result = match response {
555                    LspSemanticTokensResponse::Range(result) => result,
556                    _ => {
557                        tracing::warn!(
558                            "Semantic tokens range response {} for {} had mismatched type",
559                            request_id,
560                            uri
561                        );
562                        return;
563                    }
564                };
565
566                match result {
567                    Err(_) => {
568                        // Error already logged at the appropriate level by the
569                        // generic LSP response handler (debug for ContentModified/
570                        // ServerCancelled, warn for real errors).
571                    }
572                    Ok(tokens_opt) => {
573                        let spans = match tokens_opt {
574                            Some(SemanticTokensRangeResult::Tokens(tokens)) => {
575                                // LSP semantic tokens are always delta-encoded from document
576                                // position (0,0), even for range requests. The range only
577                                // filters which tokens are returned, not the encoding origin.
578                                let decoded = decode_semantic_token_data(
579                                    &state.buffer,
580                                    &legend,
581                                    &tokens.data,
582                                    0,
583                                );
584                                decoded.spans
585                            }
586                            Some(SemanticTokensRangeResult::Partial(partial)) => {
587                                let decoded = decode_semantic_token_data(
588                                    &state.buffer,
589                                    &legend,
590                                    &partial.data,
591                                    0,
592                                );
593                                decoded.spans
594                            }
595                            None => Vec::new(),
596                        };
597
598                        let applied = crate::services::lsp::semantic_tokens::apply_semantic_tokens_range_to_state(
599                            state,
600                            range.clone(),
601                            &spans,
602                            &*self.theme.read().unwrap(),
603                        );
604                        if applied {
605                            self.active_window_mut()
606                                .semantic_tokens_range_applied
607                                .insert(
608                                    buffer_id,
609                                    (
610                                        requested_start_line.unwrap_or(0),
611                                        requested_end_line.unwrap_or(0),
612                                        current_version,
613                                    ),
614                                );
615                        }
616                    }
617                }
618            }
619            (None, Some(super::SemanticTokensFullRequestKind::Full)) => {
620                let result = match response {
621                    LspSemanticTokensResponse::Full(result) => result,
622                    _ => {
623                        tracing::warn!(
624                            "Semantic tokens response {} for {} had mismatched type",
625                            request_id,
626                            uri
627                        );
628                        return;
629                    }
630                };
631
632                match result {
633                    Err(_) => {
634                        // Error already logged by the generic LSP response handler.
635                    }
636                    Ok(tokens_opt) => {
637                        let decoded = match tokens_opt {
638                            Some(SemanticTokensResult::Tokens(tokens)) => {
639                                let decoded = decode_semantic_token_data(
640                                    &state.buffer,
641                                    &legend,
642                                    &tokens.data,
643                                    0,
644                                );
645                                SemanticTokensFullDecode {
646                                    result_id: tokens.result_id.clone(),
647                                    raw_data: decoded.raw,
648                                    spans: decoded.spans,
649                                }
650                            }
651                            Some(SemanticTokensResult::Partial(partial)) => {
652                                let decoded = decode_semantic_token_data(
653                                    &state.buffer,
654                                    &legend,
655                                    &partial.data,
656                                    0,
657                                );
658                                SemanticTokensFullDecode {
659                                    result_id: None,
660                                    raw_data: decoded.raw,
661                                    spans: decoded.spans,
662                                }
663                            }
664                            None => SemanticTokensFullDecode {
665                                result_id: None,
666                                raw_data: Vec::new(),
667                                spans: Vec::new(),
668                            },
669                        };
670
671                        crate::services::lsp::semantic_tokens::apply_semantic_tokens_to_state(
672                            state,
673                            &decoded.spans,
674                            &*self.theme.read().unwrap(),
675                        );
676
677                        state.set_semantic_tokens(SemanticTokenStore {
678                            version: current_version,
679                            result_id: decoded.result_id,
680                            data: decoded.raw_data,
681                            tokens: decoded.spans,
682                        });
683                    }
684                }
685            }
686            (None, Some(super::SemanticTokensFullRequestKind::FullDelta)) => {
687                let result = match response {
688                    LspSemanticTokensResponse::FullDelta(result) => result,
689                    _ => {
690                        tracing::warn!(
691                            "Semantic tokens delta response {} for {} had mismatched type",
692                            request_id,
693                            uri
694                        );
695                        return;
696                    }
697                };
698
699                match result {
700                    Err(_) => {
701                        // Error already logged by the generic LSP response handler.
702                    }
703                    Ok(tokens_opt) => {
704                        let existing_store = state.semantic_tokens.as_ref();
705                        let existing_result_id =
706                            existing_store.and_then(|store| store.result_id.clone());
707                        let existing_data = existing_store.map(|store| store.data.clone());
708
709                        let decoded = match tokens_opt {
710                            Some(SemanticTokensFullDeltaResult::Tokens(tokens)) => {
711                                SemanticTokensDeltaDecode {
712                                    result_id: tokens.result_id.clone(),
713                                    raw_data: semantic_tokens_to_raw(&tokens.data),
714                                }
715                            }
716                            Some(SemanticTokensFullDeltaResult::TokensDelta(delta)) => {
717                                let Some(existing) = existing_data else {
718                                    tracing::warn!(
719                                        "Semantic tokens delta response {} for {} missing baseline",
720                                        request_id,
721                                        uri
722                                    );
723                                    return;
724                                };
725                                let updated = match apply_semantic_token_edits(
726                                    existing,
727                                    &delta.edits,
728                                ) {
729                                    Some(data) => data,
730                                    None => {
731                                        tracing::warn!(
732                                            "Semantic tokens delta response {} for {} had invalid edits",
733                                            request_id,
734                                            uri
735                                        );
736                                        return;
737                                    }
738                                };
739                                SemanticTokensDeltaDecode {
740                                    result_id: delta.result_id.clone().or(existing_result_id),
741                                    raw_data: updated,
742                                }
743                            }
744                            Some(SemanticTokensFullDeltaResult::PartialTokensDelta { edits }) => {
745                                let Some(existing) = existing_data else {
746                                    tracing::warn!(
747                                        "Semantic tokens delta response {} for {} missing baseline",
748                                        request_id,
749                                        uri
750                                    );
751                                    return;
752                                };
753                                let updated = match apply_semantic_token_edits(existing, &edits) {
754                                    Some(data) => data,
755                                    None => {
756                                        tracing::warn!(
757                                            "Semantic tokens delta response {} for {} had invalid edits",
758                                            request_id,
759                                            uri
760                                        );
761                                        return;
762                                    }
763                                };
764                                SemanticTokensDeltaDecode {
765                                    result_id: existing_result_id,
766                                    raw_data: updated,
767                                }
768                            }
769                            None => SemanticTokensDeltaDecode {
770                                result_id: None,
771                                raw_data: Vec::new(),
772                            },
773                        };
774
775                        let spans = decode_semantic_token_raw_data(
776                            &state.buffer,
777                            &legend,
778                            &decoded.raw_data,
779                            0,
780                        );
781
782                        crate::services::lsp::semantic_tokens::apply_semantic_tokens_to_state(
783                            state,
784                            &spans,
785                            &*self.theme.read().unwrap(),
786                        );
787
788                        state.set_semantic_tokens(SemanticTokenStore {
789                            version: current_version,
790                            result_id: decoded.result_id,
791                            data: decoded.raw_data,
792                            tokens: spans,
793                        });
794                    }
795                }
796            }
797            _ => {
798                tracing::warn!(
799                    "Semantic tokens response {} for {} had mismatched pending state",
800                    request_id,
801                    uri
802                );
803            }
804        }
805    }
806
807    /// Handle LSP server quiescent notification (rust-analyzer project fully loaded)
808    pub(super) fn handle_lsp_server_quiescent(&mut self, language: String) {
809        tracing::info!(
810            "LSP ({}) project fully loaded, re-requesting diagnostics and inlay hints",
811            language
812        );
813
814        // Re-pull diagnostics for all open buffers — the initial pull likely
815        // returned empty results because the server hadn't loaded the project yet
816        self.pull_diagnostics_for_language(&language);
817
818        // Skip inlay hints if disabled
819        if !self.config.editor.enable_inlay_hints {
820            // Folding ranges may improve after project is fully loaded
821            self.request_folding_ranges_for_language(&language);
822            return;
823        }
824
825        // Collect only buffers whose language matches this server's
826        // scope, before mutably borrowing `self.lsp`. Previously this
827        // iterated every open buffer regardless of language, so the
828        // rust handle was asked for inlay hints on `.json` / `.nix`
829        // URIs and replied `file not found (-32603)`.
830        let buffer_infos: Vec<_> = self
831            .buffers_for_language(&language)
832            .into_iter()
833            .map(|(buffer_id, uri)| {
834                let (line_count, version) = self
835                    .buffers()
836                    .get(&buffer_id)
837                    .map(|s| (s.buffer.line_count().unwrap_or(1000), s.buffer.version()))
838                    .unwrap_or((1000, 0));
839                (buffer_id, uri, line_count, version)
840            })
841            .collect();
842
843        let __active_id = self.active_window;
844
845        let Some(__win) = self.windows.get_mut(&__active_id) else {
846            return;
847        };
848        let Some(lsp) = __win.lsp.as_mut() else {
849            return;
850        };
851
852        // LSP should already be running since we got a quiescent notification
853        let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
854        else {
855            return;
856        };
857        let client = &mut sh.handle;
858
859        let __next_id = &mut __win.next_lsp_request_id;
860        let __pending = &mut __win.pending_inlay_hints_requests;
861
862        // Request inlay hints for each buffer. Each request is keyed in
863        // the pending map by its own id (and carries buffer_id + version)
864        // so responses across all buffers are matched individually — a
865        // single Option used to be overwritten by each iteration, dropping
866        // every response except the last.
867        for (buffer_id, uri, line_count, version) in buffer_infos {
868            let request_id = *__next_id;
869            *__next_id += 1;
870
871            let last_line = line_count.saturating_sub(1) as u32;
872            if let Err(e) =
873                client.inlay_hints(request_id, uri.as_uri().clone(), 0, 0, last_line, 10000)
874            {
875                tracing::debug!(
876                    "Failed to re-request inlay hints for {}: {}",
877                    uri.as_str(),
878                    e
879                );
880            } else {
881                __pending.insert(request_id, super::InlayHintsRequest { buffer_id, version });
882                tracing::info!(
883                    "Re-requested inlay hints for {} (request_id={})",
884                    uri.as_str(),
885                    request_id
886                );
887            }
888        }
889
890        // Folding ranges may improve after project is fully loaded
891        self.request_folding_ranges_for_language(&language);
892    }
893
894    /// Handle workspace/diagnostic/refresh request from the LSP server.
895    /// Re-pulls diagnostics for all open documents of the given language.
896    pub(super) fn handle_lsp_diagnostic_refresh(&mut self, language: String) {
897        tracing::info!(
898            "LSP ({}) diagnostic refresh requested, re-pulling diagnostics",
899            language
900        );
901        self.pull_diagnostics_for_language(&language);
902    }
903
904    /// Re-pull diagnostics for all open buffers associated with the given language.
905    pub(super) fn pull_diagnostics_for_language(&mut self, language: &str) {
906        // Use the shared language-filtered buffer enumeration so requests
907        // never leak out to a server with a different scope.
908        let uris: Vec<_> = self
909            .buffers_for_language(language)
910            .into_iter()
911            .map(|(_, uri)| uri)
912            .collect();
913
914        if uris.is_empty() {
915            return;
916        }
917
918        let __active_id = self.active_window;
919        let Some(__win) = self.windows.get_mut(&__active_id) else {
920            return;
921        };
922        let diagnostic_result_ids = &__win.diagnostic_result_ids;
923        let Some(lsp) = __win.lsp.as_mut() else {
924            return;
925        };
926        let Some(sh) = lsp.handle_for_feature_mut(language, crate::types::LspFeature::Diagnostics)
927        else {
928            return;
929        };
930        let client = &mut sh.handle;
931        let __next_id = &mut __win.next_lsp_request_id;
932
933        for uri in uris {
934            let request_id = *__next_id;
935            *__next_id += 1;
936            let previous_result_id = diagnostic_result_ids.get(uri.as_str()).cloned();
937            if let Err(e) =
938                client.document_diagnostic(request_id, uri.as_uri().clone(), previous_result_id)
939            {
940                tracing::debug!("Failed to re-pull diagnostics for {}: {}", uri.as_str(), e);
941            } else {
942                tracing::info!(
943                    "Re-pulling diagnostics for {} (request_id={})",
944                    uri.as_str(),
945                    request_id
946                );
947            }
948        }
949    }
950
951    /// Handle LSP progress notification ($/progress)
952    pub(super) fn handle_lsp_progress(
953        &mut self,
954        language: String,
955        token: String,
956        value: LspProgressValue,
957    ) {
958        match value {
959            LspProgressValue::Begin {
960                title,
961                message,
962                percentage,
963            } => {
964                self.active_window_mut().lsp_progress.insert(
965                    token.clone(),
966                    LspProgressInfo {
967                        language,
968                        title,
969                        message,
970                        percentage,
971                    },
972                );
973            }
974            LspProgressValue::Report {
975                message,
976                percentage,
977            } => {
978                if let Some(info) = self.active_window_mut().lsp_progress.get_mut(&token) {
979                    info.message = message;
980                    info.percentage = percentage;
981                }
982            }
983            LspProgressValue::End { .. } => {
984                self.active_window_mut().lsp_progress.remove(&token);
985            }
986        }
987        // If the LSP status popup is open, rebuild it so the progress line
988        // inside reflects the new title / message / percentage.  The
989        // status-bar indicator itself only shows a spinner, so the popup
990        // is the user's only window into the live progress text.
991        self.refresh_lsp_status_popup_if_open();
992    }
993
994    /// Handle LSP window message (window/showMessage)
995    pub(super) fn handle_lsp_window_message(
996        &mut self,
997        language: String,
998        message_type: LspMessageType,
999        message: String,
1000    ) {
1001        // Add to window messages list
1002        self.active_window_mut()
1003            .lsp_window_messages
1004            .push(LspMessageEntry {
1005                language: language.clone(),
1006                message_type,
1007                message: message.clone(),
1008                timestamp: Instant::now(),
1009            });
1010
1011        // Keep only last 100 messages
1012        if self.active_window_mut().lsp_window_messages.len() > 100 {
1013            self.active_window_mut().lsp_window_messages.remove(0);
1014        }
1015
1016        // Show important messages in status bar
1017        match message_type {
1018            LspMessageType::Error | LspMessageType::Warning => {
1019                self.active_window_mut().status_message =
1020                    Some(format!("LSP ({}): {}", language, message));
1021            }
1022            _ => {
1023                // Info and Log messages are not shown in status bar
1024            }
1025        }
1026    }
1027
1028    /// Handle LSP log message (window/logMessage)
1029    pub(super) fn handle_lsp_log_message(
1030        &mut self,
1031        language: String,
1032        message_type: LspMessageType,
1033        message: String,
1034    ) {
1035        self.active_window_mut()
1036            .lsp_log_messages
1037            .push(LspMessageEntry {
1038                language,
1039                message_type,
1040                message,
1041                timestamp: Instant::now(),
1042            });
1043
1044        // Keep only last 500 log messages
1045        if self.active_window_mut().lsp_log_messages.len() > 500 {
1046            self.active_window_mut().lsp_log_messages.remove(0);
1047        }
1048    }
1049
1050    /// Handle LSP server status update
1051    pub(super) fn handle_lsp_status_update(
1052        &mut self,
1053        language: String,
1054        server_name: String,
1055        status: LspServerStatus,
1056    ) {
1057        use crate::services::async_bridge::LspServerStatus;
1058
1059        let server_name_ref = server_name.clone();
1060        let key = (language.clone(), server_name);
1061
1062        // Get old status for event
1063        let old_status = self
1064            .active_window_mut()
1065            .lsp_server_statuses
1066            .get(&key)
1067            .cloned();
1068
1069        // Update server status
1070        self.active_window_mut()
1071            .lsp_server_statuses
1072            .insert(key, status);
1073
1074        // Update warning domain for LSP status indicator
1075        self.active_window_mut().update_lsp_warning_domain();
1076
1077        // When a server becomes ready, send didOpen for all open buffers of
1078        // that language so the server can start providing diagnostics, etc.
1079        // without waiting for the next user edit.
1080        if status == LspServerStatus::Running {
1081            let was_already_running = old_status
1082                .as_ref()
1083                .is_some_and(|s| matches!(s, LspServerStatus::Running));
1084            if !was_already_running {
1085                let scope = self
1086                    .lsp()
1087                    .as_ref()
1088                    .and_then(|lsp| lsp.server_scope(&server_name_ref).cloned());
1089                match scope {
1090                    Some(scope) if scope.is_universal() => {
1091                        let languages: Vec<String> =
1092                            self.buffers().languages().into_iter().collect();
1093                        for lang in languages {
1094                            self.reopen_buffers_for_language(&lang);
1095                        }
1096                    }
1097                    Some(scope) => {
1098                        for lang in scope.languages() {
1099                            self.reopen_buffers_for_language(lang);
1100                        }
1101                    }
1102                    None => {
1103                        // Per-language server — language comes from the status message
1104                        self.reopen_buffers_for_language(&language);
1105                    }
1106                }
1107            }
1108        }
1109
1110        // Handle server crash - trigger auto-restart
1111        if status == LspServerStatus::Error {
1112            let was_running = old_status
1113                .as_ref()
1114                .map(|s| matches!(s, LspServerStatus::Running | LspServerStatus::Initializing))
1115                .unwrap_or(false);
1116
1117            if was_running {
1118                // Clear stale diagnostics from the crashed server so they
1119                // don't linger on screen while we wait for a restart.
1120                self.clear_diagnostics_for_server(&server_name_ref);
1121
1122                let __active_id = self.active_window;
1123
1124                if let Some(lsp) = self
1125                    .windows
1126                    .get_mut(&__active_id)
1127                    .and_then(|w| w.lsp.as_mut())
1128                {
1129                    let message = lsp.handle_server_crash(&language, &server_name_ref);
1130                    self.active_window_mut().status_message = Some(message);
1131                }
1132            }
1133        }
1134
1135        // When a server transitions to Error or Shutdown, drop any
1136        // `$/progress` entries for this language if no other server is
1137        // still alive for it. The dead server will never emit the
1138        // matching `end` notification, so without this prune the
1139        // status-bar spinner stays stuck on the rotating braille glyph
1140        // (and the status popup keeps showing "Indexing …") even
1141        // though the process is gone — that's the "popup still says
1142        // indexing after external kill" user report.
1143        if matches!(status, LspServerStatus::Error | LspServerStatus::Shutdown) {
1144            let any_running_for_lang =
1145                self.active_window()
1146                    .lsp_server_statuses
1147                    .iter()
1148                    .any(|((lang, _), s)| {
1149                        lang == &language
1150                            && !matches!(s, LspServerStatus::Error | LspServerStatus::Shutdown,)
1151                    });
1152            if !any_running_for_lang {
1153                let lang_owned = language.clone();
1154                self.active_window_mut()
1155                    .lsp_progress
1156                    .retain(|_, info| info.language != lang_owned);
1157            }
1158            // Refresh the popup so any in-progress "(ready) ⏳ Indexing"
1159            // rows for this server flip to "(not running)" right away.
1160            self.refresh_lsp_status_popup_if_open();
1161        }
1162
1163        // Emit control event
1164        let status_str = match status {
1165            LspServerStatus::Starting => "starting",
1166            LspServerStatus::Initializing => "initializing",
1167            LspServerStatus::Running => "running",
1168            LspServerStatus::Error => "error",
1169            LspServerStatus::Shutdown => "shutdown",
1170        };
1171        let old_status_str = old_status
1172            .map(|s| match s {
1173                LspServerStatus::Starting => "starting",
1174                LspServerStatus::Initializing => "initializing",
1175                LspServerStatus::Running => "running",
1176                LspServerStatus::Error => "error",
1177                LspServerStatus::Shutdown => "shutdown",
1178            })
1179            .unwrap_or("none");
1180
1181        self.emit_event(
1182            crate::model::control_event::events::LSP_STATUS_CHANGED.name,
1183            serde_json::json!({
1184                "language": language,
1185                "old_status": old_status_str,
1186                "status": status_str
1187            }),
1188        );
1189    }
1190
1191    /// Handle custom LSP notification
1192    #[allow(dead_code)] // Prepared for future use when AsyncMessage::LspCustomNotification is added
1193    pub(super) fn handle_custom_notification(
1194        &mut self,
1195        language: String,
1196        method: String,
1197        params: Option<Value>,
1198    ) {
1199        tracing::debug!("Custom LSP notification {} from {}", method, language);
1200        let payload = serde_json::json!({
1201            "language": language,
1202            "method": method,
1203            "params": params,
1204        });
1205        self.emit_event("lsp/custom_notification", payload);
1206    }
1207
1208    /// Handle LSP server request (server -> client)
1209    /// These are requests from the LSP server that require handling, typically
1210    /// custom/extension methods specific to certain language servers.
1211    pub(super) fn handle_lsp_server_request(
1212        &mut self,
1213        language: String,
1214        server_command: String,
1215        method: String,
1216        params: Option<Value>,
1217    ) {
1218        tracing::debug!(
1219            "LSP server request {} from {} ({})",
1220            method,
1221            language,
1222            server_command
1223        );
1224
1225        // Convert params to JSON string for the hook
1226        let params_str = params.map(|p| p.to_string());
1227
1228        // Run the lsp_server_request hook for plugins
1229        self.plugin_manager.read().unwrap().run_hook(
1230            "lsp_server_request",
1231            crate::services::plugins::hooks::HookArgs::LspServerRequest {
1232                language,
1233                method,
1234                server_command,
1235                params: params_str,
1236            },
1237        );
1238    }
1239
1240    /// Handle plugin LSP response
1241    pub(super) fn handle_plugin_lsp_response(
1242        &mut self,
1243        request_id: u64,
1244        result: Result<Value, String>,
1245    ) {
1246        use fresh_core::api::JsCallbackId;
1247        tracing::debug!("Received plugin LSP response (request_id={})", request_id);
1248        let callback_id = JsCallbackId::from(request_id);
1249        match result {
1250            Ok(value) => {
1251                self.plugin_manager
1252                    .read()
1253                    .unwrap()
1254                    .resolve_callback(callback_id, value.to_string());
1255            }
1256            Err(err) => {
1257                self.plugin_manager
1258                    .read()
1259                    .unwrap()
1260                    .reject_callback(callback_id, err);
1261            }
1262        }
1263    }
1264
1265    /// Handle generic plugin response (e.g., GetBufferText result)
1266    pub(super) fn handle_plugin_response(&mut self, response: fresh_core::api::PluginResponse) {
1267        tracing::debug!("Received plugin response: {:?}", response);
1268        self.send_plugin_response(response);
1269    }
1270}
1271
1272// =============================================================================
1273// File System Event Handlers
1274// =============================================================================
1275
1276impl Editor {
1277    /// Handle file changed externally notification (from AsyncMessage)
1278    ///
1279    /// Includes debounce logic to prevent rapid auto-reverts from overwhelming the editor.
1280    /// This is different from `handle_file_changed` which actually reloads the file.
1281    pub(super) fn handle_async_file_changed(&mut self, path: String) -> bool {
1282        const DEBOUNCE_WINDOW: Duration = Duration::from_secs(10);
1283        const RAPID_REVERT_THRESHOLD: u32 = 10; // Require 10 reverts in 10 seconds to disable
1284
1285        // Skip if auto-revert is disabled
1286        if !self.active_window().auto_revert_enabled {
1287            return false;
1288        }
1289
1290        let path_buf = PathBuf::from(&path);
1291
1292        // Only track events for files that are actually open in the editor
1293        let is_file_open = self
1294            .buffers()
1295            .iter()
1296            .any(|(_, state)| state.buffer.file_path() == Some(&path_buf));
1297
1298        if !is_file_open {
1299            tracing::trace!("Ignoring file change event for non-open file: {}", path);
1300            return false;
1301        }
1302
1303        // Track rapid file change events - only disable after many reverts in short window
1304        let mut should_disable = false;
1305        let now = self.time_source.now();
1306        let elapsed_window_ok = if let Some((window_start, _)) =
1307            self.active_window().file_rapid_change_counts.get(&path_buf)
1308        {
1309            self.time_source.elapsed_since(*window_start) < DEBOUNCE_WINDOW
1310        } else {
1311            false
1312        };
1313        if let Some((window_start, count)) = self
1314            .active_window_mut()
1315            .file_rapid_change_counts
1316            .get_mut(&path_buf)
1317        {
1318            if elapsed_window_ok {
1319                *count += 1;
1320
1321                if *count >= RAPID_REVERT_THRESHOLD {
1322                    should_disable = true;
1323                    tracing::info!(
1324                        "Auto-revert disabled for {:?} ({} reverts in {:?})",
1325                        path_buf,
1326                        count,
1327                        DEBOUNCE_WINDOW
1328                    );
1329                }
1330            } else {
1331                // Reset counter - start a new window
1332                *count = 1;
1333                *window_start = now;
1334            }
1335        } else {
1336            // First event for this file
1337            let now = self.time_source.now();
1338            self.active_window_mut()
1339                .file_rapid_change_counts
1340                .insert(path_buf.clone(), (now, 1));
1341        }
1342        if should_disable {
1343            self.active_window_mut().auto_revert_enabled = false;
1344            self.active_window_mut().status_message = Some(format!(
1345                "Auto-revert disabled: {} is updating too frequently (use Ctrl+Shift+R to re-enable)",
1346                path_buf.file_name().unwrap_or_default().to_string_lossy()
1347            ));
1348            return false;
1349        }
1350
1351        tracing::info!("File changed externally: {}", path);
1352        self.handle_file_changed(&path);
1353        true
1354    }
1355}
1356
1357// =============================================================================
1358// File Explorer Handlers
1359// =============================================================================
1360
1361impl Editor {
1362    /// Handle file explorer initialized
1363    pub(super) fn handle_file_explorer_initialized(&mut self, mut view: FileTreeView) {
1364        tracing::info!("File explorer initialized");
1365
1366        // Load root .gitignore
1367        let root_id = view.tree().root_id();
1368        let root_path = view.tree().get_node(root_id).map(|n| n.entry.path.clone());
1369
1370        if let Some(root_path) = root_path {
1371            crate::app::file_operations::load_gitignore_via_fs(
1372                self.authority.filesystem.as_ref(),
1373                &mut view,
1374                &root_path,
1375            );
1376            tracing::debug!("Loaded root .gitignore from {:?}", root_path);
1377        }
1378
1379        // Apply show_hidden / show_gitignored settings.
1380        // Use pending session-restore values if present, otherwise fall back
1381        // to the persisted config so the setting survives across sessions.
1382        let show_hidden = self
1383            .active_window_mut()
1384            .pending_file_explorer_show_hidden
1385            .take()
1386            .unwrap_or(self.config.file_explorer.show_hidden);
1387        view.ignore_patterns_mut().set_show_hidden(show_hidden);
1388        tracing::debug!("Applied show_hidden={} on init", show_hidden);
1389
1390        let show_gitignored = self
1391            .active_window_mut()
1392            .pending_file_explorer_show_gitignored
1393            .take()
1394            .unwrap_or(self.config.file_explorer.show_gitignored);
1395        view.ignore_patterns_mut()
1396            .set_show_gitignored(show_gitignored);
1397        tracing::debug!("Applied show_gitignored={} on init", show_gitignored);
1398
1399        view.set_compact_directories(self.config.file_explorer.compact_directories);
1400
1401        self.active_window_mut().file_explorer = Some(view);
1402        self.set_status_message(t!("status.file_explorer_ready").to_string());
1403
1404        // If the user opened the explorer while a file from a nested
1405        // directory was active, the sync triggered by toggle_file_explorer
1406        // ran before this initialization completed (file_explorer was still
1407        // None) and did nothing. Run it again now so the tree auto-expands
1408        // to reveal the current file on first open (issue #1569).
1409        if self.file_explorer_visible() {
1410            self.active_window_mut().sync_file_explorer_to_active_file();
1411        }
1412    }
1413
1414    /// Handle file explorer node toggle completed
1415    pub(super) fn handle_file_explorer_toggle_node(&mut self, node_id: NodeId) {
1416        tracing::debug!("File explorer toggle completed for node {:?}", node_id);
1417    }
1418
1419    /// Handle file explorer node refresh completed
1420    pub(super) fn handle_file_explorer_refresh_node(&mut self, node_id: NodeId) {
1421        tracing::debug!("File explorer refresh completed for node {:?}", node_id);
1422        self.set_status_message(t!("explorer.refreshed_default").to_string());
1423    }
1424
1425    /// Handle file explorer expanded to path
1426    pub(super) fn handle_file_explorer_expanded_to_path(&mut self, mut view: FileTreeView) {
1427        tracing::trace!(
1428            "handle_file_explorer_expanded_to_path: restoring file_explorer after async expand"
1429        );
1430        view.update_scroll_for_selection();
1431        self.active_window_mut().file_explorer = Some(view);
1432        self.active_window_mut().file_explorer_sync_in_progress = false;
1433    }
1434}
1435
1436// =============================================================================
1437// Plugin Handlers
1438// =============================================================================
1439
1440impl Editor {
1441    /// Handle plugin process output completion
1442    pub(super) fn handle_plugin_process_output(
1443        &mut self,
1444        callback_id: fresh_core::api::JsCallbackId,
1445        stdout: String,
1446        stderr: String,
1447        exit_code: i32,
1448    ) {
1449        tracing::debug!(
1450            "Process {} completed: exit_code={}, stdout_len={}, stderr_len={}",
1451            callback_id,
1452            exit_code,
1453            stdout.len(),
1454            stderr.len()
1455        );
1456        // Resolve the plugin callback with the process output
1457        // Using SpawnResult struct ensures field names match TypeScript types
1458        let result = fresh_core::api::SpawnResult {
1459            stdout,
1460            stderr,
1461            exit_code,
1462        };
1463        self.plugin_manager
1464            .read()
1465            .unwrap()
1466            .resolve_callback(callback_id, serde_json::to_string(&result).unwrap());
1467    }
1468
1469    /// Process TypeScript plugin commands
1470    ///
1471    /// Returns true if any visual commands were processed (i.e. a re-render is needed).
1472    /// No-op sentinels like `HookCompleted` do not count.
1473    pub(super) fn process_plugin_commands(&mut self) -> bool {
1474        let commands = self.plugin_manager.write().unwrap().process_commands();
1475        if commands.is_empty() {
1476            return false;
1477        }
1478
1479        // Classify each command as visual (needs re-render) or not.
1480        // `HookCompleted` is a pure ack. `SetStatusBarValue` is treated as
1481        // visual only when the value actually differs from what's stored —
1482        // many plugins (e.g. git_statusbar) re-publish the same value on
1483        // every `render_start` hook, which would otherwise create a
1484        // render → hook → ack → render feedback loop at ~13Hz forever.
1485        //
1486        // The remaining `=> false` arms are side-effecting commands that
1487        // never touch the rendered buffer: scheduling a timer, spawning
1488        // processes / HTTP, watching paths, and writing plugin-private
1489        // state. Any *visual* result they eventually produce arrives as its
1490        // own command (overlay, virtual text, status value, …) and is
1491        // counted then. Treating these as visual forced a redraw on every
1492        // debounce tick — e.g. live_diff's 75ms `editor.delay()` recompute
1493        // repainted the screen twice per keystroke with no change. Invisible
1494        // on a fast terminal, but real lag over a serial console (#2100).
1495        use fresh_core::api::PluginCommand as Pc;
1496        let has_visual_commands = commands.iter().any(|c| match c {
1497            Pc::HookCompleted { .. }
1498            | Pc::Delay { .. }
1499            | Pc::SpawnProcess { .. }
1500            | Pc::SpawnBackgroundProcess { .. }
1501            | Pc::KillBackgroundProcess { .. }
1502            | Pc::SpawnProcessWait { .. }
1503            | Pc::HttpFetch { .. }
1504            | Pc::WatchPath { .. }
1505            | Pc::UnwatchPath { .. }
1506            | Pc::SetGlobalState { .. }
1507            | Pc::SetWindowState { .. }
1508            | Pc::SetViewState { .. } => false,
1509            Pc::SetStatusBarValue {
1510                buffer_id,
1511                key,
1512                value,
1513            } => {
1514                self.current_status_bar_value(fresh_core::BufferId(*buffer_id as usize), key)
1515                    != Some(value.as_str())
1516            }
1517            _ => true,
1518        });
1519
1520        let cmd_names: Vec<String> = commands.iter().map(|c| c.debug_variant_name()).collect();
1521        tracing::trace!(
1522            count = commands.len(),
1523            cmds = ?cmd_names,
1524            "process_plugin_commands"
1525        );
1526
1527        for command in &commands {
1528            match command {
1529                fresh_core::api::PluginCommand::RegisterGrammar {
1530                    language,
1531                    grammar_path,
1532                    extensions,
1533                } => {
1534                    tracing::info!(
1535                        "[SYNTAX DEBUG] processing RegisterGrammar: lang='{}', path='{}', ext={:?}",
1536                        language,
1537                        grammar_path,
1538                        extensions
1539                    );
1540                }
1541                fresh_core::api::PluginCommand::ReloadGrammars { .. } => {
1542                    tracing::info!("[SYNTAX DEBUG] processing ReloadGrammars command");
1543                }
1544                _ => {}
1545            }
1546        }
1547
1548        for command in commands {
1549            tracing::trace!(
1550                "process_plugin_commands: handling command {:?}",
1551                std::mem::discriminant(&command)
1552            );
1553            if let Err(e) = self.handle_plugin_command(command) {
1554                tracing::error!("Error handling TypeScript plugin command: {}", e);
1555            }
1556        }
1557
1558        // Flush any deferred grammar rebuilds as a single batch
1559        self.flush_pending_grammars();
1560
1561        has_visual_commands
1562    }
1563
1564    /// Process pending plugin action completions
1565    #[cfg(feature = "plugins")]
1566    pub(super) fn process_pending_plugin_actions(&mut self) {
1567        self.pending_plugin_actions
1568            .retain(|(action_name, receiver)| {
1569                match receiver.try_recv() {
1570                    Ok(result) => {
1571                        match result {
1572                            Ok(()) => {
1573                                tracing::info!(
1574                                    "Plugin action '{}' executed successfully",
1575                                    action_name
1576                                );
1577                            }
1578                            Err(e) => {
1579                                tracing::error!("Plugin action '{}' error: {}", action_name, e);
1580                            }
1581                        }
1582                        false // Remove completed action
1583                    }
1584                    Err(std::sync::mpsc::TryRecvError::Empty) => {
1585                        true // Keep pending action
1586                    }
1587                    Err(std::sync::mpsc::TryRecvError::Disconnected) => {
1588                        tracing::error!(
1589                            "Plugin thread disconnected during action '{}'",
1590                            action_name
1591                        );
1592                        false // Remove disconnected action
1593                    }
1594                }
1595            });
1596    }
1597
1598    /// True iff no plugin actions are currently in-flight on the
1599    /// plugin thread. Test harness helper — used by `send_key` to
1600    /// know when async plugin work queued by the key has fully
1601    /// settled before returning, so tests see synchronous-looking
1602    /// behavior between sequential key presses (e.g. a mode-bound
1603    /// `Home` followed by a synchronous-bypass `Shift+Right`).
1604    /// Outside tests, the editor's main loop pumps these alongside
1605    /// other async messages on every frame so there's nothing to
1606    /// drain explicitly.
1607    #[cfg(feature = "plugins")]
1608    #[doc(hidden)]
1609    pub fn pending_plugin_actions_is_empty(&self) -> bool {
1610        self.pending_plugin_actions.is_empty()
1611    }
1612
1613    /// Stub for builds without plugin support — there are no
1614    /// plugin actions to track, so we're always "settled".
1615    #[cfg(not(feature = "plugins"))]
1616    #[doc(hidden)]
1617    pub fn pending_plugin_actions_is_empty(&self) -> bool {
1618        true
1619    }
1620
1621    /// Process pending LSP server restarts (with exponential backoff)
1622    pub(super) fn process_pending_lsp_restarts(&mut self) {
1623        let __active_id = self.active_window;
1624        let Some(lsp) = self
1625            .windows
1626            .get_mut(&__active_id)
1627            .and_then(|w| w.lsp.as_mut())
1628        else {
1629            return;
1630        };
1631
1632        let restart_results = lsp.process_pending_restarts();
1633
1634        for (language, success, message) in restart_results {
1635            self.active_window_mut().status_message = Some(message.clone());
1636
1637            if success {
1638                self.resend_did_open_for_language(&language);
1639            }
1640        }
1641    }
1642
1643    /// Re-send didOpen notifications for all buffers of a given language
1644    pub(super) fn resend_did_open_for_language(&mut self, language: &str) {
1645        // Find all open buffers for this language using stored buffer language
1646        let buffers_for_language: Vec<_> = self
1647            .buffers()
1648            .iter()
1649            .filter_map(|(buf_id, state)| {
1650                if state.language == language {
1651                    self.active_window()
1652                        .buffer_metadata
1653                        .get(buf_id)
1654                        .and_then(|meta| meta.file_path().map(|p| (*buf_id, p.clone())))
1655                } else {
1656                    None
1657                }
1658            })
1659            .collect();
1660
1661        // Re-send didOpen for each buffer
1662        for (buffer_id, path) in buffers_for_language {
1663            if let Some(state) = self
1664                .windows
1665                .get(&self.active_window)
1666                .map(|w| &w.buffers)
1667                .expect("active window present")
1668                .get(&buffer_id)
1669            {
1670                let content = match state.buffer.to_string() {
1671                    Some(c) => c,
1672                    None => continue, // Skip buffers that aren't fully loaded
1673                };
1674                let uri: Option<lsp_types::Uri> =
1675                    super::types::file_path_to_lsp_uri_with_translation(
1676                        &path,
1677                        self.authority.path_translation.as_ref(),
1678                    );
1679
1680                if let Some(uri) = uri {
1681                    let lang_id = state.language.clone();
1682                    let __active_id = self.active_window;
1683                    if let Some(__win) = self.windows.get_mut(&__active_id) {
1684                        if let Some(lsp) = __win.lsp.as_mut() {
1685                            // Send didOpen to ALL handles for this language,
1686                            // not just the first. Each server needs its own
1687                            // didOpen notification.
1688                            for sh in lsp.get_handles_mut(&lang_id) {
1689                                let handle_id = sh.handle.id();
1690                                if let Err(e) = sh.handle.did_open(
1691                                    uri.clone(),
1692                                    content.clone(),
1693                                    lang_id.clone(),
1694                                ) {
1695                                    tracing::warn!(
1696                                        "LSP did_open failed for '{}' after restart: {}",
1697                                        sh.name,
1698                                        e
1699                                    );
1700                                } else if let Some(metadata) =
1701                                    __win.buffer_metadata.get_mut(&buffer_id)
1702                                {
1703                                    // Mark buffer as opened with this handle
1704                                    // so send_lsp_changes_for_buffer doesn't
1705                                    // re-send didOpen.
1706                                    metadata.lsp_opened_with.insert(handle_id);
1707                                }
1708                            }
1709                        }
1710                    }
1711                }
1712            }
1713        }
1714    }
1715
1716    /// Request semantic tokens for all open buffers matching a language.
1717    pub(super) fn request_semantic_tokens_for_language(&mut self, language: &str) {
1718        let buffer_ids: Vec<_> = self
1719            .buffers_for_language(language)
1720            .into_iter()
1721            .map(|(id, _)| id)
1722            .collect();
1723        for buffer_id in buffer_ids {
1724            self.active_window_mut()
1725                .schedule_semantic_tokens_full_refresh(buffer_id);
1726        }
1727    }
1728
1729    /// Request folding ranges for all open buffers matching a language.
1730    pub(super) fn request_folding_ranges_for_language(&mut self, language: &str) {
1731        let buffer_ids: Vec<_> = self
1732            .buffers_for_language(language)
1733            .into_iter()
1734            .map(|(id, _)| id)
1735            .collect();
1736        for buffer_id in buffer_ids {
1737            self.active_window_mut()
1738                .schedule_folding_ranges_refresh(buffer_id);
1739        }
1740    }
1741
1742    /// Request inlay hints for all open buffers matching a language.
1743    ///
1744    /// Used on `LspInitialized` so buffers that opened before the server
1745    /// finished its `initialize` handshake still receive hints once
1746    /// capabilities are known. Per-buffer requests route through
1747    /// `handle_for_feature_mut(InlayHints)`, so servers that didn't
1748    /// advertise `inlayHintProvider` are transparently skipped.
1749    pub(super) fn request_inlay_hints_for_language(&mut self, language: &str) {
1750        let buffer_ids: Vec<_> = self
1751            .buffers_for_language(language)
1752            .into_iter()
1753            .map(|(id, _)| id)
1754            .collect();
1755        for buffer_id in buffer_ids {
1756            self.request_inlay_hints_for_buffer(buffer_id);
1757        }
1758    }
1759}
1760
1761fn semantic_tokens_to_raw(tokens: &[SemanticToken]) -> Vec<u32> {
1762    let mut raw = Vec::with_capacity(tokens.len().saturating_mul(5));
1763    for token in tokens {
1764        raw.push(token.delta_line);
1765        raw.push(token.delta_start);
1766        raw.push(token.length);
1767        raw.push(token.token_type);
1768        raw.push(token.token_modifiers_bitset);
1769    }
1770    raw
1771}
1772
1773fn decode_semantic_token_raw_data(
1774    buffer: &Buffer,
1775    legend: &SemanticTokensLegend,
1776    data: &[u32],
1777    base_line: usize,
1778) -> Vec<SemanticTokenSpan> {
1779    if !data.len().is_multiple_of(5) {
1780        tracing::warn!(
1781            "Semantic token data length {} is not divisible by 5",
1782            data.len()
1783        );
1784        return Vec::new();
1785    }
1786
1787    let mut result = Vec::with_capacity(data.len() / 5);
1788    let mut current_line = base_line as u32;
1789    let mut current_start = 0u32;
1790
1791    for chunk in data.chunks_exact(5) {
1792        let delta_line = chunk[0];
1793        let delta_start = chunk[1];
1794        let length = chunk[2];
1795        let token_type = chunk[3];
1796        let token_modifiers_bitset = chunk[4];
1797
1798        current_line += delta_line;
1799        if delta_line == 0 {
1800            current_start += delta_start;
1801        } else {
1802            current_start = delta_start;
1803        }
1804
1805        let start_utf16 = current_start as usize;
1806        let end_utf16 = start_utf16 + length as usize;
1807        let start_byte = buffer.lsp_position_to_byte(current_line as usize, start_utf16);
1808        let end_byte = buffer.lsp_position_to_byte(current_line as usize, end_utf16);
1809
1810        let token_type_name = legend
1811            .token_types
1812            .get(token_type as usize)
1813            .map(|ty| ty.as_str().to_string())
1814            .unwrap_or_else(|| "unknown".to_string());
1815
1816        let mut modifiers = Vec::new();
1817        for (idx, modifier) in legend.token_modifiers.iter().enumerate() {
1818            if (token_modifiers_bitset >> idx) & 1 == 1 {
1819                modifiers.push(modifier.as_str().to_string());
1820            }
1821        }
1822
1823        result.push(SemanticTokenSpan {
1824            range: start_byte..end_byte,
1825            token_type: token_type_name,
1826            modifiers,
1827        });
1828    }
1829
1830    result
1831}
1832
1833struct SemanticTokenDecode {
1834    raw: Vec<u32>,
1835    spans: Vec<SemanticTokenSpan>,
1836}
1837
1838struct SemanticTokensFullDecode {
1839    result_id: Option<String>,
1840    raw_data: Vec<u32>,
1841    spans: Vec<SemanticTokenSpan>,
1842}
1843
1844struct SemanticTokensDeltaDecode {
1845    result_id: Option<String>,
1846    raw_data: Vec<u32>,
1847}
1848
1849fn decode_semantic_token_data(
1850    buffer: &Buffer,
1851    legend: &SemanticTokensLegend,
1852    data: &[SemanticToken],
1853    base_line: usize,
1854) -> SemanticTokenDecode {
1855    let raw = semantic_tokens_to_raw(data);
1856    let spans = decode_semantic_token_raw_data(buffer, legend, &raw, base_line);
1857    SemanticTokenDecode { raw, spans }
1858}
1859
1860fn apply_semantic_token_edits(
1861    mut data: Vec<u32>,
1862    edits: &[SemanticTokensEdit],
1863) -> Option<Vec<u32>> {
1864    if edits.is_empty() {
1865        return Some(data);
1866    }
1867
1868    for edit in edits.iter().rev() {
1869        let start = edit.start as usize;
1870        let delete_count = edit.delete_count as usize;
1871        if start > data.len() || start.saturating_add(delete_count) > data.len() {
1872            return None;
1873        }
1874
1875        let insert = edit
1876            .data
1877            .as_ref()
1878            .map(|tokens| semantic_tokens_to_raw(tokens))
1879            .unwrap_or_default();
1880
1881        data.splice(start..start + delete_count, insert);
1882    }
1883
1884    Some(data)
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889    use super::*;
1890
1891    #[test]
1892    fn semantic_token_delta_edits_apply() {
1893        let base = vec![0, 0, 2, 0, 0, 0, 3, 4, 1, 0];
1894        let edit = SemanticTokensEdit {
1895            start: 5,
1896            delete_count: 5,
1897            data: Some(vec![SemanticToken {
1898                delta_line: 0,
1899                delta_start: 5,
1900                length: 1,
1901                token_type: 2,
1902                token_modifiers_bitset: 0,
1903            }]),
1904        };
1905
1906        let updated = apply_semantic_token_edits(base, &[edit]).expect("edit should apply");
1907        assert_eq!(updated.len(), 10);
1908        assert_eq!(&updated[5..10], &[0, 5, 1, 2, 0]);
1909    }
1910}