Skip to main content

deps_core/
ecosystem.rs

1use std::any::Any;
2use std::pin::Pin;
3use std::sync::Arc;
4use tower_lsp_server::ls_types::{
5    CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position, Uri,
6};
7
8use crate::{Registry, lsp_helpers::EcosystemFormatter};
9
10pub mod private {
11    pub trait Sealed {}
12}
13
14pub type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
15
16/// Parse result trait containing dependencies and metadata.
17///
18/// Implementations hold ecosystem-specific dependency types
19/// but expose them through trait object interfaces.
20pub trait ParseResult: Send + Sync {
21    /// All dependencies found in the manifest
22    fn dependencies(&self) -> Vec<&dyn Dependency>;
23
24    /// Workspace root path (for monorepo support)
25    fn workspace_root(&self) -> Option<&std::path::Path>;
26
27    /// Document URI
28    fn uri(&self) -> &Uri;
29
30    /// Downcast to concrete type for ecosystem-specific operations
31    fn as_any(&self) -> &dyn Any;
32}
33
34/// Generic dependency trait.
35///
36/// All parsed dependencies must implement this for generic handler access.
37pub trait Dependency: Send + Sync {
38    /// Package name
39    fn name(&self) -> &str;
40
41    /// LSP range of the dependency name
42    fn name_range(&self) -> tower_lsp_server::ls_types::Range;
43
44    /// Version requirement string (e.g., "^1.0", ">=2.0")
45    fn version_requirement(&self) -> Option<&str>;
46
47    /// LSP range of the version string
48    fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range>;
49
50    /// Dependency source (registry, git, path)
51    fn source(&self) -> crate::parser::DependencySource;
52
53    /// Feature flags (ecosystem-specific, empty if not supported)
54    fn features(&self) -> &[String] {
55        &[]
56    }
57
58    /// Downcast to concrete type
59    fn as_any(&self) -> &dyn Any;
60}
61
62/// Configuration for LSP inlay hints feature.
63#[derive(Debug, Clone)]
64pub struct EcosystemConfig {
65    /// Whether to show inlay hints for up-to-date dependencies
66    pub show_up_to_date_hints: bool,
67    /// Text to display for up-to-date dependencies
68    pub up_to_date_text: String,
69    /// Text to display for dependencies needing updates (use {} for version placeholder)
70    pub needs_update_text: String,
71    /// Text to display while loading registry data
72    pub loading_text: String,
73    /// Whether to show loading hints in inlay hints
74    pub show_loading_hints: bool,
75}
76
77impl Default for EcosystemConfig {
78    fn default() -> Self {
79        Self {
80            show_up_to_date_hints: true,
81            up_to_date_text: "✅".to_string(),
82            needs_update_text: "❌ {}".to_string(),
83            loading_text: "⏳".to_string(),
84            show_loading_hints: true,
85        }
86    }
87}
88
89/// Main trait that all ecosystem implementations must implement.
90///
91/// Each ecosystem (Cargo, npm, PyPI, etc.) provides its own implementation.
92/// This trait defines the contract for parsing manifests, fetching registry data,
93/// and generating LSP responses.
94///
95/// # Type Erasure
96///
97/// This trait uses `Box<dyn Trait>` instead of associated types to allow
98/// runtime polymorphism and dynamic ecosystem registration.
99///
100/// # Examples
101///
102/// ```no_run
103/// use deps_core::{Ecosystem, ParseResult, Registry, EcosystemConfig};
104/// use deps_core::lsp_helpers::EcosystemFormatter;
105/// use std::sync::Arc;
106/// use std::any::Any;
107/// use tower_lsp_server::ls_types::{Uri, CompletionItem, Position};
108///
109/// struct MyFormatter;
110/// impl EcosystemFormatter for MyFormatter {
111///     fn format_version_for_text_edit(&self, version: &str) -> String { version.to_string() }
112///     fn package_url(&self, name: &str) -> String { format!("https://example.com/{name}") }
113/// }
114///
115/// struct MyEcosystem {
116///     registry: Arc<dyn Registry>,
117///     formatter: MyFormatter,
118/// }
119///
120/// impl deps_core::ecosystem::private::Sealed for MyEcosystem {}
121///
122/// impl Ecosystem for MyEcosystem {
123///     fn id(&self) -> &'static str { "my-ecosystem" }
124///     fn display_name(&self) -> &'static str { "My Ecosystem" }
125///     fn manifest_filenames(&self) -> &[&'static str] { &["my-manifest.toml"] }
126///
127///     fn parse_manifest<'a>(
128///         &'a self,
129///         _content: &'a str,
130///         _uri: &'a Uri,
131///     ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::error::Result<Box<dyn ParseResult>>> {
132///         Box::pin(async move { todo!() })
133///     }
134///
135///     fn registry(&self) -> Arc<dyn Registry> { self.registry.clone() }
136///
137///     fn formatter(&self) -> &dyn EcosystemFormatter { &self.formatter }
138///
139///     fn generate_completions<'a>(
140///         &'a self,
141///         _parse_result: &'a dyn ParseResult,
142///         _position: Position,
143///         _content: &'a str,
144///     ) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
145///         Box::pin(async move { vec![] })
146///     }
147///
148///     fn as_any(&self) -> &dyn Any { self }
149/// }
150/// ```
151pub trait Ecosystem: Send + Sync + private::Sealed {
152    /// Unique identifier (e.g., "cargo", "npm", "pypi")
153    ///
154    /// This identifier is used for ecosystem registration and routing.
155    fn id(&self) -> &'static str;
156
157    /// Human-readable name (e.g., "Cargo (Rust)", "npm (JavaScript)")
158    ///
159    /// This name is displayed in diagnostic messages and logs.
160    fn display_name(&self) -> &'static str;
161
162    /// Manifest filenames this ecosystem handles (e.g., ["Cargo.toml"])
163    ///
164    /// The ecosystem registry uses these filenames to route file URIs
165    /// to the appropriate ecosystem implementation.
166    fn manifest_filenames(&self) -> &[&'static str];
167
168    /// Lock file filenames this ecosystem uses (e.g., ["Cargo.lock"])
169    ///
170    /// Used for file watching - LSP will monitor changes to these files
171    /// and refresh UI when they change. Returns empty slice if ecosystem
172    /// doesn't use lock files.
173    ///
174    /// # Default Implementation
175    ///
176    /// Returns empty slice by default, indicating no lock files are used.
177    fn lockfile_filenames(&self) -> &[&'static str] {
178        &[]
179    }
180
181    /// Parse a manifest file and return parsed result
182    ///
183    /// # Arguments
184    ///
185    /// * `content` - Raw file content
186    /// * `uri` - Document URI for position tracking
187    ///
188    /// # Errors
189    ///
190    /// Returns error if manifest cannot be parsed
191    fn parse_manifest<'a>(
192        &'a self,
193        content: &'a str,
194        uri: &'a Uri,
195    ) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>>;
196
197    /// Get the registry client for this ecosystem
198    ///
199    /// The registry provides version lookup and package search capabilities.
200    fn registry(&self) -> Arc<dyn Registry>;
201
202    /// Get the lock file provider for this ecosystem.
203    ///
204    /// Returns `None` if the ecosystem doesn't support lock files.
205    /// Lock files provide resolved dependency versions without network requests.
206    fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
207        None
208    }
209
210    /// Get the ecosystem-specific formatter for LSP response generation.
211    ///
212    /// The formatter handles version comparison, package URLs, and text formatting.
213    /// Override this to customize LSP response generation.
214    fn formatter(&self) -> &dyn EcosystemFormatter;
215
216    /// Generate inlay hints for the document.
217    ///
218    /// Default implementation delegates to `lsp_helpers::generate_inlay_hints`
219    /// using `self.formatter()`. Override only if custom behavior is needed.
220    fn generate_inlay_hints<'a>(
221        &'a self,
222        parse_result: &'a dyn ParseResult,
223        cached_versions: &'a std::collections::HashMap<String, String>,
224        resolved_versions: &'a std::collections::HashMap<String, String>,
225        loading_state: crate::LoadingState,
226        config: &'a EcosystemConfig,
227    ) -> BoxFuture<'a, Vec<InlayHint>> {
228        Box::pin(async move {
229            crate::lsp_helpers::generate_inlay_hints(
230                parse_result,
231                cached_versions,
232                resolved_versions,
233                loading_state,
234                config,
235                self.formatter(),
236            )
237        })
238    }
239
240    /// Generate hover information for a position.
241    ///
242    /// Default implementation delegates to `lsp_helpers::generate_hover`
243    /// using `self.formatter()` and `self.registry()`.
244    fn generate_hover<'a>(
245        &'a self,
246        parse_result: &'a dyn ParseResult,
247        position: Position,
248        cached_versions: &'a std::collections::HashMap<String, String>,
249        resolved_versions: &'a std::collections::HashMap<String, String>,
250    ) -> BoxFuture<'a, Option<Hover>> {
251        Box::pin(async move {
252            let registry = self.registry();
253            crate::lsp_helpers::generate_hover(
254                parse_result,
255                position,
256                cached_versions,
257                resolved_versions,
258                registry.as_ref(),
259                self.formatter(),
260            )
261            .await
262        })
263    }
264
265    /// Generate code actions for a position.
266    ///
267    /// Default implementation delegates to `lsp_helpers::generate_code_actions`
268    /// using `self.formatter()` and `self.registry()`.
269    fn generate_code_actions<'a>(
270        &'a self,
271        parse_result: &'a dyn ParseResult,
272        position: Position,
273        _cached_versions: &'a std::collections::HashMap<String, String>,
274        uri: &'a Uri,
275    ) -> BoxFuture<'a, Vec<CodeAction>> {
276        Box::pin(async move {
277            let registry = self.registry();
278            crate::lsp_helpers::generate_code_actions(
279                parse_result,
280                position,
281                uri,
282                registry.as_ref(),
283                self.formatter(),
284            )
285            .await
286        })
287    }
288
289    /// Generate diagnostics for the document.
290    ///
291    /// Default implementation delegates to `lsp_helpers::generate_diagnostics_from_cache`
292    /// using `self.formatter()`.
293    fn generate_diagnostics<'a>(
294        &'a self,
295        parse_result: &'a dyn ParseResult,
296        cached_versions: &'a std::collections::HashMap<String, String>,
297        resolved_versions: &'a std::collections::HashMap<String, String>,
298        _uri: &'a Uri,
299    ) -> BoxFuture<'a, Vec<Diagnostic>> {
300        Box::pin(async move {
301            crate::lsp_helpers::generate_diagnostics_from_cache(
302                parse_result,
303                cached_versions,
304                resolved_versions,
305                self.formatter(),
306            )
307        })
308    }
309
310    /// Generate completions for a position.
311    ///
312    /// Provides autocomplete suggestions for package names and versions.
313    fn generate_completions<'a>(
314        &'a self,
315        parse_result: &'a dyn ParseResult,
316        position: Position,
317        content: &'a str,
318    ) -> BoxFuture<'a, Vec<CompletionItem>>;
319
320    /// Support for downcasting to concrete ecosystem type
321    ///
322    /// This allows ecosystem-specific operations when needed.
323    fn as_any(&self) -> &dyn Any;
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_ecosystem_config_default() {
332        let config = EcosystemConfig::default();
333        assert!(config.show_up_to_date_hints);
334        assert_eq!(config.up_to_date_text, "✅");
335        assert_eq!(config.needs_update_text, "❌ {}");
336    }
337
338    #[test]
339    fn test_ecosystem_config_custom() {
340        let config = EcosystemConfig {
341            show_up_to_date_hints: false,
342            up_to_date_text: "OK".to_string(),
343            needs_update_text: "Update to {}".to_string(),
344            loading_text: "Loading...".to_string(),
345            show_loading_hints: false,
346        };
347        assert!(!config.show_up_to_date_hints);
348        assert_eq!(config.up_to_date_text, "OK");
349        assert_eq!(config.needs_update_text, "Update to {}");
350    }
351
352    #[test]
353    fn test_ecosystem_config_clone() {
354        let config1 = EcosystemConfig::default();
355        let config2 = config1.clone();
356        assert_eq!(config1.up_to_date_text, config2.up_to_date_text);
357        assert_eq!(config1.show_up_to_date_hints, config2.show_up_to_date_hints);
358        assert_eq!(config1.needs_update_text, config2.needs_update_text);
359    }
360
361    #[test]
362    fn test_dependency_default_features() {
363        struct MockDep;
364        impl Dependency for MockDep {
365            fn name(&self) -> &'static str {
366                "test"
367            }
368            fn name_range(&self) -> tower_lsp_server::ls_types::Range {
369                tower_lsp_server::ls_types::Range::default()
370            }
371            fn version_requirement(&self) -> Option<&str> {
372                None
373            }
374            fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
375                None
376            }
377            fn source(&self) -> crate::parser::DependencySource {
378                crate::parser::DependencySource::Registry
379            }
380            fn as_any(&self) -> &dyn std::any::Any {
381                self
382            }
383        }
384
385        let dep = MockDep;
386        assert_eq!(dep.features(), &[] as &[String]);
387    }
388}