deps_core/
ecosystem.rs

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