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 lsp = &mut __win.lsp;
849
850        // LSP should already be running since we got a quiescent notification
851        let Some(sh) = lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
852        else {
853            return;
854        };
855        let client = &mut sh.handle;
856
857        let __next_id = &mut __win.next_lsp_request_id;
858        let __pending = &mut __win.pending_inlay_hints_requests;
859
860        // Request inlay hints for each buffer. Each request is keyed in
861        // the pending map by its own id (and carries buffer_id + version)
862        // so responses across all buffers are matched individually — a
863        // single Option used to be overwritten by each iteration, dropping
864        // every response except the last.
865        for (buffer_id, uri, line_count, version) in buffer_infos {
866            let request_id = *__next_id;
867            *__next_id += 1;
868
869            let last_line = line_count.saturating_sub(1) as u32;
870            if let Err(e) =
871                client.inlay_hints(request_id, uri.as_uri().clone(), 0, 0, last_line, 10000)
872            {
873                tracing::debug!(
874                    "Failed to re-request inlay hints for {}: {}",
875                    uri.as_str(),
876                    e
877                );
878            } else {
879                __pending.insert(request_id, super::InlayHintsRequest { buffer_id, version });
880                tracing::info!(
881                    "Re-requested inlay hints for {} (request_id={})",
882                    uri.as_str(),
883                    request_id
884                );
885            }
886        }
887
888        // Folding ranges may improve after project is fully loaded
889        self.request_folding_ranges_for_language(&language);
890    }
891
892    /// Handle workspace/diagnostic/refresh request from the LSP server.
893    /// Re-pulls diagnostics for all open documents of the given language.
894    pub(super) fn handle_lsp_diagnostic_refresh(&mut self, language: String) {
895        tracing::info!(
896            "LSP ({}) diagnostic refresh requested, re-pulling diagnostics",
897            language
898        );
899        self.pull_diagnostics_for_language(&language);
900    }
901
902    pub(super) fn handle_lsp_inlay_hint_refresh(&mut self, language: String) {
903        tracing::info!(
904            "LSP ({}) inlay-hint refresh requested, re-pulling inlay hints",
905            language
906        );
907        self.request_inlay_hints_for_language(&language);
908    }
909
910    pub(super) fn handle_lsp_semantic_tokens_refresh(&mut self, language: String) {
911        tracing::info!(
912            "LSP ({}) semantic-tokens refresh requested, re-pulling semantic tokens",
913            language
914        );
915        self.request_semantic_tokens_for_language(&language);
916    }
917
918    /// Apply a dynamic capability (un)registration from the server, then — when
919    /// a capability newly turned on — kick off the corresponding requests for
920    /// buffers that were already open before the registration arrived (they
921    /// would otherwise never be requested, mirroring `LspInitialized`).
922    pub(super) fn handle_lsp_dynamic_capabilities(
923        &mut self,
924        language: String,
925        server_name: String,
926        register: bool,
927        registrations: Vec<(String, Option<serde_json::Value>)>,
928    ) {
929        tracing::info!(
930            "LSP ({}) server '{}' {} {} capability registration(s)",
931            language,
932            server_name,
933            if register {
934                "registered"
935            } else {
936                "unregistered"
937            },
938            registrations.len()
939        );
940
941        let __active_id = self.active_window;
942        let changed = self
943            .windows
944            .get_mut(&__active_id)
945            .map(|w| &mut w.lsp)
946            .is_some_and(|lsp| {
947                lsp.apply_dynamic_capabilities(&server_name, register, &registrations)
948            });
949
950        // Only re-issue requests on a net-new capability; an unregister or a
951        // no-op registration should not trigger a fresh round of requests.
952        if changed && register {
953            self.request_semantic_tokens_for_language(&language);
954            self.request_folding_ranges_for_language(&language);
955            self.request_inlay_hints_for_language(&language);
956            self.pull_diagnostics_for_language(&language);
957        }
958    }
959
960    /// Re-pull diagnostics for all open buffers associated with the given language.
961    pub(super) fn pull_diagnostics_for_language(&mut self, language: &str) {
962        // Use the shared language-filtered buffer enumeration so requests
963        // never leak out to a server with a different scope.
964        let uris: Vec<_> = self
965            .buffers_for_language(language)
966            .into_iter()
967            .map(|(_, uri)| uri)
968            .collect();
969
970        if uris.is_empty() {
971            return;
972        }
973
974        let __active_id = self.active_window;
975        let Some(__win) = self.windows.get_mut(&__active_id) else {
976            return;
977        };
978        let diagnostic_result_ids = &__win.diagnostic_result_ids;
979        let lsp = &mut __win.lsp;
980        let Some(sh) = lsp.handle_for_feature_mut(language, crate::types::LspFeature::Diagnostics)
981        else {
982            return;
983        };
984        let client = &mut sh.handle;
985        let __next_id = &mut __win.next_lsp_request_id;
986
987        for uri in uris {
988            let request_id = *__next_id;
989            *__next_id += 1;
990            let previous_result_id = diagnostic_result_ids.get(uri.as_str()).cloned();
991            if let Err(e) =
992                client.document_diagnostic(request_id, uri.as_uri().clone(), previous_result_id)
993            {
994                tracing::debug!("Failed to re-pull diagnostics for {}: {}", uri.as_str(), e);
995            } else {
996                tracing::info!(
997                    "Re-pulling diagnostics for {} (request_id={})",
998                    uri.as_str(),
999                    request_id
1000                );
1001            }
1002        }
1003    }
1004
1005    /// Handle LSP progress notification ($/progress)
1006    pub(super) fn handle_lsp_progress(
1007        &mut self,
1008        language: String,
1009        token: String,
1010        value: LspProgressValue,
1011    ) {
1012        match value {
1013            LspProgressValue::Begin {
1014                title,
1015                message,
1016                percentage,
1017            } => {
1018                self.active_window_mut().lsp_progress.insert(
1019                    token.clone(),
1020                    LspProgressInfo {
1021                        language,
1022                        title,
1023                        message,
1024                        percentage,
1025                    },
1026                );
1027            }
1028            LspProgressValue::Report {
1029                message,
1030                percentage,
1031            } => {
1032                if let Some(info) = self.active_window_mut().lsp_progress.get_mut(&token) {
1033                    info.message = message;
1034                    info.percentage = percentage;
1035                }
1036            }
1037            LspProgressValue::End { .. } => {
1038                self.active_window_mut().lsp_progress.remove(&token);
1039            }
1040        }
1041        // If the LSP status popup is open, rebuild it so the progress line
1042        // inside reflects the new title / message / percentage.  The
1043        // status-bar indicator itself only shows a spinner, so the popup
1044        // is the user's only window into the live progress text.
1045        self.refresh_lsp_status_popup_if_open();
1046    }
1047
1048    /// Handle LSP window message (window/showMessage)
1049    pub(super) fn handle_lsp_window_message(
1050        &mut self,
1051        language: String,
1052        message_type: LspMessageType,
1053        message: String,
1054    ) {
1055        // Add to window messages list
1056        self.active_window_mut()
1057            .lsp_window_messages
1058            .push(LspMessageEntry {
1059                language: language.clone(),
1060                message_type,
1061                message: message.clone(),
1062                timestamp: Instant::now(),
1063            });
1064
1065        // Keep only last 100 messages
1066        if self.active_window_mut().lsp_window_messages.len() > 100 {
1067            self.active_window_mut().lsp_window_messages.remove(0);
1068        }
1069
1070        // Show important messages in status bar
1071        match message_type {
1072            LspMessageType::Error | LspMessageType::Warning => {
1073                self.active_window_mut().status_message =
1074                    Some(format!("LSP ({}): {}", language, message));
1075            }
1076            _ => {
1077                // Info and Log messages are not shown in status bar
1078            }
1079        }
1080    }
1081
1082    /// Handle LSP log message (window/logMessage)
1083    pub(super) fn handle_lsp_log_message(
1084        &mut self,
1085        language: String,
1086        message_type: LspMessageType,
1087        message: String,
1088    ) {
1089        self.active_window_mut()
1090            .lsp_log_messages
1091            .push(LspMessageEntry {
1092                language,
1093                message_type,
1094                message,
1095                timestamp: Instant::now(),
1096            });
1097
1098        // Keep only last 500 log messages
1099        if self.active_window_mut().lsp_log_messages.len() > 500 {
1100            self.active_window_mut().lsp_log_messages.remove(0);
1101        }
1102    }
1103
1104    /// Handle LSP server status update
1105    pub(super) fn handle_lsp_status_update(
1106        &mut self,
1107        language: String,
1108        server_name: String,
1109        status: LspServerStatus,
1110    ) {
1111        use crate::services::async_bridge::LspServerStatus;
1112
1113        let server_name_ref = server_name.clone();
1114        let key = (language.clone(), server_name);
1115
1116        // Get old status for event
1117        let old_status = self
1118            .active_window_mut()
1119            .lsp_server_statuses
1120            .get(&key)
1121            .cloned();
1122
1123        // Update server status
1124        self.active_window_mut()
1125            .lsp_server_statuses
1126            .insert(key, status);
1127
1128        // Update warning domain for LSP status indicator
1129        self.active_window_mut().update_lsp_warning_domain();
1130
1131        // When a server becomes ready, send didOpen for all open buffers of
1132        // that language so the server can start providing diagnostics, etc.
1133        // without waiting for the next user edit.
1134        if status == LspServerStatus::Running {
1135            let was_already_running = old_status
1136                .as_ref()
1137                .is_some_and(|s| matches!(s, LspServerStatus::Running));
1138            if !was_already_running {
1139                let scope = self
1140                    .lsp()
1141                    .as_ref()
1142                    .and_then(|lsp| lsp.server_scope(&server_name_ref).cloned());
1143                match scope {
1144                    Some(scope) if scope.is_universal() => {
1145                        let languages: Vec<String> =
1146                            self.buffers().languages().into_iter().collect();
1147                        for lang in languages {
1148                            self.reopen_buffers_for_language(&lang);
1149                        }
1150                    }
1151                    Some(scope) => {
1152                        for lang in scope.languages() {
1153                            self.reopen_buffers_for_language(lang);
1154                        }
1155                    }
1156                    None => {
1157                        // Per-language server — language comes from the status message
1158                        self.reopen_buffers_for_language(&language);
1159                    }
1160                }
1161            }
1162        }
1163
1164        // Handle server crash - trigger auto-restart
1165        if status == LspServerStatus::Error {
1166            let was_running = old_status
1167                .as_ref()
1168                .map(|s| matches!(s, LspServerStatus::Running | LspServerStatus::Initializing))
1169                .unwrap_or(false);
1170
1171            if was_running {
1172                // Clear stale diagnostics from the crashed server so they
1173                // don't linger on screen while we wait for a restart.
1174                self.clear_diagnostics_for_server(&server_name_ref);
1175
1176                let __active_id = self.active_window;
1177
1178                if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1179                    let message = lsp.handle_server_crash(&language, &server_name_ref);
1180                    self.active_window_mut().status_message = Some(message);
1181                }
1182            }
1183        }
1184
1185        // When a server transitions to Error or Shutdown, drop any
1186        // `$/progress` entries for this language if no other server is
1187        // still alive for it. The dead server will never emit the
1188        // matching `end` notification, so without this prune the
1189        // status-bar spinner stays stuck on the rotating braille glyph
1190        // (and the status popup keeps showing "Indexing …") even
1191        // though the process is gone — that's the "popup still says
1192        // indexing after external kill" user report.
1193        if matches!(status, LspServerStatus::Error | LspServerStatus::Shutdown) {
1194            let any_running_for_lang =
1195                self.active_window()
1196                    .lsp_server_statuses
1197                    .iter()
1198                    .any(|((lang, _), s)| {
1199                        lang == &language
1200                            && !matches!(s, LspServerStatus::Error | LspServerStatus::Shutdown,)
1201                    });
1202            if !any_running_for_lang {
1203                let lang_owned = language.clone();
1204                self.active_window_mut()
1205                    .lsp_progress
1206                    .retain(|_, info| info.language != lang_owned);
1207            }
1208            // Refresh the popup so any in-progress "(ready) ⏳ Indexing"
1209            // rows for this server flip to "(not running)" right away.
1210            self.refresh_lsp_status_popup_if_open();
1211        }
1212
1213        // Emit control event
1214        let status_str = match status {
1215            LspServerStatus::Starting => "starting",
1216            LspServerStatus::Initializing => "initializing",
1217            LspServerStatus::Running => "running",
1218            LspServerStatus::Error => "error",
1219            LspServerStatus::Shutdown => "shutdown",
1220        };
1221        let old_status_str = old_status
1222            .map(|s| match s {
1223                LspServerStatus::Starting => "starting",
1224                LspServerStatus::Initializing => "initializing",
1225                LspServerStatus::Running => "running",
1226                LspServerStatus::Error => "error",
1227                LspServerStatus::Shutdown => "shutdown",
1228            })
1229            .unwrap_or("none");
1230
1231        self.emit_event(
1232            crate::model::control_event::events::LSP_STATUS_CHANGED.name,
1233            serde_json::json!({
1234                "language": language,
1235                "old_status": old_status_str,
1236                "status": status_str
1237            }),
1238        );
1239    }
1240
1241    /// Handle custom LSP notification
1242    #[allow(dead_code)] // Prepared for future use when AsyncMessage::LspCustomNotification is added
1243    pub(super) fn handle_custom_notification(
1244        &mut self,
1245        language: String,
1246        method: String,
1247        params: Option<Value>,
1248    ) {
1249        tracing::debug!("Custom LSP notification {} from {}", method, language);
1250        let payload = serde_json::json!({
1251            "language": language,
1252            "method": method,
1253            "params": params,
1254        });
1255        self.emit_event("lsp/custom_notification", payload);
1256    }
1257
1258    /// Handle LSP server request (server -> client)
1259    /// These are requests from the LSP server that require handling, typically
1260    /// custom/extension methods specific to certain language servers.
1261    pub(super) fn handle_lsp_server_request(
1262        &mut self,
1263        language: String,
1264        server_command: String,
1265        method: String,
1266        params: Option<Value>,
1267    ) {
1268        tracing::debug!(
1269            "LSP server request {} from {} ({})",
1270            method,
1271            language,
1272            server_command
1273        );
1274
1275        // Convert params to JSON string for the hook
1276        let params_str = params.map(|p| p.to_string());
1277
1278        // Run the lsp_server_request hook for plugins
1279        self.plugin_manager.read().unwrap().run_hook(
1280            "lsp_server_request",
1281            crate::services::plugins::hooks::HookArgs::LspServerRequest {
1282                language,
1283                method,
1284                server_command,
1285                params: params_str,
1286            },
1287        );
1288    }
1289
1290    /// Handle plugin LSP response
1291    pub(super) fn handle_plugin_lsp_response(
1292        &mut self,
1293        request_id: u64,
1294        result: Result<Value, String>,
1295    ) {
1296        use fresh_core::api::JsCallbackId;
1297        tracing::debug!("Received plugin LSP response (request_id={})", request_id);
1298        let callback_id = JsCallbackId::from(request_id);
1299        match result {
1300            Ok(value) => {
1301                self.plugin_manager
1302                    .read()
1303                    .unwrap()
1304                    .resolve_callback(callback_id, value.to_string());
1305            }
1306            Err(err) => {
1307                self.plugin_manager
1308                    .read()
1309                    .unwrap()
1310                    .reject_callback(callback_id, err);
1311            }
1312        }
1313    }
1314
1315    /// Handle generic plugin response (e.g., GetBufferText result)
1316    pub(super) fn handle_plugin_response(&mut self, response: fresh_core::api::PluginResponse) {
1317        tracing::debug!("Received plugin response: {:?}", response);
1318        self.send_plugin_response(response);
1319    }
1320}
1321
1322// =============================================================================
1323// File System Event Handlers
1324// =============================================================================
1325
1326impl Editor {
1327    /// Handle file changed externally notification (from AsyncMessage)
1328    ///
1329    /// Includes debounce logic to prevent rapid auto-reverts from overwhelming the editor.
1330    /// This is different from `handle_file_changed` which actually reloads the file.
1331    pub(super) fn handle_async_file_changed(&mut self, path: String) -> bool {
1332        const DEBOUNCE_WINDOW: Duration = Duration::from_secs(10);
1333        const RAPID_REVERT_THRESHOLD: u32 = 10; // Require 10 reverts in 10 seconds to disable
1334
1335        // Skip if auto-revert is disabled
1336        if !self.active_window().auto_revert_enabled {
1337            return false;
1338        }
1339
1340        let path_buf = PathBuf::from(&path);
1341
1342        // Only track events for files that are actually open in the editor
1343        let is_file_open = self
1344            .buffers()
1345            .iter()
1346            .any(|(_, state)| state.buffer.file_path() == Some(&path_buf));
1347
1348        if !is_file_open {
1349            tracing::trace!("Ignoring file change event for non-open file: {}", path);
1350            return false;
1351        }
1352
1353        // Track rapid file change events - only disable after many reverts in short window
1354        let mut should_disable = false;
1355        let now = self.time_source.now();
1356        let elapsed_window_ok = if let Some((window_start, _)) =
1357            self.active_window().file_rapid_change_counts.get(&path_buf)
1358        {
1359            self.time_source.elapsed_since(*window_start) < DEBOUNCE_WINDOW
1360        } else {
1361            false
1362        };
1363        if let Some((window_start, count)) = self
1364            .active_window_mut()
1365            .file_rapid_change_counts
1366            .get_mut(&path_buf)
1367        {
1368            if elapsed_window_ok {
1369                *count += 1;
1370
1371                if *count >= RAPID_REVERT_THRESHOLD {
1372                    should_disable = true;
1373                    tracing::info!(
1374                        "Auto-revert disabled for {:?} ({} reverts in {:?})",
1375                        path_buf,
1376                        count,
1377                        DEBOUNCE_WINDOW
1378                    );
1379                }
1380            } else {
1381                // Reset counter - start a new window
1382                *count = 1;
1383                *window_start = now;
1384            }
1385        } else {
1386            // First event for this file
1387            let now = self.time_source.now();
1388            self.active_window_mut()
1389                .file_rapid_change_counts
1390                .insert(path_buf.clone(), (now, 1));
1391        }
1392        if should_disable {
1393            self.active_window_mut().auto_revert_enabled = false;
1394            self.active_window_mut().status_message = Some(format!(
1395                "Auto-revert disabled: {} is updating too frequently (use Ctrl+Shift+R to re-enable)",
1396                path_buf.file_name().unwrap_or_default().to_string_lossy()
1397            ));
1398            return false;
1399        }
1400
1401        tracing::info!("File changed externally: {}", path);
1402        self.handle_file_changed(&path);
1403        true
1404    }
1405}
1406
1407// =============================================================================
1408// File Explorer Handlers
1409// =============================================================================
1410
1411impl Editor {
1412    /// Handle file explorer initialized
1413    pub(super) fn handle_file_explorer_initialized(
1414        &mut self,
1415        window: fresh_core::WindowId,
1416        view: FileTreeView,
1417    ) {
1418        tracing::info!("File explorer initialized for window {window}");
1419        let defaults = crate::app::file_explorer::FileExplorerViewDefaults {
1420            show_hidden: self.config.file_explorer.show_hidden,
1421            show_gitignored: self.config.file_explorer.show_gitignored,
1422            compact_directories: self.config.file_explorer.compact_directories,
1423        };
1424        let is_active = window == self.active_window_id();
1425        // Route the result back to the window that asked for it. If that window
1426        // is gone (closed before its tree finished building), drop it. The
1427        // window applies the view to itself, so a background-built tree can
1428        // never clobber a different (active) window's explorer.
1429        let Some(win) = self.windows.get_mut(&window) else {
1430            return;
1431        };
1432        win.install_initialized_file_explorer(view, defaults);
1433        if is_active {
1434            self.set_status_message(t!("status.file_explorer_ready").to_string());
1435        }
1436    }
1437
1438    /// Handle file explorer node toggle completed
1439    pub(super) fn handle_file_explorer_toggle_node(&mut self, node_id: NodeId) {
1440        tracing::debug!("File explorer toggle completed for node {:?}", node_id);
1441    }
1442
1443    /// Handle file explorer node refresh completed
1444    pub(super) fn handle_file_explorer_refresh_node(&mut self, node_id: NodeId) {
1445        tracing::debug!("File explorer refresh completed for node {:?}", node_id);
1446        self.set_status_message(t!("explorer.refreshed_default").to_string());
1447    }
1448
1449    /// Handle file explorer expanded to path
1450    pub(super) fn handle_file_explorer_expanded_to_path(
1451        &mut self,
1452        window: fresh_core::WindowId,
1453        view: FileTreeView,
1454    ) {
1455        tracing::trace!(
1456            "handle_file_explorer_expanded_to_path: restoring file_explorer for window {window}"
1457        );
1458        // Route to the requesting window (see `handle_file_explorer_initialized`).
1459        if let Some(win) = self.windows.get_mut(&window) {
1460            win.install_expanded_file_explorer(view);
1461        }
1462    }
1463}
1464
1465// =============================================================================
1466// Plugin Handlers
1467// =============================================================================
1468
1469impl Editor {
1470    /// Handle plugin process output completion
1471    pub(super) fn handle_plugin_process_output(
1472        &mut self,
1473        callback_id: fresh_core::api::JsCallbackId,
1474        stdout: String,
1475        stderr: String,
1476        exit_code: i32,
1477    ) {
1478        tracing::debug!(
1479            "Process {} completed: exit_code={}, stdout_len={}, stderr_len={}",
1480            callback_id,
1481            exit_code,
1482            stdout.len(),
1483            stderr.len()
1484        );
1485        // Resolve the plugin callback with the process output
1486        // Using SpawnResult struct ensures field names match TypeScript types
1487        let result = fresh_core::api::SpawnResult {
1488            stdout,
1489            stderr,
1490            exit_code,
1491        };
1492        self.plugin_manager
1493            .read()
1494            .unwrap()
1495            .resolve_callback(callback_id, serde_json::to_string(&result).unwrap());
1496    }
1497
1498    /// Process TypeScript plugin commands
1499    ///
1500    /// Returns true if any visual commands were processed (i.e. a re-render is needed).
1501    /// No-op sentinels like `HookCompleted` do not count.
1502    #[cfg(feature = "plugins")]
1503    pub(super) fn process_plugin_commands(&mut self) -> bool {
1504        let commands = self.plugin_manager.write().unwrap().process_commands();
1505        if commands.is_empty() {
1506            return false;
1507        }
1508
1509        // Classify each command as visual (needs re-render) or not.
1510        // `HookCompleted` is a pure ack. `SetStatusBarValue` is treated as
1511        // visual only when the value actually differs from what's stored —
1512        // many plugins (e.g. git_statusbar) re-publish the same value on
1513        // every `render_start` hook, which would otherwise create a
1514        // render → hook → ack → render feedback loop at ~13Hz forever.
1515        //
1516        // The remaining `=> false` arms are side-effecting commands that
1517        // never touch the rendered buffer: scheduling a timer, spawning
1518        // processes / HTTP, watching paths, and writing plugin-private
1519        // state. Any *visual* result they eventually produce arrives as its
1520        // own command (overlay, virtual text, status value, …) and is
1521        // counted then. Treating these as visual forced a redraw on every
1522        // debounce tick — e.g. live_diff's 75ms `editor.delay()` recompute
1523        // repainted the screen twice per keystroke with no change. Invisible
1524        // on a fast terminal, but real lag over a serial console (#2100).
1525        use fresh_core::api::PluginCommand as Pc;
1526        let has_visual_commands = commands.iter().any(|c| match c {
1527            Pc::HookCompleted { .. }
1528            | Pc::Delay { .. }
1529            | Pc::SpawnProcess { .. }
1530            | Pc::SpawnBackgroundProcess { .. }
1531            | Pc::KillBackgroundProcess { .. }
1532            | Pc::SpawnProcessWait { .. }
1533            | Pc::HttpFetch { .. }
1534            | Pc::WatchPath { .. }
1535            | Pc::UnwatchPath { .. }
1536            | Pc::SetGlobalState { .. }
1537            | Pc::SetWindowState { .. }
1538            | Pc::SetViewState { .. } => false,
1539            Pc::SetStatusBarValue {
1540                buffer_id,
1541                key,
1542                value,
1543            } => {
1544                self.current_status_bar_value(fresh_core::BufferId(*buffer_id as usize), key)
1545                    != Some(value.as_str())
1546            }
1547            _ => true,
1548        });
1549
1550        let cmd_names: Vec<String> = commands.iter().map(|c| c.debug_variant_name()).collect();
1551        tracing::trace!(
1552            count = commands.len(),
1553            cmds = ?cmd_names,
1554            "process_plugin_commands"
1555        );
1556
1557        for command in &commands {
1558            match command {
1559                fresh_core::api::PluginCommand::RegisterGrammar {
1560                    language,
1561                    grammar_path,
1562                    extensions,
1563                } => {
1564                    tracing::info!(
1565                        "[SYNTAX DEBUG] processing RegisterGrammar: lang='{}', path='{}', ext={:?}",
1566                        language,
1567                        grammar_path,
1568                        extensions
1569                    );
1570                }
1571                fresh_core::api::PluginCommand::ReloadGrammars { .. } => {
1572                    tracing::info!("[SYNTAX DEBUG] processing ReloadGrammars command");
1573                }
1574                _ => {}
1575            }
1576        }
1577
1578        for command in commands {
1579            tracing::trace!(
1580                "process_plugin_commands: handling command {:?}",
1581                std::mem::discriminant(&command)
1582            );
1583            if let Err(e) = self.handle_plugin_command(command) {
1584                tracing::error!("Error handling TypeScript plugin command: {}", e);
1585            }
1586        }
1587
1588        // Flush any deferred grammar rebuilds as a single batch
1589        self.flush_pending_grammars();
1590
1591        has_visual_commands
1592    }
1593
1594    /// Process pending plugin action completions
1595    #[cfg(feature = "plugins")]
1596    pub(super) fn process_pending_plugin_actions(&mut self) {
1597        self.pending_plugin_actions
1598            .retain(|(action_name, receiver)| {
1599                match receiver.try_recv() {
1600                    Ok(result) => {
1601                        match result {
1602                            Ok(()) => {
1603                                tracing::info!(
1604                                    "Plugin action '{}' executed successfully",
1605                                    action_name
1606                                );
1607                            }
1608                            Err(e) => {
1609                                tracing::error!("Plugin action '{}' error: {}", action_name, e);
1610                            }
1611                        }
1612                        false // Remove completed action
1613                    }
1614                    Err(std::sync::mpsc::TryRecvError::Empty) => {
1615                        true // Keep pending action
1616                    }
1617                    Err(std::sync::mpsc::TryRecvError::Disconnected) => {
1618                        tracing::error!(
1619                            "Plugin thread disconnected during action '{}'",
1620                            action_name
1621                        );
1622                        false // Remove disconnected action
1623                    }
1624                }
1625            });
1626    }
1627
1628    /// True iff no plugin actions are currently in-flight on the
1629    /// plugin thread. Test harness helper — used by `send_key` to
1630    /// know when async plugin work queued by the key has fully
1631    /// settled before returning, so tests see synchronous-looking
1632    /// behavior between sequential key presses (e.g. a mode-bound
1633    /// `Home` followed by a synchronous-bypass `Shift+Right`).
1634    /// Outside tests, the editor's main loop pumps these alongside
1635    /// other async messages on every frame so there's nothing to
1636    /// drain explicitly.
1637    #[cfg(feature = "plugins")]
1638    #[doc(hidden)]
1639    pub fn pending_plugin_actions_is_empty(&self) -> bool {
1640        self.pending_plugin_actions.is_empty()
1641    }
1642
1643    /// Stub for builds without plugin support — there are no
1644    /// plugin actions to track, so we're always "settled".
1645    #[cfg(not(feature = "plugins"))]
1646    #[doc(hidden)]
1647    pub fn pending_plugin_actions_is_empty(&self) -> bool {
1648        true
1649    }
1650
1651    /// Process pending LSP server restarts (with exponential backoff)
1652    pub(super) fn process_pending_lsp_restarts(&mut self) {
1653        let __active_id = self.active_window;
1654        let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) else {
1655            return;
1656        };
1657
1658        let restart_results = lsp.process_pending_restarts();
1659
1660        for (language, success, message) in restart_results {
1661            self.active_window_mut().status_message = Some(message.clone());
1662
1663            if success {
1664                self.resend_did_open_for_language(&language);
1665            }
1666        }
1667    }
1668
1669    /// Re-send didOpen notifications for all buffers of a given language
1670    pub(super) fn resend_did_open_for_language(&mut self, language: &str) {
1671        // Find all open buffers for this language using stored buffer language
1672        let buffers_for_language: Vec<_> = self
1673            .buffers()
1674            .iter()
1675            .filter_map(|(buf_id, state)| {
1676                if state.language == language {
1677                    self.active_window()
1678                        .buffer_metadata
1679                        .get(buf_id)
1680                        .and_then(|meta| meta.file_path().map(|p| (*buf_id, p.clone())))
1681                } else {
1682                    None
1683                }
1684            })
1685            .collect();
1686
1687        // Re-send didOpen for each buffer
1688        for (buffer_id, path) in buffers_for_language {
1689            if let Some(state) = self
1690                .windows
1691                .get(&self.active_window)
1692                .map(|w| &w.buffers)
1693                .expect("active window present")
1694                .get(&buffer_id)
1695            {
1696                let content = match state.buffer.to_string() {
1697                    Some(c) => c,
1698                    None => continue, // Skip buffers that aren't fully loaded
1699                };
1700                let uri: Option<lsp_types::Uri> =
1701                    super::types::file_path_to_lsp_uri_with_translation(
1702                        &path,
1703                        self.authority().path_translation.as_ref(),
1704                    );
1705
1706                if let Some(uri) = uri {
1707                    let lang_id = state.language.clone();
1708                    let __active_id = self.active_window;
1709                    if let Some(__win) = self.windows.get_mut(&__active_id) {
1710                        {
1711                            let lsp = &mut __win.lsp;
1712                            // Send didOpen to ALL handles for this language,
1713                            // not just the first. Each server needs its own
1714                            // didOpen notification.
1715                            for sh in lsp.get_handles_mut(&lang_id) {
1716                                let handle_id = sh.handle.id();
1717                                if let Err(e) = sh.handle.did_open(
1718                                    uri.clone(),
1719                                    content.clone(),
1720                                    lang_id.clone(),
1721                                ) {
1722                                    tracing::warn!(
1723                                        "LSP did_open failed for '{}' after restart: {}",
1724                                        sh.name,
1725                                        e
1726                                    );
1727                                } else if let Some(metadata) =
1728                                    __win.buffer_metadata.get_mut(&buffer_id)
1729                                {
1730                                    // Mark buffer as opened with this handle
1731                                    // so send_lsp_changes_for_buffer doesn't
1732                                    // re-send didOpen.
1733                                    metadata.lsp_opened_with.insert(handle_id);
1734                                }
1735                            }
1736                        }
1737                    }
1738                }
1739            }
1740        }
1741    }
1742
1743    /// Request semantic tokens for all open buffers matching a language.
1744    pub(super) fn request_semantic_tokens_for_language(&mut self, language: &str) {
1745        let buffer_ids: Vec<_> = self
1746            .buffers_for_language(language)
1747            .into_iter()
1748            .map(|(id, _)| id)
1749            .collect();
1750        for buffer_id in buffer_ids {
1751            self.active_window_mut()
1752                .schedule_semantic_tokens_full_refresh(buffer_id);
1753        }
1754    }
1755
1756    /// Request folding ranges for all open buffers matching a language.
1757    pub(super) fn request_folding_ranges_for_language(&mut self, language: &str) {
1758        let buffer_ids: Vec<_> = self
1759            .buffers_for_language(language)
1760            .into_iter()
1761            .map(|(id, _)| id)
1762            .collect();
1763        for buffer_id in buffer_ids {
1764            self.active_window_mut()
1765                .schedule_folding_ranges_refresh(buffer_id);
1766        }
1767    }
1768
1769    /// Request inlay hints for all open buffers matching a language.
1770    ///
1771    /// Used on `LspInitialized` so buffers that opened before the server
1772    /// finished its `initialize` handshake still receive hints once
1773    /// capabilities are known. Per-buffer requests route through
1774    /// `handle_for_feature_mut(InlayHints)`, so servers that didn't
1775    /// advertise `inlayHintProvider` are transparently skipped.
1776    pub(super) fn request_inlay_hints_for_language(&mut self, language: &str) {
1777        let buffer_ids: Vec<_> = self
1778            .buffers_for_language(language)
1779            .into_iter()
1780            .map(|(id, _)| id)
1781            .collect();
1782        for buffer_id in buffer_ids {
1783            self.request_inlay_hints_for_buffer(buffer_id);
1784        }
1785    }
1786}
1787
1788fn semantic_tokens_to_raw(tokens: &[SemanticToken]) -> Vec<u32> {
1789    let mut raw = Vec::with_capacity(tokens.len().saturating_mul(5));
1790    for token in tokens {
1791        raw.push(token.delta_line);
1792        raw.push(token.delta_start);
1793        raw.push(token.length);
1794        raw.push(token.token_type);
1795        raw.push(token.token_modifiers_bitset);
1796    }
1797    raw
1798}
1799
1800fn decode_semantic_token_raw_data(
1801    buffer: &Buffer,
1802    legend: &SemanticTokensLegend,
1803    data: &[u32],
1804    base_line: usize,
1805) -> Vec<SemanticTokenSpan> {
1806    if !data.len().is_multiple_of(5) {
1807        tracing::warn!(
1808            "Semantic token data length {} is not divisible by 5",
1809            data.len()
1810        );
1811        return Vec::new();
1812    }
1813
1814    let mut result = Vec::with_capacity(data.len() / 5);
1815    let mut current_line = base_line as u32;
1816    let mut current_start = 0u32;
1817
1818    for chunk in data.chunks_exact(5) {
1819        let delta_line = chunk[0];
1820        let delta_start = chunk[1];
1821        let length = chunk[2];
1822        let token_type = chunk[3];
1823        let token_modifiers_bitset = chunk[4];
1824
1825        current_line += delta_line;
1826        if delta_line == 0 {
1827            current_start += delta_start;
1828        } else {
1829            current_start = delta_start;
1830        }
1831
1832        let start_utf16 = current_start as usize;
1833        let end_utf16 = start_utf16 + length as usize;
1834        let start_byte = buffer.lsp_position_to_byte(current_line as usize, start_utf16);
1835        let end_byte = buffer.lsp_position_to_byte(current_line as usize, end_utf16);
1836
1837        let token_type_name = legend
1838            .token_types
1839            .get(token_type as usize)
1840            .map(|ty| ty.as_str().to_string())
1841            .unwrap_or_else(|| "unknown".to_string());
1842
1843        let mut modifiers = Vec::new();
1844        for (idx, modifier) in legend.token_modifiers.iter().enumerate() {
1845            if (token_modifiers_bitset >> idx) & 1 == 1 {
1846                modifiers.push(modifier.as_str().to_string());
1847            }
1848        }
1849
1850        result.push(SemanticTokenSpan {
1851            range: start_byte..end_byte,
1852            token_type: token_type_name,
1853            modifiers,
1854        });
1855    }
1856
1857    result
1858}
1859
1860struct SemanticTokenDecode {
1861    raw: Vec<u32>,
1862    spans: Vec<SemanticTokenSpan>,
1863}
1864
1865struct SemanticTokensFullDecode {
1866    result_id: Option<String>,
1867    raw_data: Vec<u32>,
1868    spans: Vec<SemanticTokenSpan>,
1869}
1870
1871struct SemanticTokensDeltaDecode {
1872    result_id: Option<String>,
1873    raw_data: Vec<u32>,
1874}
1875
1876fn decode_semantic_token_data(
1877    buffer: &Buffer,
1878    legend: &SemanticTokensLegend,
1879    data: &[SemanticToken],
1880    base_line: usize,
1881) -> SemanticTokenDecode {
1882    let raw = semantic_tokens_to_raw(data);
1883    let spans = decode_semantic_token_raw_data(buffer, legend, &raw, base_line);
1884    SemanticTokenDecode { raw, spans }
1885}
1886
1887fn apply_semantic_token_edits(
1888    mut data: Vec<u32>,
1889    edits: &[SemanticTokensEdit],
1890) -> Option<Vec<u32>> {
1891    if edits.is_empty() {
1892        return Some(data);
1893    }
1894
1895    for edit in edits.iter().rev() {
1896        let start = edit.start as usize;
1897        let delete_count = edit.delete_count as usize;
1898        if start > data.len() || start.saturating_add(delete_count) > data.len() {
1899            return None;
1900        }
1901
1902        let insert = edit
1903            .data
1904            .as_ref()
1905            .map(|tokens| semantic_tokens_to_raw(tokens))
1906            .unwrap_or_default();
1907
1908        data.splice(start..start + delete_count, insert);
1909    }
1910
1911    Some(data)
1912}
1913
1914#[cfg(test)]
1915mod tests {
1916    use super::*;
1917
1918    #[test]
1919    fn semantic_token_delta_edits_apply() {
1920        let base = vec![0, 0, 2, 0, 0, 0, 3, 4, 1, 0];
1921        let edit = SemanticTokensEdit {
1922            start: 5,
1923            delete_count: 5,
1924            data: Some(vec![SemanticToken {
1925                delta_line: 0,
1926                delta_start: 5,
1927                length: 1,
1928                token_type: 2,
1929                token_modifiers_bitset: 0,
1930            }]),
1931        };
1932
1933        let updated = apply_semantic_token_edits(base, &[edit]).expect("edit should apply");
1934        assert_eq!(updated.len(), 10);
1935        assert_eq!(&updated[5..10], &[0, 5, 1, 2, 0]);
1936    }
1937}