deps_core/ecosystem.rs
1use async_trait::async_trait;
2use std::any::Any;
3use std::sync::Arc;
4use tower_lsp::lsp_types::{
5 CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position, Url,
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) -> &Url;
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::lsp_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::lsp_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::lsp_types::{Url, 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: &Url,
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: &Url,
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: &Url,
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: &Url,
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: &Url,
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: &Url,
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::lsp_types::Range {
363 tower_lsp::lsp_types::Range::default()
364 }
365 fn version_requirement(&self) -> Option<&str> {
366 None
367 }
368 fn version_range(&self) -> Option<tower_lsp::lsp_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}