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