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    /// Parse a manifest file and return parsed result
202    ///
203    /// # Arguments
204    ///
205    /// * `content` - Raw file content
206    /// * `uri` - Document URI for position tracking
207    ///
208    /// # Errors
209    ///
210    /// Returns error if manifest cannot be parsed
211    async fn parse_manifest(
212        &self,
213        content: &str,
214        uri: &Uri,
215    ) -> crate::error::Result<Box<dyn ParseResult>>;
216
217    /// Get the registry client for this ecosystem
218    ///
219    /// The registry provides version lookup and package search capabilities.
220    fn registry(&self) -> Arc<dyn Registry>;
221
222    /// Get the lock file provider for this ecosystem.
223    ///
224    /// Returns `None` if the ecosystem doesn't support lock files.
225    /// Lock files provide resolved dependency versions without network requests.
226    fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
227        None
228    }
229
230    /// Generate inlay hints for the document
231    ///
232    /// Inlay hints show additional version information inline in the editor.
233    ///
234    /// # Arguments
235    ///
236    /// * `parse_result` - Parsed dependencies from manifest
237    /// * `cached_versions` - Pre-fetched version information (name -> latest version from registry)
238    /// * `resolved_versions` - Resolved versions from lock file (name -> locked version)
239    /// * `config` - User configuration for hint display
240    async fn generate_inlay_hints(
241        &self,
242        parse_result: &dyn ParseResult,
243        cached_versions: &std::collections::HashMap<String, String>,
244        resolved_versions: &std::collections::HashMap<String, String>,
245        config: &EcosystemConfig,
246    ) -> Vec<InlayHint>;
247
248    /// Generate hover information for a position
249    ///
250    /// Shows package information when hovering over a dependency name or version.
251    ///
252    /// # Arguments
253    ///
254    /// * `parse_result` - Parsed dependencies from manifest
255    /// * `position` - Cursor position in document
256    /// * `cached_versions` - Pre-fetched latest version information from registry
257    /// * `resolved_versions` - Resolved versions from lock file (takes precedence for "Current" display)
258    async fn generate_hover(
259        &self,
260        parse_result: &dyn ParseResult,
261        position: Position,
262        cached_versions: &std::collections::HashMap<String, String>,
263        resolved_versions: &std::collections::HashMap<String, String>,
264    ) -> Option<Hover>;
265
266    /// Generate code actions for a position
267    ///
268    /// Code actions provide quick fixes like "Update to latest version".
269    ///
270    /// # Arguments
271    ///
272    /// * `parse_result` - Parsed dependencies from manifest
273    /// * `position` - Cursor position in document
274    /// * `cached_versions` - Pre-fetched version information
275    /// * `uri` - Document URI for workspace edits
276    async fn generate_code_actions(
277        &self,
278        parse_result: &dyn ParseResult,
279        position: Position,
280        cached_versions: &std::collections::HashMap<String, String>,
281        uri: &Uri,
282    ) -> Vec<CodeAction>;
283
284    /// Generate diagnostics for the document
285    ///
286    /// Diagnostics highlight issues like outdated dependencies or unknown packages.
287    ///
288    /// # Arguments
289    ///
290    /// * `parse_result` - Parsed dependencies from manifest
291    /// * `cached_versions` - Pre-fetched version information
292    /// * `uri` - Document URI for diagnostic reporting
293    async fn generate_diagnostics(
294        &self,
295        parse_result: &dyn ParseResult,
296        cached_versions: &std::collections::HashMap<String, String>,
297        uri: &Uri,
298    ) -> Vec<Diagnostic>;
299
300    /// Generate completions for a position
301    ///
302    /// Provides autocomplete suggestions for package names and versions.
303    ///
304    /// # Arguments
305    ///
306    /// * `parse_result` - Parsed dependencies from manifest
307    /// * `position` - Cursor position in document
308    /// * `content` - Full document content for context analysis
309    async fn generate_completions(
310        &self,
311        parse_result: &dyn ParseResult,
312        position: Position,
313        content: &str,
314    ) -> Vec<CompletionItem>;
315
316    /// Support for downcasting to concrete ecosystem type
317    ///
318    /// This allows ecosystem-specific operations when needed.
319    fn as_any(&self) -> &dyn Any;
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_ecosystem_config_default() {
328        let config = EcosystemConfig::default();
329        assert!(config.show_up_to_date_hints);
330        assert_eq!(config.up_to_date_text, "✅");
331        assert_eq!(config.needs_update_text, "❌ {}");
332    }
333
334    #[test]
335    fn test_ecosystem_config_custom() {
336        let config = EcosystemConfig {
337            show_up_to_date_hints: false,
338            up_to_date_text: "OK".to_string(),
339            needs_update_text: "Update to {}".to_string(),
340        };
341        assert!(!config.show_up_to_date_hints);
342        assert_eq!(config.up_to_date_text, "OK");
343        assert_eq!(config.needs_update_text, "Update to {}");
344    }
345
346    #[test]
347    fn test_ecosystem_config_clone() {
348        let config1 = EcosystemConfig::default();
349        let config2 = config1.clone();
350        assert_eq!(config1.up_to_date_text, config2.up_to_date_text);
351        assert_eq!(config1.show_up_to_date_hints, config2.show_up_to_date_hints);
352        assert_eq!(config1.needs_update_text, config2.needs_update_text);
353    }
354
355    #[test]
356    fn test_dependency_default_features() {
357        struct MockDep;
358        impl Dependency for MockDep {
359            fn name(&self) -> &str {
360                "test"
361            }
362            fn name_range(&self) -> tower_lsp_server::ls_types::Range {
363                tower_lsp_server::ls_types::Range::default()
364            }
365            fn version_requirement(&self) -> Option<&str> {
366                None
367            }
368            fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
369                None
370            }
371            fn source(&self) -> crate::parser::DependencySource {
372                crate::parser::DependencySource::Registry
373            }
374            fn as_any(&self) -> &dyn std::any::Any {
375                self
376            }
377        }
378
379        let dep = MockDep;
380        assert_eq!(dep.features(), &[] as &[String]);
381    }
382}