Skip to main content

fresh/services/lsp/
manager.rs

1//! LSP Manager - manages multiple language servers using async I/O
2//!
3//! This module provides a manager for multiple LSP servers that:
4//! - Spawns one server per language
5//! - Uses async LspHandle for non-blocking I/O
6//! - Routes requests to appropriate servers
7//! - Configured via config.json
8
9use crate::services::async_bridge::AsyncBridge;
10use crate::services::lsp::async_handler::LspHandle;
11use crate::types::{FeatureFilter, LspFeature, LspServerConfig};
12use lsp_types::{SemanticTokensLegend, Uri};
13use std::collections::HashMap;
14use std::collections::HashSet;
15use std::path::Path;
16use std::time::{Duration, Instant};
17
18/// Consume and discard a `Result` from a fire-and-forget operation.
19///
20/// Use for best-effort cleanup where failure is expected and non-actionable,
21/// e.g. shutting down an LSP server that may have already exited.
22fn fire_and_forget<E: std::fmt::Debug>(result: Result<(), E>) {
23    if let Err(e) = result {
24        tracing::trace!(error = ?e, "fire-and-forget operation failed");
25    }
26}
27
28/// Which languages an LSP server handles.
29///
30/// Empty means the server is universal (accepts all languages).
31/// Non-empty lists only the accepted languages.
32#[derive(Debug, Clone)]
33pub struct LanguageScope(Vec<String>);
34
35impl LanguageScope {
36    /// Universal scope — accepts all languages.
37    pub fn all() -> Self {
38        Self(Vec::new())
39    }
40
41    /// Scope for a single language.
42    pub fn single(language: impl Into<String>) -> Self {
43        Self(vec![language.into()])
44    }
45
46    /// Whether this scope accepts documents of the given language.
47    pub fn accepts(&self, language: &str) -> bool {
48        self.0.is_empty() || self.0.iter().any(|l| l == language)
49    }
50
51    /// Whether this is a universal scope (accepts all languages).
52    pub fn is_universal(&self) -> bool {
53        self.0.is_empty()
54    }
55
56    /// The language list. Empty means all.
57    pub fn languages(&self) -> &[String] {
58        &self.0
59    }
60
61    /// A display label for logging and status messages.
62    pub fn label(&self) -> &str {
63        self.0.first().map(|s| s.as_str()).unwrap_or("universal")
64    }
65}
66
67/// Result of attempting to spawn an LSP server
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum LspSpawnResult {
70    /// Server was spawned or already running
71    Spawned,
72    /// Server is not configured for auto-start
73    /// The server can still be started manually via command palette
74    NotAutoStart,
75    /// No LSP server is configured for this language
76    NotConfigured,
77    /// Server spawn failed or is disabled
78    Failed,
79}
80
81/// Constants for restart behavior
82const MAX_RESTARTS_IN_WINDOW: usize = 5;
83const RESTART_WINDOW_SECS: u64 = 180; // 3 minutes
84const RESTART_BACKOFF_BASE_MS: u64 = 1000; // 1s, 2s, 4s, 8s...
85
86/// Convert a directory path to an LSP `file://` URI without the `url` crate.
87fn path_to_uri(path: &Path) -> Option<Uri> {
88    let abs = if path.is_absolute() {
89        path.to_path_buf()
90    } else {
91        std::env::current_dir().ok()?.join(path)
92    };
93    // Percent-encode each path component for RFC 3986 compliance
94    let encoded: String = abs
95        .components()
96        .filter_map(|c| match c {
97            std::path::Component::RootDir => None, // handled by leading '/' in Normal
98            std::path::Component::Normal(s) => {
99                let s = s.to_str()?;
100                let mut out = String::with_capacity(s.len() + 1);
101                out.push('/');
102                for b in s.bytes() {
103                    if b.is_ascii_alphanumeric()
104                        || matches!(
105                            b,
106                            b'-' | b'.'
107                                | b'_'
108                                | b'~'
109                                | b'@'
110                                | b'!'
111                                | b'$'
112                                | b'&'
113                                | b'\''
114                                | b'('
115                                | b')'
116                                | b'+'
117                                | b','
118                                | b';'
119                                | b'='
120                        )
121                    {
122                        out.push(b as char);
123                    } else {
124                        out.push_str(&format!("%{:02X}", b));
125                    }
126                }
127                Some(out)
128            }
129            _ => None,
130        })
131        .collect();
132    format!("file://{}", encoded).parse().ok()
133}
134
135/// Detect workspace root by walking upward from a file looking for marker files/directories.
136///
137/// Returns the first directory containing any of the markers, or the file's parent
138/// directory if no marker is found.
139pub fn detect_workspace_root(file_path: &Path, root_markers: &[String]) -> std::path::PathBuf {
140    let file_dir = file_path.parent().unwrap_or(file_path).to_path_buf();
141
142    if root_markers.is_empty() {
143        return file_dir;
144    }
145
146    let mut dir = Some(file_dir.as_path());
147    while let Some(d) = dir {
148        for marker in root_markers {
149            if d.join(marker).exists() {
150                return d.to_path_buf();
151            }
152        }
153        dir = d.parent();
154    }
155
156    file_dir
157}
158
159/// Summary of capabilities reported by an LSP server during initialization.
160///
161/// This is extracted from `ServerCapabilities` in the `initialize` response
162/// and stored per-server so that requests are only sent to servers that
163/// actually support them. Follows the LSP 3.17 specification.
164///
165#[derive(Debug, Clone, Default)]
166pub struct ServerCapabilitySummary {
167    /// Whether capabilities have been received from the server.
168    /// When false, `has_capability()` defers to the handle's readiness state.
169    pub initialized: bool,
170    pub hover: bool,
171    pub completion: bool,
172    pub completion_resolve: bool,
173    pub completion_trigger_characters: Vec<String>,
174    pub definition: bool,
175    pub references: bool,
176    pub document_formatting: bool,
177    pub document_range_formatting: bool,
178    pub rename: bool,
179    pub signature_help: bool,
180    pub inlay_hints: bool,
181    pub folding_ranges: bool,
182    pub semantic_tokens_full: bool,
183    pub semantic_tokens_full_delta: bool,
184    pub semantic_tokens_range: bool,
185    pub semantic_tokens_legend: Option<SemanticTokensLegend>,
186    pub document_highlight: bool,
187    pub code_action: bool,
188    pub code_action_resolve: bool,
189    pub document_symbols: bool,
190    pub workspace_symbols: bool,
191    pub diagnostics: bool,
192}
193
194/// A named LSP handle with feature filter metadata and per-server capabilities.
195/// Wraps an LspHandle with the server's display name, feature routing filter,
196/// and the capabilities reported by this specific server during initialization.
197pub struct ServerHandle {
198    /// Display name for this server (e.g., "rust-analyzer", "eslint")
199    pub name: String,
200    /// The underlying LSP handle
201    pub handle: LspHandle,
202    /// Feature filter controlling which LSP features this server handles
203    pub feature_filter: FeatureFilter,
204    /// Capabilities reported by this server during initialization.
205    pub capabilities: ServerCapabilitySummary,
206}
207
208impl ServerHandle {
209    /// Check if this server has the actual capability for a feature.
210    ///
211    /// Checks the server's reported capabilities (from the `initialize` response).
212    /// Before initialization completes (capabilities not yet received), returns
213    /// `false` — the main loop must not route feature requests to servers whose
214    /// capabilities are unknown. Callers handle `None` from `handle_for_feature_mut`
215    /// by relying on existing retry mechanisms (render-cycle polling, timer retries,
216    /// or explicit re-requests from the `LspInitialized` handler).
217    pub fn has_capability(&self, feature: LspFeature) -> bool {
218        if !self.capabilities.initialized {
219            return false;
220        }
221        match feature {
222            LspFeature::Hover => self.capabilities.hover,
223            LspFeature::Completion => self.capabilities.completion,
224            LspFeature::Definition => self.capabilities.definition,
225            LspFeature::References => self.capabilities.references,
226            LspFeature::Format => {
227                self.capabilities.document_formatting || self.capabilities.document_range_formatting
228            }
229            LspFeature::Rename => self.capabilities.rename,
230            LspFeature::SignatureHelp => self.capabilities.signature_help,
231            LspFeature::InlayHints => self.capabilities.inlay_hints,
232            LspFeature::FoldingRange => self.capabilities.folding_ranges,
233            LspFeature::SemanticTokens => {
234                self.capabilities.semantic_tokens_full || self.capabilities.semantic_tokens_range
235            }
236            LspFeature::DocumentHighlight => self.capabilities.document_highlight,
237            LspFeature::CodeAction => self.capabilities.code_action,
238            LspFeature::DocumentSymbols => self.capabilities.document_symbols,
239            LspFeature::WorkspaceSymbols => self.capabilities.workspace_symbols,
240            LspFeature::Diagnostics => self.capabilities.diagnostics,
241        }
242    }
243}
244
245/// Manager for multiple language servers (async version)
246pub struct LspManager {
247    /// All running LSP server handles. Each handle's `LanguageScope` determines
248    /// which languages it serves. Universal servers have `LanguageScope::all()`.
249    handles: Vec<ServerHandle>,
250
251    /// Configuration for each language (supports multiple servers per language)
252    config: HashMap<String, Vec<LspServerConfig>>,
253
254    /// Universal (global) LSP server configs — spawned once per project.
255    universal_configs: Vec<LspServerConfig>,
256
257    /// Default root URI for workspace (used if no per-language root is set)
258    root_uri: Option<Uri>,
259
260    /// Per-language root URIs (allows plugins to specify project roots)
261    per_language_root_uris: HashMap<String, Uri>,
262
263    /// Tokio runtime reference
264    runtime: Option<tokio::runtime::Handle>,
265
266    /// Async bridge for communication
267    async_bridge: Option<AsyncBridge>,
268
269    /// Restart attempt timestamps per language (for tracking restart frequency)
270    restart_attempts: HashMap<String, Vec<Instant>>,
271
272    /// Languages currently in restart cooldown (gave up after too many restarts)
273    restart_cooldown: HashSet<String>,
274
275    /// Scheduled restart times (language -> when to restart)
276    pending_restarts: HashMap<String, Instant>,
277
278    /// Languages that have been manually started by the user
279    /// If a language is in this set, it will spawn even if auto_start=false in config
280    allowed_languages: HashSet<String>,
281
282    /// Languages that have been explicitly disabled/stopped by the user
283    /// These will not auto-restart until user manually restarts them
284    disabled_languages: HashSet<String>,
285}
286
287impl LspManager {
288    /// Create a new LSP manager
289    pub fn new(root_uri: Option<Uri>) -> Self {
290        Self {
291            handles: Vec::new(),
292            config: HashMap::new(),
293            universal_configs: Vec::new(),
294            root_uri,
295            per_language_root_uris: HashMap::new(),
296            runtime: None,
297            async_bridge: None,
298            restart_attempts: HashMap::new(),
299            restart_cooldown: HashSet::new(),
300            pending_restarts: HashMap::new(),
301            allowed_languages: HashSet::new(),
302            disabled_languages: HashSet::new(),
303        }
304    }
305
306    /// Check if a language has been manually enabled (allowing spawn even if auto_start=false)
307    pub fn is_language_allowed(&self, language: &str) -> bool {
308        self.allowed_languages.contains(language)
309    }
310
311    /// Allow a language to spawn LSP server (used by manual start command)
312    pub fn allow_language(&mut self, language: &str) {
313        self.allowed_languages.insert(language.to_string());
314        tracing::info!("LSP language '{}' manually enabled", language);
315    }
316
317    /// Get the set of manually enabled languages
318    pub fn allowed_languages(&self) -> &HashSet<String> {
319        &self.allowed_languages
320    }
321
322    /// Get the configurations for a specific language (one or more servers).
323    pub fn get_configs(&self, language: &str) -> Option<&[LspServerConfig]> {
324        self.config.get(language).map(|v| v.as_slice())
325    }
326
327    /// Get the primary (first) configuration for a specific language.
328    pub fn get_config(&self, language: &str) -> Option<&LspServerConfig> {
329        self.config.get(language).and_then(|v| v.first())
330    }
331
332    /// Store capabilities on the specific server handle identified by server_name.
333    pub fn set_server_capabilities(
334        &mut self,
335        _language: &str,
336        server_name: &str,
337        mut capabilities: ServerCapabilitySummary,
338    ) {
339        capabilities.initialized = true;
340
341        if let Some(sh) = self.handles.iter_mut().find(|sh| sh.name == server_name) {
342            sh.capabilities = capabilities;
343        }
344    }
345
346    /// Get the semantic token legend for a language from the first eligible server.
347    pub fn semantic_tokens_legend(&self, language: &str) -> Option<&SemanticTokensLegend> {
348        self.get_handles(language).into_iter().find_map(|sh| {
349            if sh.feature_filter.allows(LspFeature::SemanticTokens)
350                && sh.has_capability(LspFeature::SemanticTokens)
351            {
352                sh.capabilities.semantic_tokens_legend.as_ref()
353            } else {
354                None
355            }
356        })
357    }
358
359    /// Check if any eligible server for the language supports full semantic tokens.
360    pub fn semantic_tokens_full_supported(&self, language: &str) -> bool {
361        self.get_handles(language).iter().any(|sh| {
362            sh.feature_filter.allows(LspFeature::SemanticTokens)
363                && sh.capabilities.semantic_tokens_full
364        })
365    }
366
367    /// Check if any eligible server for the language supports full semantic token deltas.
368    pub fn semantic_tokens_full_delta_supported(&self, language: &str) -> bool {
369        self.get_handles(language).iter().any(|sh| {
370            sh.feature_filter.allows(LspFeature::SemanticTokens)
371                && sh.capabilities.semantic_tokens_full_delta
372        })
373    }
374
375    /// Check if any eligible server for the language supports range semantic tokens.
376    pub fn semantic_tokens_range_supported(&self, language: &str) -> bool {
377        self.get_handles(language).iter().any(|sh| {
378            sh.feature_filter.allows(LspFeature::SemanticTokens)
379                && sh.capabilities.semantic_tokens_range
380        })
381    }
382
383    /// Check if any eligible server for the language supports folding ranges.
384    pub fn folding_ranges_supported(&self, language: &str) -> bool {
385        self.get_handles(language).iter().any(|sh| {
386            sh.feature_filter.allows(LspFeature::FoldingRange) && sh.capabilities.folding_ranges
387        })
388    }
389
390    /// Check if a character is a completion trigger for any running language server.
391    pub fn is_completion_trigger_char(&self, ch: char, language: &str) -> bool {
392        let ch_str = ch.to_string();
393        self.get_handles(language).iter().any(|sh| {
394            sh.feature_filter.allows(LspFeature::Completion)
395                && sh
396                    .capabilities
397                    .completion_trigger_characters
398                    .contains(&ch_str)
399        })
400    }
401
402    /// Try to spawn an LSP server, checking auto_start configuration
403    ///
404    /// This is the main entry point for spawning LSP servers on file open.
405    /// It returns:
406    /// - `LspSpawnResult::Spawned` if the server was spawned or already running
407    /// - `LspSpawnResult::NotAutoStart` if auto_start is false and not manually allowed
408    /// - `LspSpawnResult::NotConfigured` if no LSP server is configured for the language
409    /// - `LspSpawnResult::Failed` if spawn failed or language is disabled
410    ///
411    /// The `file_path` is used for workspace root detection via `root_markers`.
412    ///
413    /// IMPORTANT: Callers should only call this when there is at least one buffer
414    /// with a matching language. Do not call for languages with no open files.
415    pub fn try_spawn(&mut self, language: &str, file_path: Option<&Path>) -> LspSpawnResult {
416        // If handles already exist for this language, just ensure universals are running too
417        if self
418            .handles
419            .iter()
420            .any(|sh| sh.handle.scope().accepts(language))
421        {
422            self.ensure_universal_servers_running(file_path);
423            return LspSpawnResult::Spawned;
424        }
425
426        // Check if we have runtime and bridge
427        if self.runtime.is_none() || self.async_bridge.is_none() {
428            return LspSpawnResult::Failed;
429        }
430
431        // Always try to start universal servers (they manage their own auto_start check)
432        self.ensure_universal_servers_running(file_path);
433
434        // Check if language is configured
435        let configs = match self.config.get(language) {
436            Some(configs) if !configs.is_empty() => configs,
437            _ => {
438                // No per-language config, but universal servers may be running
439                if self
440                    .handles
441                    .iter()
442                    .any(|sh| sh.handle.scope().is_universal())
443                {
444                    return LspSpawnResult::Spawned;
445                }
446                return LspSpawnResult::NotConfigured;
447            }
448        };
449
450        // Check if any per-language config is enabled
451        if !configs.iter().any(|c| c.enabled) {
452            if self
453                .handles
454                .iter()
455                .any(|sh| sh.handle.scope().is_universal())
456            {
457                return LspSpawnResult::Spawned;
458            }
459            return LspSpawnResult::Failed;
460        }
461
462        // Check if auto_start is enabled (on any per-language config) or language was manually allowed
463        let any_auto_start = configs.iter().any(|c| c.auto_start && c.enabled);
464        if !any_auto_start && !self.allowed_languages.contains(language) {
465            if self
466                .handles
467                .iter()
468                .any(|sh| sh.handle.scope().is_universal())
469            {
470                return LspSpawnResult::Spawned;
471            }
472            return LspSpawnResult::NotAutoStart;
473        }
474
475        // Spawn per-language servers
476        let spawned = self.force_spawn(language, file_path).is_some();
477
478        if spawned
479            || self
480                .handles
481                .iter()
482                .any(|sh| sh.handle.scope().is_universal())
483        {
484            LspSpawnResult::Spawned
485        } else {
486            LspSpawnResult::Failed
487        }
488    }
489
490    /// Set the Tokio runtime and async bridge
491    ///
492    /// Must be called before spawning any servers
493    pub fn set_runtime(&mut self, runtime: tokio::runtime::Handle, async_bridge: AsyncBridge) {
494        self.runtime = Some(runtime);
495        self.async_bridge = Some(async_bridge);
496    }
497
498    /// Set configuration for a language (single server).
499    pub fn set_language_config(&mut self, language: String, config: LspServerConfig) {
500        self.config.insert(language, vec![config]);
501    }
502
503    /// Set configurations for a language (one or more servers).
504    pub fn set_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
505        self.config.insert(language, configs);
506    }
507
508    /// Append additional server configs to an existing language entry.
509    pub fn append_language_configs(&mut self, language: String, configs: Vec<LspServerConfig>) {
510        self.config.entry(language).or_default().extend(configs);
511    }
512
513    /// Set universal (global) LSP server configs.
514    ///
515    /// Universal servers are spawned once per project and shared across all
516    /// languages, rather than being duplicated into each language's config list.
517    pub fn set_universal_configs(&mut self, configs: Vec<LspServerConfig>) {
518        self.universal_configs = configs;
519    }
520
521    /// Return the list of currently configured language keys.
522    pub fn configured_languages(&self) -> Vec<String> {
523        self.config.keys().cloned().collect()
524    }
525
526    /// Set a new root URI for the workspace
527    ///
528    /// This should be called after shutting down all servers when switching projects.
529    /// Servers spawned after this will use the new root URI.
530    pub fn set_root_uri(&mut self, root_uri: Option<Uri>) {
531        self.root_uri = root_uri;
532    }
533
534    /// Set a language-specific root URI
535    ///
536    /// This allows plugins to specify project roots for specific languages.
537    /// For example, a C# plugin can set the root to the directory containing .csproj.
538    /// Returns true if an existing server was restarted with the new root.
539    pub fn set_language_root_uri(&mut self, language: &str, uri: Uri) -> bool {
540        tracing::info!("Setting root URI for {}: {}", language, uri.as_str());
541        self.per_language_root_uris
542            .insert(language.to_string(), uri.clone());
543
544        // If there's an existing server for this language, restart it with the new root
545        if self
546            .handles
547            .iter()
548            .any(|sh| sh.handle.scope().accepts(language))
549        {
550            tracing::info!(
551                "Restarting {} LSP server with new root: {}",
552                language,
553                uri.as_str()
554            );
555            self.shutdown_server(language);
556            // The server will be respawned on next request with the new root
557            return true;
558        }
559        false
560    }
561
562    /// Resolve the root URI for a language, using root_markers for detection.
563    ///
564    /// Priority:
565    /// 1. Plugin-set per-language root (per_language_root_uris)
566    /// 2. Walk upward from file_path using config's root_markers
567    /// 3. File's parent directory
568    pub fn resolve_root_uri(&self, language: &str, file_path: Option<&Path>) -> Option<Uri> {
569        // 1. Plugin-set root takes priority
570        if let Some(uri) = self.per_language_root_uris.get(language) {
571            return Some(uri.clone());
572        }
573
574        // 2. Use root_markers to detect workspace root from file path
575        if let Some(path) = file_path {
576            let markers = self
577                .config
578                .get(language)
579                .and_then(|configs| configs.first())
580                .map(|c| c.root_markers.as_slice())
581                .unwrap_or(&[]);
582            let root = detect_workspace_root(path, markers);
583            if let Some(uri) = path_to_uri(&root) {
584                return Some(uri);
585            }
586        }
587
588        // 3. No file path available — use the global root_uri
589        self.root_uri.clone()
590    }
591
592    /// Get the effective root URI for a language (legacy, without file-based detection)
593    ///
594    /// Returns the language-specific root if set, otherwise the default root.
595    pub fn get_effective_root_uri(&self, language: &str) -> Option<Uri> {
596        self.resolve_root_uri(language, None)
597    }
598
599    /// Reset the manager for a new project
600    ///
601    /// This shuts down all servers and clears state, preparing for a fresh start.
602    /// The configuration is preserved but servers will need to be respawned.
603    pub fn reset_for_new_project(&mut self, new_root_uri: Option<Uri>) {
604        // Shutdown all servers
605        self.shutdown_all();
606
607        // Update root URI
608        self.root_uri = new_root_uri;
609
610        // Clear restart tracking state (fresh start)
611        self.restart_attempts.clear();
612        self.restart_cooldown.clear();
613        self.pending_restarts.clear();
614
615        // Keep allowed_languages and disabled_languages as user preferences
616        // Keep config as it's not project-specific
617
618        tracing::info!(
619            "LSP manager reset for new project: {:?}",
620            self.root_uri.as_ref().map(|u| u.as_str())
621        );
622    }
623
624    /// Get the primary (first) existing LSP handle for a language (no spawning).
625    /// Checks language-specific handles first, then universal handles.
626    pub fn get_handle(&self, language: &str) -> Option<&LspHandle> {
627        self.handles
628            .iter()
629            .find(|sh| sh.handle.scope().accepts(language))
630            .map(|sh| &sh.handle)
631    }
632
633    /// Get the primary (first) mutable existing LSP handle for a language (no spawning).
634    /// Checks language-specific handles first, then universal handles.
635    pub fn get_handle_mut(&mut self, language: &str) -> Option<&mut LspHandle> {
636        self.handles
637            .iter_mut()
638            .find(|sh| sh.handle.scope().accepts(language))
639            .map(|sh| &mut sh.handle)
640    }
641
642    /// Get all handles that accept a language (both language-specific and universal).
643    pub fn get_handles(&self, language: &str) -> Vec<&ServerHandle> {
644        self.handles
645            .iter()
646            .filter(|sh| sh.handle.scope().accepts(language))
647            .collect()
648    }
649
650    /// Get all mutable handles that accept a language (both language-specific and universal).
651    pub fn get_handles_mut(&mut self, language: &str) -> Vec<&mut ServerHandle> {
652        self.handles
653            .iter_mut()
654            .filter(|sh| sh.handle.scope().accepts(language))
655            .collect()
656    }
657
658    /// Get the language scope for a server by name.
659    ///
660    /// Returns `None` if the server is not found.
661    pub fn server_scope(&self, server_name: &str) -> Option<&LanguageScope> {
662        self.handles
663            .iter()
664            .find(|sh| sh.name == server_name)
665            .map(|sh| sh.handle.scope())
666    }
667
668    /// Check if any handles (language-specific or universal) exist for a language.
669    pub fn has_handles(&self, language: &str) -> bool {
670        self.handles
671            .iter()
672            .any(|sh| sh.handle.scope().accepts(language))
673    }
674
675    /// Count all handles that accept a language.
676    pub fn handle_count(&self, language: &str) -> usize {
677        self.handles
678            .iter()
679            .filter(|sh| sh.handle.scope().accepts(language))
680            .count()
681    }
682
683    /// Check if a server with the given name exists.
684    pub fn has_server_named(&self, server_name: &str) -> bool {
685        self.handles.iter().any(|sh| sh.name == server_name)
686    }
687
688    /// Get the first handle for a language that allows a given feature (for exclusive features).
689    /// For capability-gated features (semantic tokens, folding ranges), this also checks
690    /// that the server actually reported the capability during initialization.
691    /// Checks per-language handles first, then universal handles.
692    /// Returns `None` if no handle matches.
693    pub fn handle_for_feature(&self, language: &str, feature: LspFeature) -> Option<&ServerHandle> {
694        self.handles
695            .iter()
696            .filter(|sh| sh.handle.scope().accepts(language))
697            .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
698    }
699
700    /// Get the first mutable handle for a language that allows a given feature.
701    /// For capability-gated features, this also checks the server's actual capabilities.
702    /// Checks per-language handles first, then universal handles.
703    pub fn handle_for_feature_mut(
704        &mut self,
705        language: &str,
706        feature: LspFeature,
707    ) -> Option<&mut ServerHandle> {
708        self.handles
709            .iter_mut()
710            .filter(|sh| sh.handle.scope().accepts(language))
711            .find(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
712    }
713
714    /// Get all handles for a language that allow a given feature (for merged features).
715    /// Like `handle_for_feature`, also checks per-server capabilities.
716    /// Includes both per-language and universal handles.
717    pub fn handles_for_feature(&self, language: &str, feature: LspFeature) -> Vec<&ServerHandle> {
718        self.handles
719            .iter()
720            .filter(|sh| sh.handle.scope().accepts(language))
721            .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
722            .collect()
723    }
724
725    /// Get all mutable handles for a language that allow a given feature.
726    /// Like `handle_for_feature_mut`, also checks per-server capabilities.
727    /// Includes both per-language and universal handles.
728    pub fn handles_for_feature_mut(
729        &mut self,
730        language: &str,
731        feature: LspFeature,
732    ) -> Vec<&mut ServerHandle> {
733        self.handles
734            .iter_mut()
735            .filter(|sh| sh.handle.scope().accepts(language))
736            .filter(|sh| sh.feature_filter.allows(feature) && sh.has_capability(feature))
737            .collect()
738    }
739
740    /// Force spawn LSP server(s) for a language.
741    ///
742    /// Spawns servers configured for the language, filtered as follows:
743    /// - If the language is in `allowed_languages` (the user explicitly
744    ///   started or approved this language via a manual command), spawns
745    ///   every configured server regardless of its `enabled` / `auto_start`
746    ///   flags. This is the "manual" path used by the command palette's
747    ///   Start / Restart LSP commands and the LSP confirmation popup.
748    /// - Otherwise (the auto-start path, reached via `try_spawn` on buffer
749    ///   load or by crash recovery), spawns only servers that have both
750    ///   `enabled=true` AND `auto_start=true`. Each config's own
751    ///   `auto_start` flag is honoured individually, so configuring one
752    ///   auto-start server alongside an opt-in manual server no longer
753    ///   drags the manual one along for the ride.
754    ///
755    /// Returns a mutable reference to the primary (first) handle if any
756    /// were spawned. The `file_path` is used for workspace root detection
757    /// via `root_markers`.
758    pub fn force_spawn(
759        &mut self,
760        language: &str,
761        file_path: Option<&Path>,
762    ) -> Option<&mut LspHandle> {
763        tracing::debug!("force_spawn called for language: {}", language);
764
765        // Return existing handle if available
766        if self
767            .handles
768            .iter()
769            .any(|sh| sh.handle.scope().accepts(language))
770        {
771            tracing::debug!("force_spawn: returning existing handle for {}", language);
772            return self
773                .handles
774                .iter_mut()
775                .find(|sh| sh.handle.scope().accepts(language))
776                .map(|sh| &mut sh.handle);
777        }
778
779        // Check if language was explicitly disabled by user (via stop command)
780        if self.disabled_languages.contains(language) {
781            tracing::debug!(
782                "LSP for {} is disabled, not spawning (use manual restart to re-enable)",
783                language
784            );
785            return None;
786        }
787
788        // Get configs for this language
789        let configs = match self.config.get(language) {
790            Some(configs) if !configs.is_empty() => configs.clone(),
791            _ => {
792                tracing::warn!(
793                    "force_spawn: no config found for language '{}', available configs: {:?}",
794                    language,
795                    self.config.keys().collect::<Vec<_>>()
796                );
797                return None;
798            }
799        };
800
801        // Check we have runtime and bridge
802        let runtime = match self.runtime.as_ref() {
803            Some(r) => r.clone(),
804            None => {
805                tracing::error!("force_spawn: no tokio runtime available for {}", language);
806                return None;
807            }
808        };
809        let async_bridge = match self.async_bridge.as_ref() {
810            Some(b) => b.clone(),
811            None => {
812                tracing::error!("force_spawn: no async bridge available for {}", language);
813                return None;
814            }
815        };
816
817        let mut spawned_handles = Vec::new();
818        let manually_allowed = self.allowed_languages.contains(language);
819
820        for config in &configs {
821            if manually_allowed {
822                // User explicitly started this language via command palette:
823                // spawn every configured server, even if individually
824                // disabled or marked not-auto-start.
825            } else {
826                // Auto-start path: only spawn servers that the user has
827                // opted into both via `enabled=true` AND `auto_start=true`.
828                // This honours each config's flags independently so that
829                // e.g. configuring rust-auto (auto_start=true) alongside
830                // rust-manual (auto_start=false) does not spawn both.
831                if !config.enabled || !config.auto_start {
832                    continue;
833                }
834            }
835
836            if config.command.is_empty() {
837                tracing::warn!(
838                    "force_spawn: LSP command is empty for {} server '{}'",
839                    language,
840                    config.display_name()
841                );
842                continue;
843            }
844
845            let server_name = config.display_name();
846            tracing::info!(
847                "Spawning LSP server '{}' for language: {}",
848                server_name,
849                language
850            );
851
852            match LspHandle::spawn(
853                &runtime,
854                &config.command,
855                &config.args,
856                config.env.clone(),
857                LanguageScope::single(language),
858                server_name.clone(),
859                &async_bridge,
860                config.process_limits.clone(),
861                config.language_id_overrides.clone(),
862            ) {
863                Ok(handle) => {
864                    let effective_root = self.resolve_root_uri(language, file_path);
865                    if let Err(e) =
866                        handle.initialize(effective_root, config.initialization_options.clone())
867                    {
868                        tracing::error!(
869                            "Failed to send initialize command for {} ({}): {}",
870                            language,
871                            server_name,
872                            e
873                        );
874                        continue;
875                    }
876
877                    tracing::info!(
878                        "LSP initialization started for {} ({}), will be ready asynchronously",
879                        language,
880                        server_name
881                    );
882
883                    spawned_handles.push(ServerHandle {
884                        name: server_name,
885                        handle,
886                        feature_filter: config.feature_filter(),
887                        capabilities: ServerCapabilitySummary::default(),
888                    });
889                }
890                Err(e) => {
891                    tracing::error!(
892                        "Failed to spawn LSP handle for {} ({}): {}",
893                        language,
894                        server_name,
895                        e
896                    );
897                }
898            }
899        }
900
901        if spawned_handles.is_empty() {
902            return None;
903        }
904
905        self.handles.extend(spawned_handles);
906        self.handles
907            .iter_mut()
908            .rev()
909            .find(|sh| sh.handle.scope().accepts(language))
910            .map(|sh| &mut sh.handle)
911    }
912
913    /// Spawn universal LSP servers if they aren't already running.
914    ///
915    /// Called from `try_spawn` — universal servers are spawned once and shared
916    /// across all languages. Only servers with `enabled=true` and
917    /// `auto_start=true` are started automatically.
918    fn ensure_universal_servers_running(&mut self, file_path: Option<&Path>) {
919        if self
920            .handles
921            .iter()
922            .any(|sh| sh.handle.scope().is_universal())
923            || self.universal_configs.is_empty()
924        {
925            return;
926        }
927
928        let runtime = match self.runtime.as_ref() {
929            Some(r) => r.clone(),
930            None => return,
931        };
932        let async_bridge = match self.async_bridge.as_ref() {
933            Some(b) => b.clone(),
934            None => return,
935        };
936
937        let mut spawned = Vec::new();
938        for config in &self.universal_configs {
939            if !config.enabled || !config.auto_start {
940                continue;
941            }
942            if config.command.is_empty() {
943                continue;
944            }
945
946            let server_name = config.display_name();
947            tracing::info!("Spawning universal LSP server '{}'", server_name);
948
949            match LspHandle::spawn(
950                &runtime,
951                &config.command,
952                &config.args,
953                config.env.clone(),
954                LanguageScope::all(),
955                server_name.clone(),
956                &async_bridge,
957                config.process_limits.clone(),
958                config.language_id_overrides.clone(),
959            ) {
960                Ok(handle) => {
961                    let effective_root = file_path
962                        .map(|p| {
963                            let root = detect_workspace_root(p, &config.root_markers);
964                            path_to_uri(&root)
965                        })
966                        .flatten()
967                        .or_else(|| self.root_uri.clone());
968                    if let Err(e) =
969                        handle.initialize(effective_root, config.initialization_options.clone())
970                    {
971                        tracing::error!(
972                            "Failed to initialize universal LSP server '{}': {}",
973                            server_name,
974                            e
975                        );
976                        continue;
977                    }
978                    tracing::info!(
979                        "Universal LSP server '{}' initialization started",
980                        server_name
981                    );
982                    spawned.push(ServerHandle {
983                        name: server_name,
984                        handle,
985                        feature_filter: config.feature_filter(),
986                        capabilities: ServerCapabilitySummary::default(),
987                    });
988                }
989                Err(e) => {
990                    tracing::error!(
991                        "Failed to spawn universal LSP server '{}': {}",
992                        server_name,
993                        e
994                    );
995                }
996            }
997        }
998
999        self.handles.extend(spawned);
1000    }
1001
1002    /// Handle a server crash by scheduling a restart with exponential backoff
1003    ///
1004    /// Returns a message describing the action taken (for UI notification)
1005    pub fn handle_server_crash(&mut self, language: &str, server_name: &str) -> String {
1006        // Check if the crashed server is a universal handle
1007        if self
1008            .handles
1009            .iter()
1010            .any(|sh| sh.name == server_name && sh.handle.scope().is_universal())
1011        {
1012            // Drain all universal handles and shut them down
1013            let universals: Vec<ServerHandle> = {
1014                let mut drained = Vec::new();
1015                let mut i = 0;
1016                while i < self.handles.len() {
1017                    if self.handles[i].handle.scope().is_universal() {
1018                        drained.push(self.handles.remove(i));
1019                    } else {
1020                        i += 1;
1021                    }
1022                }
1023                drained
1024            };
1025            for sh in universals {
1026                fire_and_forget(sh.handle.shutdown());
1027            }
1028            // Universal servers will be re-spawned on next try_spawn call
1029            return "Universal LSP server crashed. It will restart on next file open.".to_string();
1030        }
1031
1032        // Remove all handles that accept this language (but not universal ones)
1033        {
1034            let mut i = 0;
1035            while i < self.handles.len() {
1036                if !self.handles[i].handle.scope().is_universal()
1037                    && self.handles[i].handle.scope().accepts(language)
1038                {
1039                    let sh = self.handles.remove(i);
1040                    fire_and_forget(sh.handle.shutdown());
1041                } else {
1042                    i += 1;
1043                }
1044            }
1045        }
1046
1047        // Check if server was explicitly disabled by user (via stop command)
1048        // Don't auto-restart disabled servers
1049        if self.disabled_languages.contains(language) {
1050            return format!(
1051                "LSP server for {} stopped. Use 'Restart LSP Server' command to start it again.",
1052                language
1053            );
1054        }
1055
1056        // Check if we're in cooldown
1057        if self.restart_cooldown.contains(language) {
1058            return format!(
1059                "LSP server for {} crashed. Too many restarts - use 'Restart LSP Server' command to retry.",
1060                language
1061            );
1062        }
1063
1064        // Clean up old restart attempts outside the window
1065        let now = Instant::now();
1066        let window = Duration::from_secs(RESTART_WINDOW_SECS);
1067        let attempts = self
1068            .restart_attempts
1069            .entry(language.to_string())
1070            .or_default();
1071        attempts.retain(|t| now.duration_since(*t) < window);
1072
1073        // Check if we've exceeded max restarts
1074        if attempts.len() >= MAX_RESTARTS_IN_WINDOW {
1075            self.restart_cooldown.insert(language.to_string());
1076            tracing::warn!(
1077                "LSP server for {} has crashed {} times in {} minutes, entering cooldown",
1078                language,
1079                MAX_RESTARTS_IN_WINDOW,
1080                RESTART_WINDOW_SECS / 60
1081            );
1082            return format!(
1083                "LSP server for {} has crashed too many times ({} in {} min). Use 'Restart LSP Server' command to manually restart.",
1084                language,
1085                MAX_RESTARTS_IN_WINDOW,
1086                RESTART_WINDOW_SECS / 60
1087            );
1088        }
1089
1090        // Calculate exponential backoff delay
1091        let attempt_number = attempts.len();
1092        let delay_ms = RESTART_BACKOFF_BASE_MS * (1 << attempt_number); // 1s, 2s, 4s, 8s
1093        let restart_time = now + Duration::from_millis(delay_ms);
1094
1095        // Schedule the restart
1096        self.pending_restarts
1097            .insert(language.to_string(), restart_time);
1098
1099        tracing::info!(
1100            "LSP server for {} crashed (attempt {}/{}), will restart in {}ms",
1101            language,
1102            attempt_number + 1,
1103            MAX_RESTARTS_IN_WINDOW,
1104            delay_ms
1105        );
1106
1107        format!(
1108            "LSP server for {} crashed (attempt {}/{}), restarting in {}s...",
1109            language,
1110            attempt_number + 1,
1111            MAX_RESTARTS_IN_WINDOW,
1112            delay_ms / 1000
1113        )
1114    }
1115
1116    /// Check and process any pending restarts that are due
1117    ///
1118    /// Returns list of (language, success, message) for each restart attempted
1119    pub fn process_pending_restarts(&mut self) -> Vec<(String, bool, String)> {
1120        let now = Instant::now();
1121        let mut results = Vec::new();
1122
1123        // Find restarts that are due
1124        let due_restarts: Vec<String> = self
1125            .pending_restarts
1126            .iter()
1127            .filter(|(_, time)| **time <= now)
1128            .map(|(lang, _)| lang.clone())
1129            .collect();
1130
1131        for language in due_restarts {
1132            self.pending_restarts.remove(&language);
1133
1134            // Record this restart attempt
1135            self.restart_attempts
1136                .entry(language.clone())
1137                .or_default()
1138                .push(now);
1139
1140            // Attempt to spawn the server (bypassing auto_start for crash recovery)
1141            if self.force_spawn(&language, None).is_some() {
1142                let message = format!("LSP server for {} restarted successfully", language);
1143                tracing::info!("{}", message);
1144                results.push((language, true, message));
1145            } else {
1146                let message = format!("Failed to restart LSP server for {}", language);
1147                tracing::error!("{}", message);
1148                results.push((language, false, message));
1149            }
1150        }
1151
1152        results
1153    }
1154
1155    /// Check if a language server is in restart cooldown
1156    pub fn is_in_cooldown(&self, language: &str) -> bool {
1157        self.restart_cooldown.contains(language)
1158    }
1159
1160    /// Check if a language server has a pending restart
1161    pub fn has_pending_restart(&self, language: &str) -> bool {
1162        self.pending_restarts.contains_key(language)
1163    }
1164
1165    /// Clear cooldown for a language and allow manual restart
1166    pub fn clear_cooldown(&mut self, language: &str) {
1167        self.restart_cooldown.remove(language);
1168        self.restart_attempts.remove(language);
1169        self.pending_restarts.remove(language);
1170        tracing::info!("Cleared restart cooldown for {}", language);
1171    }
1172
1173    /// Manually restart/start a language server (bypasses cooldown and auto_start check)
1174    ///
1175    /// This is used both to restart a crashed server and to manually start a server
1176    /// that has auto_start=false in its configuration.
1177    ///
1178    /// Returns (success, message) tuple
1179    pub fn manual_restart(&mut self, language: &str, file_path: Option<&Path>) -> (bool, String) {
1180        // Clear any existing state
1181        self.clear_cooldown(language);
1182
1183        // Re-enable the language (remove from disabled set)
1184        self.disabled_languages.remove(language);
1185
1186        // Add to allowed languages so it stays active even if auto_start=false
1187        self.allowed_languages.insert(language.to_string());
1188
1189        // Remove existing handles for this language (non-universal)
1190        {
1191            let mut i = 0;
1192            while i < self.handles.len() {
1193                if !self.handles[i].handle.scope().is_universal()
1194                    && self.handles[i].handle.scope().accepts(language)
1195                {
1196                    let sh = self.handles.remove(i);
1197                    fire_and_forget(sh.handle.shutdown());
1198                } else {
1199                    i += 1;
1200                }
1201            }
1202        }
1203
1204        // Spawn new server (bypassing auto_start for user-initiated restart)
1205        if self.force_spawn(language, file_path).is_some() {
1206            let message = format!("LSP server for {} started", language);
1207            tracing::info!("{}", message);
1208            (true, message)
1209        } else {
1210            let message = format!("Failed to start LSP server for {}", language);
1211            tracing::error!("{}", message);
1212            (false, message)
1213        }
1214    }
1215
1216    /// Restart a single server by name for a specific language.
1217    ///
1218    /// Shuts down just that server and re-spawns it from config.
1219    /// Returns (success, message) tuple.
1220    pub fn manual_restart_server(
1221        &mut self,
1222        language: &str,
1223        server_name: &str,
1224        file_path: Option<&Path>,
1225    ) -> (bool, String) {
1226        self.clear_cooldown(language);
1227        self.disabled_languages.remove(language);
1228        self.allowed_languages.insert(language.to_string());
1229
1230        // Find and shut down just the named server
1231        if let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) {
1232            let sh = self.handles.remove(idx);
1233            fire_and_forget(sh.handle.shutdown());
1234        }
1235
1236        // Find the matching config (check per-language first, then universal)
1237        let is_universal = self
1238            .universal_configs
1239            .iter()
1240            .any(|c| c.display_name() == server_name);
1241        let config = if is_universal {
1242            self.universal_configs
1243                .iter()
1244                .find(|c| c.display_name() == server_name)
1245                .cloned()
1246        } else {
1247            self.config
1248                .get(language)
1249                .and_then(|configs| configs.iter().find(|c| c.display_name() == server_name))
1250                .cloned()
1251        };
1252
1253        let Some(config) = config else {
1254            let message = format!(
1255                "No config found for server '{}' ({})",
1256                server_name, language
1257            );
1258            tracing::error!("{}", message);
1259            return (false, message);
1260        };
1261
1262        if config.command.is_empty() {
1263            let message = format!(
1264                "LSP command is empty for {} server '{}'",
1265                language, server_name
1266            );
1267            tracing::error!("{}", message);
1268            return (false, message);
1269        }
1270
1271        let runtime = match self.runtime.as_ref() {
1272            Some(r) => r.clone(),
1273            None => return (false, "No tokio runtime available".to_string()),
1274        };
1275        let async_bridge = match self.async_bridge.as_ref() {
1276            Some(b) => b.clone(),
1277            None => return (false, "No async bridge available".to_string()),
1278        };
1279
1280        let scope = if is_universal {
1281            LanguageScope::all()
1282        } else {
1283            LanguageScope::single(language)
1284        };
1285
1286        match LspHandle::spawn(
1287            &runtime,
1288            &config.command,
1289            &config.args,
1290            config.env.clone(),
1291            scope,
1292            server_name.to_string(),
1293            &async_bridge,
1294            config.process_limits.clone(),
1295            config.language_id_overrides.clone(),
1296        ) {
1297            Ok(handle) => {
1298                let effective_root = if is_universal {
1299                    file_path
1300                        .map(|p| {
1301                            let root = detect_workspace_root(p, &config.root_markers);
1302                            path_to_uri(&root)
1303                        })
1304                        .flatten()
1305                        .or_else(|| self.root_uri.clone())
1306                } else {
1307                    self.resolve_root_uri(language, file_path)
1308                };
1309                if let Err(e) =
1310                    handle.initialize(effective_root, config.initialization_options.clone())
1311                {
1312                    let message = format!(
1313                        "Failed to initialize LSP server '{}' for {}: {}",
1314                        server_name, language, e
1315                    );
1316                    tracing::error!("{}", message);
1317                    return (false, message);
1318                }
1319
1320                let sh = ServerHandle {
1321                    name: server_name.to_string(),
1322                    handle,
1323                    feature_filter: config.feature_filter(),
1324                    capabilities: ServerCapabilitySummary::default(),
1325                };
1326
1327                self.handles.push(sh);
1328
1329                let message = format!("LSP server '{}' for {} started", server_name, language);
1330                tracing::info!("{}", message);
1331                (true, message)
1332            }
1333            Err(e) => {
1334                let message = format!(
1335                    "Failed to start LSP server '{}' for {}: {}",
1336                    server_name, language, e
1337                );
1338                tracing::error!("{}", message);
1339                (false, message)
1340            }
1341        }
1342    }
1343
1344    /// Get the number of recent restart attempts for a language
1345    pub fn restart_attempt_count(&self, language: &str) -> usize {
1346        let now = Instant::now();
1347        let window = Duration::from_secs(RESTART_WINDOW_SECS);
1348        self.restart_attempts
1349            .get(language)
1350            .map(|attempts| {
1351                attempts
1352                    .iter()
1353                    .filter(|t| now.duration_since(**t) < window)
1354                    .count()
1355            })
1356            .unwrap_or(0)
1357    }
1358
1359    /// Get a list of currently running LSP server language labels (deduplicated).
1360    pub fn running_servers(&self) -> Vec<String> {
1361        let mut labels: Vec<String> = self
1362            .handles
1363            .iter()
1364            .map(|sh| sh.handle.scope().label().to_string())
1365            .collect();
1366        labels.sort();
1367        labels.dedup();
1368        labels
1369    }
1370
1371    /// Get the names of all running servers for a given language
1372    pub fn server_names_for_language(&self, language: &str) -> Vec<String> {
1373        self.handles
1374            .iter()
1375            .filter(|sh| sh.handle.scope().accepts(language))
1376            .map(|sh| sh.name.clone())
1377            .collect()
1378    }
1379
1380    /// Check if any LSP server for a language is running and ready to serve requests
1381    pub fn is_server_ready(&self, language: &str) -> bool {
1382        self.handles
1383            .iter()
1384            .filter(|sh| sh.handle.scope().accepts(language))
1385            .any(|sh| sh.handle.state().can_send_requests())
1386    }
1387
1388    /// Shutdown a single server by name for a specific language.
1389    ///
1390    /// Returns true if the server was found and shut down.
1391    /// If this was the last server for the language, marks the language as disabled.
1392    pub fn shutdown_server_by_name(&mut self, language: &str, server_name: &str) -> bool {
1393        let Some(idx) = self.handles.iter().position(|sh| sh.name == server_name) else {
1394            tracing::warn!(
1395                "No running LSP server named '{}' found for {}",
1396                server_name,
1397                language
1398            );
1399            return false;
1400        };
1401
1402        let sh = self.handles.remove(idx);
1403        tracing::info!(
1404            "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1405            sh.name,
1406            language
1407        );
1408        fire_and_forget(sh.handle.shutdown());
1409
1410        // If no more non-universal handles remain for this language, mark it disabled
1411        let has_remaining = self
1412            .handles
1413            .iter()
1414            .any(|sh| !sh.handle.scope().is_universal() && sh.handle.scope().accepts(language));
1415        if !has_remaining {
1416            self.disabled_languages.insert(language.to_string());
1417            self.pending_restarts.remove(language);
1418            self.restart_cooldown.remove(language);
1419            self.allowed_languages.remove(language);
1420        }
1421
1422        true
1423    }
1424
1425    /// Shutdown all servers for a specific language.
1426    ///
1427    /// This marks the language as disabled, preventing auto-restart until the user
1428    /// explicitly restarts it using the restart command.
1429    pub fn shutdown_server(&mut self, language: &str) -> bool {
1430        let mut found = false;
1431        let mut i = 0;
1432        while i < self.handles.len() {
1433            if !self.handles[i].handle.scope().is_universal()
1434                && self.handles[i].handle.scope().accepts(language)
1435            {
1436                let sh = self.handles.remove(i);
1437                tracing::info!(
1438                    "Shutting down LSP server '{}' for {} (disabled until manual restart)",
1439                    sh.name,
1440                    language
1441                );
1442                fire_and_forget(sh.handle.shutdown());
1443                found = true;
1444            } else {
1445                i += 1;
1446            }
1447        }
1448
1449        if found {
1450            self.disabled_languages.insert(language.to_string());
1451            self.pending_restarts.remove(language);
1452            self.restart_cooldown.remove(language);
1453            self.allowed_languages.remove(language);
1454        } else {
1455            tracing::warn!("No running LSP server found for {}", language);
1456        }
1457
1458        found
1459    }
1460
1461    /// Shutdown all language servers (including universal servers)
1462    pub fn shutdown_all(&mut self) {
1463        for sh in &self.handles {
1464            tracing::info!(
1465                "Shutting down LSP server '{}' ({})",
1466                sh.name,
1467                sh.handle.scope().label()
1468            );
1469            fire_and_forget(sh.handle.shutdown());
1470        }
1471        self.handles.clear();
1472    }
1473}
1474
1475impl Drop for LspManager {
1476    fn drop(&mut self) {
1477        self.shutdown_all();
1478    }
1479}
1480
1481/// Helper function to detect language from file path using the config's languages section.
1482///
1483/// Priority order matches the grammar registry (`find_syntax_for_file_with_languages`):
1484/// 1. Exact filename match against `filenames` (highest priority)
1485/// 2. Glob pattern match against `filenames` entries containing wildcards
1486/// 3. File extension match against `extensions` (lowest config-based priority)
1487pub fn detect_language(
1488    path: &std::path::Path,
1489    languages: &std::collections::HashMap<String, crate::config::LanguageConfig>,
1490) -> Option<String> {
1491    use crate::primitives::glob_match::{
1492        filename_glob_matches, is_glob_pattern, is_path_pattern, path_glob_matches,
1493    };
1494
1495    if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1496        // 1. Exact filename match (highest priority)
1497        for (language_name, lang_config) in languages {
1498            if lang_config
1499                .filenames
1500                .iter()
1501                .any(|f| !is_glob_pattern(f) && f == filename)
1502            {
1503                return Some(language_name.clone());
1504            }
1505        }
1506
1507        // 2. Glob pattern match
1508        // Path patterns (containing `/`) match against the full path;
1509        // filename-only patterns match against just the filename.
1510        let path_str = path.to_str().unwrap_or("");
1511        for (language_name, lang_config) in languages {
1512            if lang_config.filenames.iter().any(|f| {
1513                if !is_glob_pattern(f) {
1514                    return false;
1515                }
1516                if is_path_pattern(f) {
1517                    path_glob_matches(f, path_str)
1518                } else {
1519                    filename_glob_matches(f, filename)
1520                }
1521            }) {
1522                return Some(language_name.clone());
1523            }
1524        }
1525    }
1526
1527    // 3. Extension match (lowest priority among config-based detection)
1528    if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
1529        for (language_name, lang_config) in languages {
1530            if lang_config.extensions.iter().any(|ext| ext == extension) {
1531                return Some(language_name.clone());
1532            }
1533        }
1534    }
1535
1536    None
1537}
1538
1539#[cfg(test)]
1540mod tests {
1541    use super::*;
1542    use std::path::Path;
1543
1544    #[test]
1545    fn test_lsp_manager_new() {
1546        let root_uri: Option<Uri> = "file:///test".parse().ok();
1547        let manager = LspManager::new(root_uri.clone());
1548
1549        // Manager should start with no handles
1550        assert_eq!(manager.handles.len(), 0);
1551        assert_eq!(manager.config.len(), 0);
1552        assert!(manager.root_uri.is_some());
1553        assert!(manager.runtime.is_none());
1554        assert!(manager.async_bridge.is_none());
1555    }
1556
1557    #[test]
1558    fn test_lsp_manager_set_language_config() {
1559        let mut manager = LspManager::new(None);
1560
1561        let config = LspServerConfig {
1562            enabled: true,
1563            command: "rust-analyzer".to_string(),
1564            args: vec![],
1565            process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1566            auto_start: false,
1567            initialization_options: None,
1568            env: Default::default(),
1569            language_id_overrides: Default::default(),
1570            name: None,
1571            only_features: None,
1572            except_features: None,
1573            root_markers: Default::default(),
1574        };
1575
1576        manager.set_language_config("rust".to_string(), config);
1577
1578        assert_eq!(manager.config.len(), 1);
1579        assert!(manager.config.contains_key("rust"));
1580        assert!(manager.config.get("rust").unwrap().first().unwrap().enabled);
1581    }
1582
1583    #[test]
1584    fn test_lsp_manager_force_spawn_no_runtime() {
1585        let mut manager = LspManager::new(None);
1586
1587        // Add config for rust
1588        manager.set_language_config(
1589            "rust".to_string(),
1590            LspServerConfig {
1591                enabled: true,
1592                command: "rust-analyzer".to_string(),
1593                args: vec![],
1594                process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1595                auto_start: false,
1596                initialization_options: None,
1597                env: Default::default(),
1598                language_id_overrides: Default::default(),
1599                name: None,
1600                only_features: None,
1601                except_features: None,
1602                root_markers: Default::default(),
1603            },
1604        );
1605
1606        // force_spawn should return None without runtime
1607        let result = manager.force_spawn("rust", None);
1608        assert!(result.is_none());
1609    }
1610
1611    #[test]
1612    fn test_lsp_manager_force_spawn_no_config() {
1613        let rt = tokio::runtime::Runtime::new().unwrap();
1614        let mut manager = LspManager::new(None);
1615        let async_bridge = AsyncBridge::new();
1616
1617        manager.set_runtime(rt.handle().clone(), async_bridge);
1618
1619        // force_spawn should return None for unconfigured language
1620        let result = manager.force_spawn("rust", None);
1621        assert!(result.is_none());
1622    }
1623
1624    #[test]
1625    fn test_lsp_manager_force_spawn_disabled_language() {
1626        let rt = tokio::runtime::Runtime::new().unwrap();
1627        let mut manager = LspManager::new(None);
1628        let async_bridge = AsyncBridge::new();
1629
1630        manager.set_runtime(rt.handle().clone(), async_bridge);
1631
1632        // Add disabled config (command is optional when disabled)
1633        manager.set_language_config(
1634            "rust".to_string(),
1635            LspServerConfig {
1636                enabled: false,
1637                command: String::new(), // command not required when disabled
1638                args: vec![],
1639                process_limits: crate::services::process_limits::ProcessLimits::unlimited(),
1640                auto_start: false,
1641                initialization_options: None,
1642                env: Default::default(),
1643                language_id_overrides: Default::default(),
1644                name: None,
1645                only_features: None,
1646                except_features: None,
1647                root_markers: Default::default(),
1648            },
1649        );
1650
1651        // force_spawn should return None for disabled language
1652        let result = manager.force_spawn("rust", None);
1653        assert!(result.is_none());
1654    }
1655
1656    #[test]
1657    fn test_lsp_manager_shutdown_all() {
1658        let mut manager = LspManager::new(None);
1659
1660        // shutdown_all should not panic even with no handles
1661        manager.shutdown_all();
1662        assert_eq!(manager.handles.len(), 0);
1663    }
1664
1665    fn test_languages() -> std::collections::HashMap<String, crate::config::LanguageConfig> {
1666        let mut languages = std::collections::HashMap::new();
1667        languages.insert(
1668            "rust".to_string(),
1669            crate::config::LanguageConfig {
1670                extensions: vec!["rs".to_string()],
1671                filenames: vec![],
1672                grammar: "rust".to_string(),
1673                comment_prefix: Some("//".to_string()),
1674                auto_indent: true,
1675                auto_close: None,
1676                auto_surround: None,
1677                textmate_grammar: None,
1678                show_whitespace_tabs: false,
1679                line_wrap: None,
1680                wrap_column: None,
1681                page_view: None,
1682                page_width: None,
1683                use_tabs: None,
1684                tab_size: None,
1685                formatter: None,
1686                format_on_save: false,
1687                on_save: vec![],
1688                word_characters: None,
1689            },
1690        );
1691        languages.insert(
1692            "javascript".to_string(),
1693            crate::config::LanguageConfig {
1694                extensions: vec!["js".to_string(), "jsx".to_string()],
1695                filenames: vec![],
1696                grammar: "javascript".to_string(),
1697                comment_prefix: Some("//".to_string()),
1698                auto_indent: true,
1699                auto_close: None,
1700                auto_surround: None,
1701                textmate_grammar: None,
1702                show_whitespace_tabs: false,
1703                line_wrap: None,
1704                wrap_column: None,
1705                page_view: None,
1706                page_width: None,
1707                use_tabs: None,
1708                tab_size: None,
1709                formatter: None,
1710                format_on_save: false,
1711                on_save: vec![],
1712                word_characters: None,
1713            },
1714        );
1715        languages.insert(
1716            "csharp".to_string(),
1717            crate::config::LanguageConfig {
1718                extensions: vec!["cs".to_string()],
1719                filenames: vec![],
1720                grammar: "c_sharp".to_string(),
1721                comment_prefix: Some("//".to_string()),
1722                auto_indent: true,
1723                auto_close: None,
1724                auto_surround: None,
1725                textmate_grammar: None,
1726                show_whitespace_tabs: false,
1727                line_wrap: None,
1728                wrap_column: None,
1729                page_view: None,
1730                page_width: None,
1731                use_tabs: None,
1732                tab_size: None,
1733                formatter: None,
1734                format_on_save: false,
1735                on_save: vec![],
1736                word_characters: None,
1737            },
1738        );
1739        languages
1740    }
1741
1742    #[test]
1743    fn test_detect_language_from_config() {
1744        let languages = test_languages();
1745
1746        // Test configured languages
1747        assert_eq!(
1748            detect_language(Path::new("main.rs"), &languages),
1749            Some("rust".to_string())
1750        );
1751        assert_eq!(
1752            detect_language(Path::new("index.js"), &languages),
1753            Some("javascript".to_string())
1754        );
1755        assert_eq!(
1756            detect_language(Path::new("App.jsx"), &languages),
1757            Some("javascript".to_string())
1758        );
1759        assert_eq!(
1760            detect_language(Path::new("Program.cs"), &languages),
1761            Some("csharp".to_string())
1762        );
1763
1764        // Test unconfigured extensions return None
1765        assert_eq!(detect_language(Path::new("main.py"), &languages), None);
1766        assert_eq!(detect_language(Path::new("file.xyz"), &languages), None);
1767        assert_eq!(detect_language(Path::new("file"), &languages), None);
1768    }
1769
1770    #[test]
1771    fn test_detect_language_no_extension() {
1772        let languages = test_languages();
1773        assert_eq!(detect_language(Path::new("README"), &languages), None);
1774        assert_eq!(detect_language(Path::new("Makefile"), &languages), None);
1775    }
1776
1777    #[test]
1778    fn test_detect_language_path_glob() {
1779        let mut languages = test_languages();
1780        languages.insert(
1781            "shell".to_string(),
1782            crate::config::LanguageConfig {
1783                extensions: vec!["sh".to_string()],
1784                filenames: vec!["/etc/**/rc.*".to_string(), "*rc".to_string()],
1785                grammar: "bash".to_string(),
1786                comment_prefix: Some("#".to_string()),
1787                auto_indent: true,
1788                auto_close: None,
1789                auto_surround: None,
1790                textmate_grammar: None,
1791                show_whitespace_tabs: false,
1792                line_wrap: None,
1793                wrap_column: None,
1794                page_view: None,
1795                page_width: None,
1796                use_tabs: None,
1797                tab_size: None,
1798                formatter: None,
1799                format_on_save: false,
1800                on_save: vec![],
1801                word_characters: None,
1802            },
1803        );
1804
1805        // Path glob: /etc/**/rc.* should match
1806        assert_eq!(
1807            detect_language(Path::new("/etc/rc.conf"), &languages),
1808            Some("shell".to_string())
1809        );
1810        assert_eq!(
1811            detect_language(Path::new("/etc/init/rc.local"), &languages),
1812            Some("shell".to_string())
1813        );
1814        // Path glob should NOT match different root
1815        assert_eq!(detect_language(Path::new("/var/rc.conf"), &languages), None);
1816
1817        // Filename glob: *rc should still work
1818        assert_eq!(
1819            detect_language(Path::new("lfrc"), &languages),
1820            Some("shell".to_string())
1821        );
1822    }
1823
1824    #[test]
1825    fn test_detect_workspace_root_finds_marker_in_parent() {
1826        let tmp = tempfile::tempdir().unwrap();
1827        let project = tmp.path().join("myproject");
1828        let src = project.join("src");
1829        std::fs::create_dir_all(&src).unwrap();
1830        std::fs::write(project.join("Cargo.toml"), "").unwrap();
1831        let file = src.join("main.rs");
1832        std::fs::write(&file, "").unwrap();
1833
1834        let root = detect_workspace_root(&file, &["Cargo.toml".to_string(), ".git".to_string()]);
1835        assert_eq!(root, project);
1836    }
1837
1838    #[test]
1839    fn test_detect_workspace_root_finds_marker_two_levels_up() {
1840        let tmp = tempfile::tempdir().unwrap();
1841        let project = tmp.path().join("myproject");
1842        let deep = project.join("src").join("nested");
1843        std::fs::create_dir_all(&deep).unwrap();
1844        std::fs::write(project.join("Cargo.toml"), "").unwrap();
1845        let file = deep.join("lib.rs");
1846        std::fs::write(&file, "").unwrap();
1847
1848        let root = detect_workspace_root(&file, &["Cargo.toml".to_string()]);
1849        assert_eq!(root, project);
1850    }
1851
1852    #[test]
1853    fn test_detect_workspace_root_no_marker_returns_parent() {
1854        let tmp = tempfile::tempdir().unwrap();
1855        let dir = tmp.path().join("somedir");
1856        std::fs::create_dir_all(&dir).unwrap();
1857        let file = dir.join("file.txt");
1858        std::fs::write(&file, "").unwrap();
1859
1860        let root = detect_workspace_root(&file, &["nonexistent_marker".to_string()]);
1861        assert_eq!(root, dir);
1862    }
1863
1864    #[test]
1865    fn test_detect_workspace_root_empty_markers_returns_parent() {
1866        let tmp = tempfile::tempdir().unwrap();
1867        let dir = tmp.path().join("somedir");
1868        std::fs::create_dir_all(&dir).unwrap();
1869        let file = dir.join("file.txt");
1870        std::fs::write(&file, "").unwrap();
1871
1872        let root = detect_workspace_root(&file, &[]);
1873        assert_eq!(root, dir);
1874    }
1875
1876    #[test]
1877    fn test_detect_workspace_root_directory_marker() {
1878        let tmp = tempfile::tempdir().unwrap();
1879        let project = tmp.path().join("myproject");
1880        let src = project.join("src");
1881        std::fs::create_dir_all(&src).unwrap();
1882        std::fs::create_dir_all(project.join(".git")).unwrap();
1883        let file = src.join("main.rs");
1884        std::fs::write(&file, "").unwrap();
1885
1886        let root = detect_workspace_root(&file, &[".git".to_string()]);
1887        assert_eq!(root, project);
1888    }
1889
1890    #[test]
1891    fn test_path_to_uri_basic() {
1892        let uri = path_to_uri(Path::new("/tmp/test")).unwrap();
1893        assert_eq!(uri.as_str(), "file:///tmp/test");
1894    }
1895
1896    #[test]
1897    fn test_path_to_uri_with_spaces() {
1898        let uri = path_to_uri(Path::new("/tmp/my project/src")).unwrap();
1899        assert_eq!(uri.as_str(), "file:///tmp/my%20project/src");
1900    }
1901}