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