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