1use async_trait::async_trait;
7use std::any::Any;
8use std::collections::HashMap;
9use std::sync::Arc;
10use tower_lsp_server::ls_types::{
11 CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position, Uri,
12};
13
14use deps_core::{
15 Ecosystem, EcosystemConfig, ParseResult as ParseResultTrait, Registry, Result, lsp_helpers,
16};
17
18use crate::formatter::NpmFormatter;
19use crate::registry::NpmRegistry;
20
21pub struct NpmEcosystem {
31 registry: Arc<NpmRegistry>,
32 formatter: NpmFormatter,
33}
34
35impl NpmEcosystem {
36 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
38 Self {
39 registry: Arc::new(NpmRegistry::new(cache)),
40 formatter: NpmFormatter,
41 }
42 }
43
44 async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> {
48 use deps_core::completion::build_package_completion;
49
50 if prefix.len() < 2 || prefix.len() > 100 {
52 return vec![];
53 }
54
55 let results = match self.registry.search(prefix, 20).await {
57 Ok(r) => r,
58 Err(e) => {
59 tracing::warn!("Package search failed for '{}': {}", prefix, e);
60 return vec![];
61 }
62 };
63
64 let insert_range = tower_lsp_server::ls_types::Range::default();
66
67 results
68 .into_iter()
69 .map(|metadata| {
70 let boxed: Box<dyn deps_core::Metadata> = Box::new(metadata);
71 build_package_completion(boxed.as_ref(), insert_range)
72 })
73 .collect()
74 }
75
76 async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
77 deps_core::completion::complete_versions_generic(
78 self.registry.as_ref(),
79 package_name,
80 prefix,
81 &['^', '~', '=', '<', '>', '*'],
82 )
83 .await
84 }
85}
86
87#[async_trait]
88impl Ecosystem for NpmEcosystem {
89 fn id(&self) -> &'static str {
90 "npm"
91 }
92
93 fn display_name(&self) -> &'static str {
94 "npm (JavaScript)"
95 }
96
97 fn manifest_filenames(&self) -> &[&'static str] {
98 &["package.json"]
99 }
100
101 fn lockfile_filenames(&self) -> &[&'static str] {
102 &["package-lock.json"]
103 }
104
105 async fn parse_manifest(&self, content: &str, uri: &Uri) -> Result<Box<dyn ParseResultTrait>> {
106 let result = crate::parser::parse_package_json(content, uri)?;
107 Ok(Box::new(result))
108 }
109
110 fn registry(&self) -> Arc<dyn Registry> {
111 self.registry.clone() as Arc<dyn Registry>
112 }
113
114 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
115 Some(Arc::new(crate::lockfile::NpmLockParser))
116 }
117
118 async fn generate_inlay_hints(
119 &self,
120 parse_result: &dyn ParseResultTrait,
121 cached_versions: &HashMap<String, String>,
122 resolved_versions: &HashMap<String, String>,
123 loading_state: deps_core::LoadingState,
124 config: &EcosystemConfig,
125 ) -> Vec<InlayHint> {
126 lsp_helpers::generate_inlay_hints(
127 parse_result,
128 cached_versions,
129 resolved_versions,
130 loading_state,
131 config,
132 &self.formatter,
133 )
134 }
135
136 async fn generate_hover(
137 &self,
138 parse_result: &dyn ParseResultTrait,
139 position: Position,
140 cached_versions: &HashMap<String, String>,
141 resolved_versions: &HashMap<String, String>,
142 ) -> Option<Hover> {
143 lsp_helpers::generate_hover(
144 parse_result,
145 position,
146 cached_versions,
147 resolved_versions,
148 self.registry.as_ref(),
149 &self.formatter,
150 )
151 .await
152 }
153
154 async fn generate_code_actions(
155 &self,
156 parse_result: &dyn ParseResultTrait,
157 position: Position,
158 _cached_versions: &HashMap<String, String>,
159 uri: &Uri,
160 ) -> Vec<CodeAction> {
161 lsp_helpers::generate_code_actions(
162 parse_result,
163 position,
164 uri,
165 self.registry.as_ref(),
166 &self.formatter,
167 )
168 .await
169 }
170
171 async fn generate_diagnostics(
172 &self,
173 parse_result: &dyn ParseResultTrait,
174 cached_versions: &HashMap<String, String>,
175 resolved_versions: &HashMap<String, String>,
176 _uri: &Uri,
177 ) -> Vec<Diagnostic> {
178 lsp_helpers::generate_diagnostics_from_cache(
179 parse_result,
180 cached_versions,
181 resolved_versions,
182 &self.formatter,
183 )
184 }
185
186 async fn generate_completions(
187 &self,
188 parse_result: &dyn ParseResultTrait,
189 position: Position,
190 content: &str,
191 ) -> Vec<CompletionItem> {
192 use deps_core::completion::{CompletionContext, detect_completion_context};
193
194 let context = detect_completion_context(parse_result, position, content);
195
196 match context {
197 CompletionContext::PackageName { prefix } => self.complete_package_names(&prefix).await,
198 CompletionContext::Version {
199 package_name,
200 prefix,
201 } => self.complete_versions(&package_name, &prefix).await,
202 CompletionContext::Feature { .. } => vec![],
204 CompletionContext::None => vec![],
205 }
206 }
207
208 fn as_any(&self) -> &dyn Any {
209 self
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_ecosystem_id() {
219 let cache = Arc::new(deps_core::HttpCache::new());
220 let ecosystem = NpmEcosystem::new(cache);
221 assert_eq!(ecosystem.id(), "npm");
222 }
223
224 #[test]
225 fn test_ecosystem_display_name() {
226 let cache = Arc::new(deps_core::HttpCache::new());
227 let ecosystem = NpmEcosystem::new(cache);
228 assert_eq!(ecosystem.display_name(), "npm (JavaScript)");
229 }
230
231 #[test]
232 fn test_ecosystem_manifest_filenames() {
233 let cache = Arc::new(deps_core::HttpCache::new());
234 let ecosystem = NpmEcosystem::new(cache);
235 assert_eq!(ecosystem.manifest_filenames(), &["package.json"]);
236 }
237
238 #[test]
239 fn test_ecosystem_lockfile_filenames() {
240 let cache = Arc::new(deps_core::HttpCache::new());
241 let ecosystem = NpmEcosystem::new(cache);
242 assert_eq!(ecosystem.lockfile_filenames(), &["package-lock.json"]);
243 }
244
245 #[test]
246 fn test_as_any() {
247 let cache = Arc::new(deps_core::HttpCache::new());
248 let ecosystem = NpmEcosystem::new(cache);
249
250 let any = ecosystem.as_any();
251 assert!(any.is::<NpmEcosystem>());
252 }
253
254 #[tokio::test]
255 async fn test_complete_package_names_minimum_prefix() {
256 let cache = Arc::new(deps_core::HttpCache::new());
257 let ecosystem = NpmEcosystem::new(cache);
258
259 let results = ecosystem.complete_package_names("e").await;
261 assert!(results.is_empty());
262
263 let results = ecosystem.complete_package_names("").await;
265 assert!(results.is_empty());
266 }
267
268 #[tokio::test]
269 #[ignore] async fn test_complete_package_names_real_search() {
271 let cache = Arc::new(deps_core::HttpCache::new());
272 let ecosystem = NpmEcosystem::new(cache);
273
274 let results = ecosystem.complete_package_names("expre").await;
275 assert!(!results.is_empty());
276 assert!(results.iter().any(|r| r.label == "express"));
277 }
278
279 #[tokio::test]
280 #[ignore] async fn test_complete_versions_real() {
282 let cache = Arc::new(deps_core::HttpCache::new());
283 let ecosystem = NpmEcosystem::new(cache);
284
285 let results = ecosystem.complete_versions("express", "4.").await;
286 assert!(!results.is_empty());
287 assert!(results.iter().all(|r| r.label.starts_with("4.")));
288 }
289
290 #[tokio::test]
291 #[ignore] async fn test_complete_versions_with_operator() {
293 let cache = Arc::new(deps_core::HttpCache::new());
294 let ecosystem = NpmEcosystem::new(cache);
295
296 let results = ecosystem.complete_versions("express", "^4.").await;
297 assert!(!results.is_empty());
298 assert!(results.iter().all(|r| r.label.starts_with("4.")));
299 }
300
301 #[tokio::test]
302 async fn test_complete_versions_unknown_package() {
303 let cache = Arc::new(deps_core::HttpCache::new());
304 let ecosystem = NpmEcosystem::new(cache);
305
306 let results = ecosystem
308 .complete_versions("this-package-does-not-exist-12345", "1.0")
309 .await;
310 assert!(results.is_empty());
311 }
312
313 #[tokio::test]
314 async fn test_complete_package_names_special_characters() {
315 let cache = Arc::new(deps_core::HttpCache::new());
316 let ecosystem = NpmEcosystem::new(cache);
317
318 let results = ecosystem.complete_package_names("@type").await;
320 assert!(results.is_empty() || !results.is_empty());
322 }
323
324 #[tokio::test]
325 async fn test_complete_package_names_max_length() {
326 let cache = Arc::new(deps_core::HttpCache::new());
327 let ecosystem = NpmEcosystem::new(cache);
328
329 let long_prefix = "a".repeat(101);
331 let results = ecosystem.complete_package_names(&long_prefix).await;
332 assert!(results.is_empty());
333
334 let max_prefix = "a".repeat(100);
336 let results = ecosystem.complete_package_names(&max_prefix).await;
337 assert!(results.is_empty() || !results.is_empty());
339 }
340
341 #[tokio::test]
342 #[ignore] async fn test_complete_versions_limit_20() {
344 let cache = Arc::new(deps_core::HttpCache::new());
345 let ecosystem = NpmEcosystem::new(cache);
346
347 let results = ecosystem.complete_versions("express", "4").await;
349 assert!(results.len() <= 20);
350 }
351
352 #[tokio::test]
353 #[ignore] async fn test_complete_package_names_scoped() {
355 let cache = Arc::new(deps_core::HttpCache::new());
356 let ecosystem = NpmEcosystem::new(cache);
357
358 let results = ecosystem.complete_package_names("@types").await;
360 assert!(!results.is_empty() || results.is_empty()); }
362
363 #[tokio::test]
364 async fn test_parse_manifest_valid_json() {
365 let cache = Arc::new(deps_core::HttpCache::new());
366 let ecosystem = NpmEcosystem::new(cache);
367 let uri = Uri::from_file_path("/test/package.json").unwrap();
368
369 let content = r#"{"dependencies": {"express": "^4.18.0"}}"#;
370
371 let result = ecosystem.parse_manifest(content, &uri).await;
372 assert!(result.is_ok());
373
374 let parse_result = result.unwrap();
375 assert!(!parse_result.dependencies().is_empty());
376 }
377
378 #[tokio::test]
379 async fn test_parse_manifest_invalid_json() {
380 let cache = Arc::new(deps_core::HttpCache::new());
381 let ecosystem = NpmEcosystem::new(cache);
382 let uri = Uri::from_file_path("/test/package.json").unwrap();
383
384 let invalid_content = r#"{"dependencies": invalid json"#;
385
386 let result = ecosystem.parse_manifest(invalid_content, &uri).await;
387 assert!(result.is_err());
388 }
389
390 #[tokio::test]
391 async fn test_parse_manifest_empty_dependencies() {
392 let cache = Arc::new(deps_core::HttpCache::new());
393 let ecosystem = NpmEcosystem::new(cache);
394 let uri = Uri::from_file_path("/test/package.json").unwrap();
395
396 let content = r#"{"dependencies": {}}"#;
397
398 let result = ecosystem.parse_manifest(content, &uri).await;
399 assert!(result.is_ok());
400
401 let parse_result = result.unwrap();
402 assert!(parse_result.dependencies().is_empty());
403 }
404
405 #[tokio::test]
406 async fn test_registry_returns_arc() {
407 let cache = Arc::new(deps_core::HttpCache::new());
408 let ecosystem = NpmEcosystem::new(cache);
409
410 let registry = ecosystem.registry();
411 assert!(Arc::strong_count(®istry) >= 1);
412 }
413
414 #[tokio::test]
415 async fn test_lockfile_provider_returns_some() {
416 let cache = Arc::new(deps_core::HttpCache::new());
417 let ecosystem = NpmEcosystem::new(cache);
418
419 let provider = ecosystem.lockfile_provider();
420 assert!(provider.is_some());
421 }
422
423 #[tokio::test]
424 async fn test_generate_inlay_hints_empty_dependencies() {
425 let cache = Arc::new(deps_core::HttpCache::new());
426 let ecosystem = NpmEcosystem::new(cache);
427 let uri = Uri::from_file_path("/test/package.json").unwrap();
428
429 let content = r#"{"dependencies": {}}"#;
430
431 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
432 let cached_versions = HashMap::new();
433 let resolved_versions = HashMap::new();
434 let config = EcosystemConfig::default();
435
436 let hints = ecosystem
437 .generate_inlay_hints(
438 parse_result.as_ref(),
439 &cached_versions,
440 &resolved_versions,
441 deps_core::LoadingState::Loaded,
442 &config,
443 )
444 .await;
445
446 assert!(hints.is_empty());
447 }
448
449 #[tokio::test]
450 async fn test_generate_completions_no_context() {
451 let cache = Arc::new(deps_core::HttpCache::new());
452 let ecosystem = NpmEcosystem::new(cache);
453 let uri = Uri::from_file_path("/test/package.json").unwrap();
454
455 let content = r#"{"name": "test"}"#;
456
457 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
458 let position = Position {
459 line: 0,
460 character: 0,
461 };
462
463 let completions = ecosystem
464 .generate_completions(parse_result.as_ref(), position, content)
465 .await;
466
467 assert!(completions.is_empty());
468 }
469
470 #[tokio::test]
471 async fn test_generate_completions_feature_context_returns_empty() {
472 let cache = Arc::new(deps_core::HttpCache::new());
473 let ecosystem = NpmEcosystem::new(cache);
474
475 let content = r#"{"dependencies": {"express": "4.0.0"}}"#;
477 let uri = Uri::from_file_path("/test/package.json").unwrap();
478 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
479
480 let position = Position {
481 line: 0,
482 character: 30,
483 };
484
485 let completions = ecosystem
486 .generate_completions(parse_result.as_ref(), position, content)
487 .await;
488
489 assert!(completions.is_empty() || !completions.is_empty());
491 }
492
493 #[tokio::test]
494 async fn test_generate_hover_no_dependency_at_position() {
495 let cache = Arc::new(deps_core::HttpCache::new());
496 let ecosystem = NpmEcosystem::new(cache);
497 let uri = Uri::from_file_path("/test/package.json").unwrap();
498
499 let content = r#"{"name": "test"}"#;
500
501 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
502 let position = Position {
503 line: 0,
504 character: 0,
505 };
506 let cached_versions = HashMap::new();
507 let resolved_versions = HashMap::new();
508
509 let hover = ecosystem
510 .generate_hover(
511 parse_result.as_ref(),
512 position,
513 &cached_versions,
514 &resolved_versions,
515 )
516 .await;
517
518 assert!(hover.is_none());
519 }
520
521 #[tokio::test]
522 async fn test_generate_code_actions_no_actions() {
523 let cache = Arc::new(deps_core::HttpCache::new());
524 let ecosystem = NpmEcosystem::new(cache);
525 let uri = Uri::from_file_path("/test/package.json").unwrap();
526
527 let content = r#"{"name": "test"}"#;
528
529 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
530 let position = Position {
531 line: 0,
532 character: 0,
533 };
534 let cached_versions = HashMap::new();
535
536 let actions = ecosystem
537 .generate_code_actions(parse_result.as_ref(), position, &cached_versions, &uri)
538 .await;
539
540 assert!(actions.is_empty());
541 }
542
543 #[tokio::test]
544 async fn test_generate_diagnostics_no_dependencies() {
545 let cache = Arc::new(deps_core::HttpCache::new());
546 let ecosystem = NpmEcosystem::new(cache);
547 let uri = Uri::from_file_path("/test/package.json").unwrap();
548
549 let content = r#"{"dependencies": {}}"#;
550
551 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
552 let cached_versions = HashMap::new();
553 let resolved_versions = HashMap::new();
554
555 let diagnostics = ecosystem
556 .generate_diagnostics(
557 parse_result.as_ref(),
558 &cached_versions,
559 &resolved_versions,
560 &uri,
561 )
562 .await;
563
564 assert!(diagnostics.is_empty());
565 }
566
567 #[tokio::test]
568 async fn test_complete_versions_empty_prefix() {
569 let cache = Arc::new(deps_core::HttpCache::new());
570 let ecosystem = NpmEcosystem::new(cache);
571
572 let results = ecosystem.complete_versions("nonexistent-package", "").await;
574 assert!(results.is_empty());
576 }
577
578 #[tokio::test]
579 async fn test_complete_versions_with_tilde_operator() {
580 let cache = Arc::new(deps_core::HttpCache::new());
581 let ecosystem = NpmEcosystem::new(cache);
582
583 let results = ecosystem.complete_versions("nonexistent-pkg", "~4.0").await;
585 assert!(results.is_empty());
586 }
587
588 #[tokio::test]
589 async fn test_complete_versions_with_wildcard() {
590 let cache = Arc::new(deps_core::HttpCache::new());
591 let ecosystem = NpmEcosystem::new(cache);
592
593 let results = ecosystem.complete_versions("nonexistent-pkg", "*").await;
595 assert!(results.is_empty());
596 }
597
598 #[tokio::test]
599 async fn test_complete_versions_with_less_than_operator() {
600 let cache = Arc::new(deps_core::HttpCache::new());
601 let ecosystem = NpmEcosystem::new(cache);
602
603 let results = ecosystem.complete_versions("nonexistent-pkg", "<2.0").await;
605 assert!(results.is_empty());
606 }
607}