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}