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::GoFormatter;
19use crate::registry::GoRegistry;
20
21pub struct GoEcosystem {
31 registry: Arc<GoRegistry>,
32 formatter: GoFormatter,
33}
34
35impl GoEcosystem {
36 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
38 Self {
39 registry: Arc::new(GoRegistry::new(cache)),
40 formatter: GoFormatter,
41 }
42 }
43
44 async fn complete_package_names(&self, _prefix: &str) -> Vec<CompletionItem> {
55 vec![]
58 }
59
60 async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
61 deps_core::completion::complete_versions_generic(
62 self.registry.as_ref(),
63 package_name,
64 prefix,
65 &[],
66 )
67 .await
68 }
69
70 async fn complete_features(&self, _package_name: &str, _prefix: &str) -> Vec<CompletionItem> {
75 vec![]
77 }
78}
79
80#[async_trait]
81impl Ecosystem for GoEcosystem {
82 fn id(&self) -> &'static str {
83 "go"
84 }
85
86 fn display_name(&self) -> &'static str {
87 "Go Modules"
88 }
89
90 fn manifest_filenames(&self) -> &[&'static str] {
91 &["go.mod"]
92 }
93
94 fn lockfile_filenames(&self) -> &[&'static str] {
95 &["go.sum"]
96 }
97
98 async fn parse_manifest(&self, content: &str, uri: &Uri) -> Result<Box<dyn ParseResultTrait>> {
99 let result = crate::parser::parse_go_mod(content, uri)?;
100 Ok(Box::new(result))
101 }
102
103 fn registry(&self) -> Arc<dyn Registry> {
104 self.registry.clone() as Arc<dyn Registry>
105 }
106
107 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
108 Some(Arc::new(crate::lockfile::GoSumParser))
109 }
110
111 async fn generate_inlay_hints(
112 &self,
113 parse_result: &dyn ParseResultTrait,
114 cached_versions: &HashMap<String, String>,
115 resolved_versions: &HashMap<String, String>,
116 loading_state: deps_core::LoadingState,
117 config: &EcosystemConfig,
118 ) -> Vec<InlayHint> {
119 lsp_helpers::generate_inlay_hints(
120 parse_result,
121 cached_versions,
122 resolved_versions,
123 loading_state,
124 config,
125 &self.formatter,
126 )
127 }
128
129 async fn generate_hover(
130 &self,
131 parse_result: &dyn ParseResultTrait,
132 position: Position,
133 cached_versions: &HashMap<String, String>,
134 resolved_versions: &HashMap<String, String>,
135 ) -> Option<Hover> {
136 lsp_helpers::generate_hover(
137 parse_result,
138 position,
139 cached_versions,
140 resolved_versions,
141 self.registry.as_ref(),
142 &self.formatter,
143 )
144 .await
145 }
146
147 async fn generate_code_actions(
148 &self,
149 parse_result: &dyn ParseResultTrait,
150 position: Position,
151 _cached_versions: &HashMap<String, String>,
152 uri: &Uri,
153 ) -> Vec<CodeAction> {
154 lsp_helpers::generate_code_actions(
155 parse_result,
156 position,
157 uri,
158 self.registry.as_ref(),
159 &self.formatter,
160 )
161 .await
162 }
163
164 async fn generate_diagnostics(
165 &self,
166 parse_result: &dyn ParseResultTrait,
167 cached_versions: &HashMap<String, String>,
168 resolved_versions: &HashMap<String, String>,
169 _uri: &Uri,
170 ) -> Vec<Diagnostic> {
171 lsp_helpers::generate_diagnostics_from_cache(
172 parse_result,
173 cached_versions,
174 resolved_versions,
175 &self.formatter,
176 )
177 }
178
179 async fn generate_completions(
180 &self,
181 parse_result: &dyn ParseResultTrait,
182 position: Position,
183 content: &str,
184 ) -> Vec<CompletionItem> {
185 use deps_core::completion::{CompletionContext, detect_completion_context};
186
187 let context = detect_completion_context(parse_result, position, content);
188
189 match context {
190 CompletionContext::PackageName { prefix } => self.complete_package_names(&prefix).await,
191 CompletionContext::Version {
192 package_name,
193 prefix,
194 } => self.complete_versions(&package_name, &prefix).await,
195 CompletionContext::Feature {
196 package_name,
197 prefix,
198 } => self.complete_features(&package_name, &prefix).await,
199 CompletionContext::None => vec![],
200 }
201 }
202
203 fn as_any(&self) -> &dyn Any {
204 self
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::types::{GoDependency, GoDirective};
212 use deps_core::Dependency;
213 use std::collections::HashMap;
214 use tower_lsp_server::ls_types::{InlayHintLabel, Position, Range};
215
216 fn mock_dependency(name: &str, version: Option<&str>, line: u32) -> GoDependency {
218 GoDependency {
219 module_path: name.to_string(),
220 module_path_range: Range::new(
221 Position::new(line, 0),
222 Position::new(line, name.len() as u32),
223 ),
224 version: version.map(String::from),
225 version_range: version
226 .map(|_| Range::new(Position::new(line, 0), Position::new(line, 10))),
227 directive: GoDirective::Require,
228 indirect: false,
229 }
230 }
231
232 struct MockParseResult {
234 dependencies: Vec<GoDependency>,
235 uri: Uri,
236 }
237
238 impl deps_core::ParseResult for MockParseResult {
239 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
240 self.dependencies
241 .iter()
242 .map(|d| d as &dyn deps_core::Dependency)
243 .collect()
244 }
245
246 fn workspace_root(&self) -> Option<&std::path::Path> {
247 None
248 }
249
250 fn uri(&self) -> &Uri {
251 &self.uri
252 }
253
254 fn as_any(&self) -> &dyn Any {
255 self
256 }
257 }
258
259 #[test]
260 fn test_ecosystem_id() {
261 let cache = Arc::new(deps_core::HttpCache::new());
262 let ecosystem = GoEcosystem::new(cache);
263 assert_eq!(ecosystem.id(), "go");
264 }
265
266 #[test]
267 fn test_ecosystem_display_name() {
268 let cache = Arc::new(deps_core::HttpCache::new());
269 let ecosystem = GoEcosystem::new(cache);
270 assert_eq!(ecosystem.display_name(), "Go Modules");
271 }
272
273 #[test]
274 fn test_ecosystem_manifest_filenames() {
275 let cache = Arc::new(deps_core::HttpCache::new());
276 let ecosystem = GoEcosystem::new(cache);
277 assert_eq!(ecosystem.manifest_filenames(), &["go.mod"]);
278 }
279
280 #[test]
281 fn test_ecosystem_lockfile_filenames() {
282 let cache = Arc::new(deps_core::HttpCache::new());
283 let ecosystem = GoEcosystem::new(cache);
284 assert_eq!(ecosystem.lockfile_filenames(), &["go.sum"]);
285 }
286
287 #[test]
288 fn test_generate_inlay_hints_up_to_date() {
289 let cache = Arc::new(deps_core::HttpCache::new());
290 let ecosystem = GoEcosystem::new(cache);
291
292 let uri = Uri::from_file_path("/test/go.mod").unwrap();
293 let parse_result = MockParseResult {
294 dependencies: vec![mock_dependency(
295 "github.com/gin-gonic/gin",
296 Some("v1.9.1"),
297 5,
298 )],
299 uri,
300 };
301
302 let mut cached_versions = HashMap::new();
303 cached_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
304
305 let config = EcosystemConfig {
306 loading_text: "⏳".to_string(),
307 show_loading_hints: true,
308 show_up_to_date_hints: true,
309 up_to_date_text: "✅".to_string(),
310 needs_update_text: "❌ {}".to_string(),
311 };
312
313 let mut resolved_versions = HashMap::new();
315 resolved_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
316 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
317 &parse_result,
318 &cached_versions,
319 &resolved_versions,
320 deps_core::LoadingState::Loaded,
321 &config,
322 ));
323
324 assert_eq!(hints.len(), 1);
325 match &hints[0].label {
326 InlayHintLabel::String(s) => assert_eq!(s, "✅ v1.9.1"),
327 _ => panic!("Expected String label"),
328 }
329 }
330
331 #[test]
332 fn test_generate_inlay_hints_needs_update() {
333 let cache = Arc::new(deps_core::HttpCache::new());
334 let ecosystem = GoEcosystem::new(cache);
335
336 let uri = Uri::from_file_path("/test/go.mod").unwrap();
337 let parse_result = MockParseResult {
338 dependencies: vec![mock_dependency(
339 "github.com/gin-gonic/gin",
340 Some("v1.9.0"),
341 5,
342 )],
343 uri,
344 };
345
346 let mut cached_versions = HashMap::new();
347 cached_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
348
349 let config = EcosystemConfig {
350 loading_text: "⏳".to_string(),
351 show_loading_hints: true,
352 show_up_to_date_hints: true,
353 up_to_date_text: "✅".to_string(),
354 needs_update_text: "❌ {}".to_string(),
355 };
356
357 let resolved_versions = HashMap::new();
358 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
359 &parse_result,
360 &cached_versions,
361 &resolved_versions,
362 deps_core::LoadingState::Loaded,
363 &config,
364 ));
365
366 assert_eq!(hints.len(), 1);
367 match &hints[0].label {
368 InlayHintLabel::String(s) => assert_eq!(s, "❌ v1.9.1"),
369 _ => panic!("Expected String label"),
370 }
371 }
372
373 #[test]
374 fn test_generate_inlay_hints_hide_up_to_date() {
375 let cache = Arc::new(deps_core::HttpCache::new());
376 let ecosystem = GoEcosystem::new(cache);
377
378 let uri = Uri::from_file_path("/test/go.mod").unwrap();
379 let parse_result = MockParseResult {
380 dependencies: vec![mock_dependency(
381 "github.com/gin-gonic/gin",
382 Some("v1.9.1"),
383 5,
384 )],
385 uri,
386 };
387
388 let mut cached_versions = HashMap::new();
389 cached_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
390
391 let config = EcosystemConfig {
392 loading_text: "⏳".to_string(),
393 show_loading_hints: true,
394 show_up_to_date_hints: false,
395 up_to_date_text: "✅".to_string(),
396 needs_update_text: "❌ {}".to_string(),
397 };
398
399 let mut resolved_versions = HashMap::new();
401 resolved_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
402 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
403 &parse_result,
404 &cached_versions,
405 &resolved_versions,
406 deps_core::LoadingState::Loaded,
407 &config,
408 ));
409
410 assert_eq!(hints.len(), 0);
411 }
412
413 #[test]
414 fn test_generate_inlay_hints_no_version_range() {
415 let cache = Arc::new(deps_core::HttpCache::new());
416 let ecosystem = GoEcosystem::new(cache);
417
418 let mut dep = mock_dependency("github.com/gin-gonic/gin", Some("v1.9.1"), 5);
419 dep.version_range = None;
420
421 let uri = Uri::from_file_path("/test/go.mod").unwrap();
422 let parse_result = MockParseResult {
423 dependencies: vec![dep],
424 uri,
425 };
426
427 let mut cached_versions = HashMap::new();
428 cached_versions.insert("github.com/gin-gonic/gin".to_string(), "v1.9.1".to_string());
429
430 let config = EcosystemConfig {
431 loading_text: "⏳".to_string(),
432 show_loading_hints: true,
433 show_up_to_date_hints: true,
434 up_to_date_text: "✅".to_string(),
435 needs_update_text: "❌ {}".to_string(),
436 };
437
438 let resolved_versions = HashMap::new();
439 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
440 &parse_result,
441 &cached_versions,
442 &resolved_versions,
443 deps_core::LoadingState::Loaded,
444 &config,
445 ));
446
447 assert_eq!(hints.len(), 0);
448 }
449
450 #[test]
451 fn test_as_any() {
452 let cache = Arc::new(deps_core::HttpCache::new());
453 let ecosystem = GoEcosystem::new(cache);
454
455 let any = ecosystem.as_any();
457 assert!(any.is::<GoEcosystem>());
458 }
459
460 #[tokio::test]
461 async fn test_complete_package_names_empty() {
462 let cache = Arc::new(deps_core::HttpCache::new());
463 let ecosystem = GoEcosystem::new(cache);
464
465 let results = ecosystem.complete_package_names("github").await;
467 assert!(results.is_empty());
468 }
469
470 #[tokio::test]
471 #[ignore] async fn test_complete_versions_real() {
473 let cache = Arc::new(deps_core::HttpCache::new());
474 let ecosystem = GoEcosystem::new(cache);
475
476 let results = ecosystem
477 .complete_versions("github.com/gin-gonic/gin", "v1.9")
478 .await;
479 assert!(!results.is_empty());
480 assert!(results.iter().all(|r| r.label.starts_with("v1.9")));
481 }
482
483 #[tokio::test]
484 async fn test_complete_versions_unknown_package() {
485 let cache = Arc::new(deps_core::HttpCache::new());
486 let ecosystem = GoEcosystem::new(cache);
487
488 let results = ecosystem
490 .complete_versions("github.com/nonexistent/package12345", "v1.0")
491 .await;
492 assert!(results.is_empty());
493 }
494
495 #[tokio::test]
496 async fn test_complete_features_always_empty() {
497 let cache = Arc::new(deps_core::HttpCache::new());
498 let ecosystem = GoEcosystem::new(cache);
499
500 let results = ecosystem
502 .complete_features("github.com/gin-gonic/gin", "")
503 .await;
504 assert!(results.is_empty());
505 }
506
507 #[tokio::test]
508 #[ignore] async fn test_complete_versions_limit_20() {
510 let cache = Arc::new(deps_core::HttpCache::new());
511 let ecosystem = GoEcosystem::new(cache);
512
513 let results = ecosystem
515 .complete_versions("github.com/gin-gonic/gin", "v")
516 .await;
517 assert!(results.len() <= 20);
518 }
519
520 #[tokio::test]
521 async fn test_generate_hover_on_module_path() {
522 let cache = Arc::new(deps_core::HttpCache::new());
523 let ecosystem = GoEcosystem::new(cache);
524
525 let uri = Uri::from_file_path("/test/go.mod").unwrap();
526 let parse_result = MockParseResult {
527 dependencies: vec![mock_dependency(
528 "github.com/gin-gonic/gin",
529 Some("v1.9.1"),
530 5,
531 )],
532 uri,
533 };
534
535 let position = Position::new(5, 5);
536 let cached_versions = HashMap::new();
537 let resolved_versions = HashMap::new();
538
539 let hover = ecosystem
540 .generate_hover(
541 &parse_result,
542 position,
543 &cached_versions,
544 &resolved_versions,
545 )
546 .await;
547
548 assert!(hover.is_some());
550 let hover_content = hover.unwrap();
551 let markdown = format!("{:?}", hover_content.contents);
552 assert!(markdown.contains("pkg.go.dev"));
553 }
554
555 #[tokio::test]
556 async fn test_generate_hover_outside_dependency() {
557 let cache = Arc::new(deps_core::HttpCache::new());
558 let ecosystem = GoEcosystem::new(cache);
559
560 let uri = Uri::from_file_path("/test/go.mod").unwrap();
561 let parse_result = MockParseResult {
562 dependencies: vec![mock_dependency(
563 "github.com/gin-gonic/gin",
564 Some("v1.9.1"),
565 5,
566 )],
567 uri,
568 };
569
570 let position = Position::new(0, 0);
571 let cached_versions = HashMap::new();
572 let resolved_versions = HashMap::new();
573
574 let hover = ecosystem
575 .generate_hover(
576 &parse_result,
577 position,
578 &cached_versions,
579 &resolved_versions,
580 )
581 .await;
582
583 assert!(hover.is_none());
584 }
585
586 #[tokio::test]
587 async fn test_generate_code_actions_on_module() {
588 let cache = Arc::new(deps_core::HttpCache::new());
589 let ecosystem = GoEcosystem::new(cache);
590
591 let uri = Uri::from_file_path("/test/go.mod").unwrap();
592 let parse_result = MockParseResult {
593 dependencies: vec![mock_dependency(
594 "github.com/gin-gonic/gin",
595 Some("v1.9.0"),
596 5,
597 )],
598 uri: uri.clone(),
599 };
600
601 let position = Position::new(5, 5);
602 let cached_versions = HashMap::new();
603
604 let actions = ecosystem
605 .generate_code_actions(&parse_result, position, &cached_versions, &uri)
606 .await;
607
608 assert!(!actions.is_empty());
610 }
611
612 #[tokio::test]
613 #[ignore = "Requires network access to proxy.golang.org"]
614 async fn test_generate_diagnostics_basic() {
615 let cache = Arc::new(deps_core::HttpCache::new());
616 let ecosystem = GoEcosystem::new(cache);
617
618 let uri = Uri::from_file_path("/test/go.mod").unwrap();
619 let parse_result = MockParseResult {
620 dependencies: vec![mock_dependency(
621 "github.com/gin-gonic/gin",
622 Some("v1.9.1"),
623 5,
624 )],
625 uri,
626 };
627
628 let cached_versions = HashMap::new();
629 let resolved_versions = HashMap::new();
630
631 let result = tokio::time::timeout(
633 std::time::Duration::from_secs(5),
634 ecosystem.generate_diagnostics(
635 &parse_result,
636 &cached_versions,
637 &resolved_versions,
638 parse_result.uri(),
639 ),
640 )
641 .await;
642
643 assert!(result.is_ok(), "Diagnostic generation timed out");
645 }
646
647 #[tokio::test]
648 async fn test_generate_completions_package_name() {
649 let cache = Arc::new(deps_core::HttpCache::new());
650 let ecosystem = GoEcosystem::new(cache);
651
652 let content = r"module example.com/myapp
653
654go 1.21
655
656require github.com/
657";
658
659 let uri = Uri::from_file_path("/test/go.mod").unwrap();
660 let parse_result = MockParseResult {
661 dependencies: vec![],
662 uri,
663 };
664
665 let position = Position::new(4, 19);
666
667 let completions = ecosystem
668 .generate_completions(&parse_result, position, content)
669 .await;
670
671 assert!(completions.is_empty());
673 }
674
675 #[tokio::test]
676 async fn test_generate_completions_outside_context() {
677 let cache = Arc::new(deps_core::HttpCache::new());
678 let ecosystem = GoEcosystem::new(cache);
679
680 let content = r"module example.com/myapp
681
682go 1.21
683";
684
685 let uri = Uri::from_file_path("/test/go.mod").unwrap();
686 let parse_result = MockParseResult {
687 dependencies: vec![],
688 uri,
689 };
690
691 let position = Position::new(0, 0);
692
693 let completions = ecosystem
694 .generate_completions(&parse_result, position, content)
695 .await;
696
697 assert!(completions.is_empty());
698 }
699
700 #[tokio::test]
701 async fn test_parse_manifest_valid() {
702 let cache = Arc::new(deps_core::HttpCache::new());
703 let ecosystem = GoEcosystem::new(cache);
704
705 let content = r"module example.com/myapp
706
707go 1.21
708
709require github.com/gin-gonic/gin v1.9.1
710";
711
712 let uri = Uri::from_file_path("/test/go.mod").unwrap();
713
714 let result = ecosystem.parse_manifest(content, &uri).await;
715 assert!(result.is_ok());
716
717 let parse_result = result.unwrap();
718 assert_eq!(parse_result.dependencies().len(), 1);
719 assert_eq!(
720 parse_result.dependencies()[0].name(),
721 "github.com/gin-gonic/gin"
722 );
723 }
724
725 #[tokio::test]
726 async fn test_parse_manifest_empty() {
727 let cache = Arc::new(deps_core::HttpCache::new());
728 let ecosystem = GoEcosystem::new(cache);
729
730 let content = "";
731 let uri = Uri::from_file_path("/test/go.mod").unwrap();
732
733 let result = ecosystem.parse_manifest(content, &uri).await;
734 assert!(result.is_ok());
735
736 let parse_result = result.unwrap();
737 assert_eq!(parse_result.dependencies().len(), 0);
738 }
739
740 #[test]
741 fn test_registry_returns_trait_object() {
742 let cache = Arc::new(deps_core::HttpCache::new());
743 let ecosystem = GoEcosystem::new(cache);
744
745 let registry = ecosystem.registry();
746 assert_eq!(
747 registry.package_url("github.com/gin-gonic/gin"),
748 "https://pkg.go.dev/github.com/gin-gonic/gin"
749 );
750 }
751
752 #[test]
753 fn test_lockfile_provider_exists() {
754 let cache = Arc::new(deps_core::HttpCache::new());
755 let ecosystem = GoEcosystem::new(cache);
756
757 assert!(ecosystem.lockfile_provider().is_some());
758 }
759
760 #[test]
761 fn test_mock_dependency_indirect() {
762 let mut dep = mock_dependency("github.com/example/pkg", Some("v1.0.0"), 10);
763 dep.indirect = true;
764
765 assert!(dep.indirect);
766 assert_eq!(dep.name(), "github.com/example/pkg");
767 }
768}