Skip to main content

deps_bundler/
ecosystem.rs

1//! Bundler ecosystem implementation for deps-lsp.
2
3use std::any::Any;
4use std::sync::Arc;
5use tower_lsp_server::ls_types::{CompletionItem, Position, Uri};
6
7use deps_core::{
8    Ecosystem, ParseResult as ParseResultTrait, Registry, Result, lsp_helpers::EcosystemFormatter,
9};
10
11use crate::formatter::BundlerFormatter;
12use crate::registry::RubyGemsRegistry;
13
14/// Bundler ecosystem implementation.
15///
16/// Provides LSP functionality for Gemfile files, including:
17/// - Dependency parsing with position tracking
18/// - Version information from rubygems.org
19/// - Inlay hints for latest versions
20/// - Hover tooltips with gem metadata
21/// - Code actions for version updates
22/// - Diagnostics for unknown/yanked gems
23pub struct BundlerEcosystem {
24    registry: Arc<RubyGemsRegistry>,
25    formatter: BundlerFormatter,
26}
27
28impl BundlerEcosystem {
29    /// Creates a new Bundler ecosystem with the given HTTP cache.
30    pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
31        Self {
32            registry: Arc::new(RubyGemsRegistry::new(cache)),
33            formatter: BundlerFormatter,
34        }
35    }
36
37    async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> {
38        deps_core::completion::complete_package_names_generic(self.registry.as_ref(), prefix, 20)
39            .await
40    }
41
42    async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
43        deps_core::completion::complete_versions_generic(
44            self.registry.as_ref(),
45            package_name,
46            prefix,
47            &['~', '>', '<', '=', '!'],
48        )
49        .await
50    }
51}
52
53impl deps_core::ecosystem::private::Sealed for BundlerEcosystem {}
54
55impl Ecosystem for BundlerEcosystem {
56    fn id(&self) -> &'static str {
57        "bundler"
58    }
59
60    fn display_name(&self) -> &'static str {
61        "Bundler (Ruby)"
62    }
63
64    fn manifest_filenames(&self) -> &[&'static str] {
65        &["Gemfile"]
66    }
67
68    fn lockfile_filenames(&self) -> &[&'static str] {
69        &["Gemfile.lock"]
70    }
71
72    fn parse_manifest<'a>(
73        &'a self,
74        content: &'a str,
75        uri: &'a Uri,
76    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
77        Box::pin(async move {
78            let result = crate::parser::parse_gemfile(content, uri)?;
79            Ok(Box::new(result) as Box<dyn ParseResultTrait>)
80        })
81    }
82
83    fn registry(&self) -> Arc<dyn Registry> {
84        self.registry.clone() as Arc<dyn Registry>
85    }
86
87    fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
88        Some(Arc::new(crate::lockfile::GemfileLockParser))
89    }
90
91    fn formatter(&self) -> &dyn EcosystemFormatter {
92        &self.formatter
93    }
94
95    fn generate_completions<'a>(
96        &'a self,
97        parse_result: &'a dyn ParseResultTrait,
98        position: Position,
99        content: &'a str,
100    ) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
101        Box::pin(async move {
102            use deps_core::completion::{CompletionContext, detect_completion_context};
103
104            let context = detect_completion_context(parse_result, position, content);
105
106            match context {
107                CompletionContext::PackageName { prefix } => {
108                    self.complete_package_names(&prefix).await
109                }
110                CompletionContext::Version {
111                    package_name,
112                    prefix,
113                } => self.complete_versions(&package_name, &prefix).await,
114                CompletionContext::Feature { .. } | CompletionContext::None => vec![],
115            }
116        })
117    }
118
119    fn as_any(&self) -> &dyn Any {
120        self
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_ecosystem_id() {
130        let cache = Arc::new(deps_core::HttpCache::new());
131        let ecosystem = BundlerEcosystem::new(cache);
132        assert_eq!(ecosystem.id(), "bundler");
133    }
134
135    #[test]
136    fn test_ecosystem_display_name() {
137        let cache = Arc::new(deps_core::HttpCache::new());
138        let ecosystem = BundlerEcosystem::new(cache);
139        assert_eq!(ecosystem.display_name(), "Bundler (Ruby)");
140    }
141
142    #[test]
143    fn test_ecosystem_manifest_filenames() {
144        let cache = Arc::new(deps_core::HttpCache::new());
145        let ecosystem = BundlerEcosystem::new(cache);
146        assert_eq!(ecosystem.manifest_filenames(), &["Gemfile"]);
147    }
148
149    #[test]
150    fn test_ecosystem_lockfile_filenames() {
151        let cache = Arc::new(deps_core::HttpCache::new());
152        let ecosystem = BundlerEcosystem::new(cache);
153        assert_eq!(ecosystem.lockfile_filenames(), &["Gemfile.lock"]);
154    }
155
156    #[test]
157    fn test_as_any() {
158        let cache = Arc::new(deps_core::HttpCache::new());
159        let ecosystem = BundlerEcosystem::new(cache);
160        let any = ecosystem.as_any();
161        assert!(any.is::<BundlerEcosystem>());
162    }
163
164    #[tokio::test]
165    async fn test_complete_package_names_minimum_prefix() {
166        let cache = Arc::new(deps_core::HttpCache::new());
167        let ecosystem = BundlerEcosystem::new(cache);
168
169        // Less than 2 characters should return empty
170        let results = ecosystem.complete_package_names("r").await;
171        assert!(results.is_empty());
172
173        // Empty prefix should return empty
174        let results = ecosystem.complete_package_names("").await;
175        assert!(results.is_empty());
176    }
177
178    #[tokio::test]
179    async fn test_complete_package_names_max_length() {
180        let cache = Arc::new(deps_core::HttpCache::new());
181        let ecosystem = BundlerEcosystem::new(cache);
182
183        // Prefix longer than 200 chars should return empty
184        let long_prefix = "a".repeat(201);
185        let results = ecosystem.complete_package_names(&long_prefix).await;
186        assert!(results.is_empty());
187    }
188
189    #[tokio::test]
190    async fn test_lockfile_provider() {
191        let cache = Arc::new(deps_core::HttpCache::new());
192        let ecosystem = BundlerEcosystem::new(cache);
193        assert!(ecosystem.lockfile_provider().is_some());
194    }
195
196    #[tokio::test]
197    async fn test_parse_manifest() {
198        let cache = Arc::new(deps_core::HttpCache::new());
199        let ecosystem = BundlerEcosystem::new(cache);
200
201        let gemfile = r"source 'https://rubygems.org'
202gem 'rails', '~> 7.0'";
203
204        #[cfg(windows)]
205        let path = "C:/test/Gemfile";
206        #[cfg(not(windows))]
207        let path = "/test/Gemfile";
208        let uri = Uri::from_file_path(path).unwrap();
209
210        let result = ecosystem.parse_manifest(gemfile, &uri).await.unwrap();
211        assert_eq!(result.dependencies().len(), 1);
212    }
213}