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///         resolved_versions: &std::collections::HashMap<String, String>,
172///         uri: &Uri,
173///     ) -> Vec<Diagnostic> {
174///         let _ = resolved_versions; // Use resolved versions for lock file support
175///         vec![]
176///     }
177///
178///     async fn generate_completions(
179///         &self,
180///         parse_result: &dyn ParseResult,
181///         position: Position,
182///         content: &str,
183///     ) -> Vec<CompletionItem> {
184///         vec![]
185///     }
186///
187///     fn as_any(&self) -> &dyn Any {
188///         self
189///     }
190/// }
191/// ```
192#[async_trait]
193pub trait Ecosystem: Send + Sync {
194    /// Unique identifier (e.g., "cargo", "npm", "pypi")
195    ///
196    /// This identifier is used for ecosystem registration and routing.
197    fn id(&self) -> &'static str;
198
199    /// Human-readable name (e.g., "Cargo (Rust)", "npm (JavaScript)")
200    ///
201    /// This name is displayed in diagnostic messages and logs.
202    fn display_name(&self) -> &'static str;
203
204    /// Manifest filenames this ecosystem handles (e.g., ["Cargo.toml"])
205    ///
206    /// The ecosystem registry uses these filenames to route file URIs
207    /// to the appropriate ecosystem implementation.
208    fn manifest_filenames(&self) -> &[&'static str];
209
210    /// Lock file filenames this ecosystem uses (e.g., ["Cargo.lock"])
211    ///
212    /// Used for file watching - LSP will monitor changes to these files
213    /// and refresh UI when they change. Returns empty slice if ecosystem
214    /// doesn't use lock files.
215    ///
216    /// # Default Implementation
217    ///
218    /// Returns empty slice by default, indicating no lock files are used.
219    fn lockfile_filenames(&self) -> &[&'static str] {
220        &[]
221    }
222
223    /// Parse a manifest file and return parsed result
224    ///
225    /// # Arguments
226    ///
227    /// * `content` - Raw file content
228    /// * `uri` - Document URI for position tracking
229    ///
230    /// # Errors
231    ///
232    /// Returns error if manifest cannot be parsed
233    async fn parse_manifest(
234        &self,
235        content: &str,
236        uri: &Uri,
237    ) -> crate::error::Result<Box<dyn ParseResult>>;
238
239    /// Get the registry client for this ecosystem
240    ///
241    /// The registry provides version lookup and package search capabilities.
242    fn registry(&self) -> Arc<dyn Registry>;
243
244    /// Get the lock file provider for this ecosystem.
245    ///
246    /// Returns `None` if the ecosystem doesn't support lock files.
247    /// Lock files provide resolved dependency versions without network requests.
248    fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
249        None
250    }
251
252    /// Generate inlay hints for the document
253    ///
254    /// Inlay hints show additional version information inline in the editor.
255    ///
256    /// # Arguments
257    ///
258    /// * `parse_result` - Parsed dependencies from manifest
259    /// * `cached_versions` - Pre-fetched version information (name -> latest version from registry)
260    /// * `resolved_versions` - Resolved versions from lock file (name -> locked version)
261    /// * `loading_state` - Current loading state for registry data
262    /// * `config` - User configuration for hint display
263    async fn generate_inlay_hints(
264        &self,
265        parse_result: &dyn ParseResult,
266        cached_versions: &std::collections::HashMap<String, String>,
267        resolved_versions: &std::collections::HashMap<String, String>,
268        loading_state: crate::LoadingState,
269        config: &EcosystemConfig,
270    ) -> Vec<InlayHint>;
271
272    /// Generate hover information for a position
273    ///
274    /// Shows package information when hovering over a dependency name or version.
275    ///
276    /// # Arguments
277    ///
278    /// * `parse_result` - Parsed dependencies from manifest
279    /// * `position` - Cursor position in document
280    /// * `cached_versions` - Pre-fetched latest version information from registry
281    /// * `resolved_versions` - Resolved versions from lock file (takes precedence for "Current" display)
282    async fn generate_hover(
283        &self,
284        parse_result: &dyn ParseResult,
285        position: Position,
286        cached_versions: &std::collections::HashMap<String, String>,
287        resolved_versions: &std::collections::HashMap<String, String>,
288    ) -> Option<Hover>;
289
290    /// Generate code actions for a position
291    ///
292    /// Code actions provide quick fixes like "Update to latest version".
293    ///
294    /// # Arguments
295    ///
296    /// * `parse_result` - Parsed dependencies from manifest
297    /// * `position` - Cursor position in document
298    /// * `cached_versions` - Pre-fetched version information
299    /// * `uri` - Document URI for workspace edits
300    async fn generate_code_actions(
301        &self,
302        parse_result: &dyn ParseResult,
303        position: Position,
304        cached_versions: &std::collections::HashMap<String, String>,
305        uri: &Uri,
306    ) -> Vec<CodeAction>;
307
308    /// Generate diagnostics for the document
309    ///
310    /// Diagnostics highlight issues like outdated dependencies or unknown packages.
311    ///
312    /// # Arguments
313    ///
314    /// * `parse_result` - Parsed dependencies from manifest
315    /// * `cached_versions` - Pre-fetched latest version information from registry
316    /// * `resolved_versions` - Resolved versions from lock file
317    /// * `uri` - Document URI for diagnostic reporting
318    async fn generate_diagnostics(
319        &self,
320        parse_result: &dyn ParseResult,
321        cached_versions: &std::collections::HashMap<String, String>,
322        resolved_versions: &std::collections::HashMap<String, String>,
323        uri: &Uri,
324    ) -> Vec<Diagnostic>;
325
326    /// Generate completions for a position
327    ///
328    /// Provides autocomplete suggestions for package names and versions.
329    ///
330    /// # Arguments
331    ///
332    /// * `parse_result` - Parsed dependencies from manifest
333    /// * `position` - Cursor position in document
334    /// * `content` - Full document content for context analysis
335    async fn generate_completions(
336        &self,
337        parse_result: &dyn ParseResult,
338        position: Position,
339        content: &str,
340    ) -> Vec<CompletionItem>;
341
342    /// Support for downcasting to concrete ecosystem type
343    ///
344    /// This allows ecosystem-specific operations when needed.
345    fn as_any(&self) -> &dyn Any;
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_ecosystem_config_default() {
354        let config = EcosystemConfig::default();
355        assert!(config.show_up_to_date_hints);
356        assert_eq!(config.up_to_date_text, "✅");
357        assert_eq!(config.needs_update_text, "❌ {}");
358    }
359
360    #[test]
361    fn test_ecosystem_config_custom() {
362        let config = EcosystemConfig {
363            show_up_to_date_hints: false,
364            up_to_date_text: "OK".to_string(),
365            needs_update_text: "Update to {}".to_string(),
366            loading_text: "Loading...".to_string(),
367            show_loading_hints: false,
368        };
369        assert!(!config.show_up_to_date_hints);
370        assert_eq!(config.up_to_date_text, "OK");
371        assert_eq!(config.needs_update_text, "Update to {}");
372    }
373
374    #[test]
375    fn test_ecosystem_config_clone() {
376        let config1 = EcosystemConfig::default();
377        let config2 = config1.clone();
378        assert_eq!(config1.up_to_date_text, config2.up_to_date_text);
379        assert_eq!(config1.show_up_to_date_hints, config2.show_up_to_date_hints);
380        assert_eq!(config1.needs_update_text, config2.needs_update_text);
381    }
382
383    #[test]
384    fn test_dependency_default_features() {
385        struct MockDep;
386        impl Dependency for MockDep {
387            fn name(&self) -> &'static str {
388                "test"
389            }
390            fn name_range(&self) -> tower_lsp_server::ls_types::Range {
391                tower_lsp_server::ls_types::Range::default()
392            }
393            fn version_requirement(&self) -> Option<&str> {
394                None
395            }
396            fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
397                None
398            }
399            fn source(&self) -> crate::parser::DependencySource {
400                crate::parser::DependencySource::Registry
401            }
402            fn as_any(&self) -> &dyn std::any::Any {
403                self
404            }
405        }
406
407        let dep = MockDep;
408        assert_eq!(dep.features(), &[] as &[String]);
409    }
410}