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}