deps_bundler/
ecosystem.rs1use std::any::Any;
4use std::sync::Arc;
5use tower_lsp_server::ls_types::{CompletionItem, Position, Range, Uri};
6
7use deps_core::{
8 Ecosystem, ParseResult as ParseResultTrait, Registry, Result, completion::Completions,
9 lsp_helpers::EcosystemFormatter,
10};
11
12use crate::formatter::BundlerFormatter;
13use crate::registry::RubyGemsRegistry;
14
15pub struct BundlerEcosystem {
25 registry: Arc<RubyGemsRegistry>,
26 formatter: BundlerFormatter,
27}
28
29impl BundlerEcosystem {
30 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
32 Self {
33 registry: Arc::new(RubyGemsRegistry::new(cache)),
34 formatter: BundlerFormatter,
35 }
36 }
37
38 async fn complete_package_names(&self, prefix: &str, range: Range) -> Vec<CompletionItem> {
39 deps_core::completion::complete_package_names_generic(
40 self.registry.as_ref(),
41 prefix,
42 20,
43 range,
44 )
45 .await
46 }
47
48 async fn complete_versions(
49 &self,
50 package_name: &deps_core::PackageName,
51 prefix: &str,
52 freshness: deps_core::FreshnessSettings,
53 ) -> Vec<CompletionItem> {
54 deps_core::completion::complete_versions_generic(
55 self.registry.as_ref(),
56 package_name,
57 prefix,
58 &['~', '>', '<', '=', '!'],
59 freshness,
60 )
61 .await
62 }
63}
64
65impl deps_core::ecosystem::private::Sealed for BundlerEcosystem {}
66
67impl Ecosystem for BundlerEcosystem {
68 fn id(&self) -> &'static str {
69 "bundler"
70 }
71
72 fn display_name(&self) -> &'static str {
73 "Bundler (Ruby)"
74 }
75
76 fn manifest_filenames(&self) -> &[&'static str] {
77 &["Gemfile"]
78 }
79
80 fn lockfile_filenames(&self) -> &[&'static str] {
81 &["Gemfile.lock"]
82 }
83
84 fn parse_manifest<'a>(
85 &'a self,
86 content: &'a str,
87 uri: &'a Uri,
88 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
89 Box::pin(async move {
90 let result = crate::parser::parse_gemfile(content, uri)?;
91 Ok(Box::new(result) as Box<dyn ParseResultTrait>)
92 })
93 }
94
95 fn registry(&self) -> Arc<dyn Registry> {
96 self.registry.clone() as Arc<dyn Registry>
97 }
98
99 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
100 Some(Arc::new(crate::lockfile::GemfileLockParser))
101 }
102
103 fn formatter(&self) -> &dyn EcosystemFormatter {
104 &self.formatter
105 }
106
107 fn generate_completions<'a>(
108 &'a self,
109 parse_result: &'a dyn ParseResultTrait,
110 position: Position,
111 content: &'a str,
112 freshness: deps_core::FreshnessSettings,
113 ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
114 Box::pin(async move {
115 use deps_core::completion::{CompletionContext, detect_completion_context};
116
117 let context = detect_completion_context(parse_result, position, content);
118
119 match context {
120 CompletionContext::PackageName { prefix, range } => {
121 self.complete_package_names(&prefix, range).await
122 }
123 CompletionContext::Version {
124 package_name,
125 prefix,
126 } => {
127 self.complete_versions(&package_name, &prefix, freshness)
128 .await
129 }
130 CompletionContext::Feature { .. } | CompletionContext::None => vec![],
131 }
132 .into()
133 })
134 }
135
136 fn as_any(&self) -> &dyn Any {
137 self
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_ecosystem_id() {
147 let cache = Arc::new(deps_core::HttpCache::new());
148 let ecosystem = BundlerEcosystem::new(cache);
149 assert_eq!(ecosystem.id(), "bundler");
150 }
151
152 #[test]
153 fn test_ecosystem_display_name() {
154 let cache = Arc::new(deps_core::HttpCache::new());
155 let ecosystem = BundlerEcosystem::new(cache);
156 assert_eq!(ecosystem.display_name(), "Bundler (Ruby)");
157 }
158
159 #[test]
160 fn test_ecosystem_manifest_filenames() {
161 let cache = Arc::new(deps_core::HttpCache::new());
162 let ecosystem = BundlerEcosystem::new(cache);
163 assert_eq!(ecosystem.manifest_filenames(), &["Gemfile"]);
164 }
165
166 #[test]
167 fn test_ecosystem_lockfile_filenames() {
168 let cache = Arc::new(deps_core::HttpCache::new());
169 let ecosystem = BundlerEcosystem::new(cache);
170 assert_eq!(ecosystem.lockfile_filenames(), &["Gemfile.lock"]);
171 }
172
173 #[test]
174 fn test_as_any() {
175 let cache = Arc::new(deps_core::HttpCache::new());
176 let ecosystem = BundlerEcosystem::new(cache);
177 let any = ecosystem.as_any();
178 assert!(any.is::<BundlerEcosystem>());
179 }
180
181 #[tokio::test]
182 async fn test_package_name_completion_context_has_real_range() {
183 let cache = Arc::new(deps_core::HttpCache::new());
186 let ecosystem = BundlerEcosystem::new(cache);
187 let content = "source 'https://rubygems.org'\ngem 'rails', '~> 7.0'";
188 let uri = deps_core::test_util::test_uri("/test/Gemfile");
189
190 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
191 let position = Position::new(1, 7); let context = deps_core::completion::detect_completion_context(
194 parse_result.as_ref(),
195 position,
196 content,
197 );
198
199 match context {
200 deps_core::completion::CompletionContext::PackageName { prefix, range } => {
201 assert_eq!(prefix, "ra");
202 assert_ne!(range, Range::default());
203 assert_eq!(range, Range::new(Position::new(1, 5), Position::new(1, 10)));
204 }
205 other => panic!("Expected PackageName context, got {other:?}"),
206 }
207 }
208
209 #[tokio::test]
210 async fn test_complete_package_names_minimum_prefix() {
211 let cache = Arc::new(deps_core::HttpCache::new());
212 let ecosystem = BundlerEcosystem::new(cache);
213
214 let results = ecosystem
216 .complete_package_names("r", Range::default())
217 .await;
218 assert!(results.is_empty());
219
220 let results = ecosystem.complete_package_names("", Range::default()).await;
222 assert!(results.is_empty());
223 }
224
225 #[tokio::test]
226 async fn test_complete_package_names_max_length() {
227 let cache = Arc::new(deps_core::HttpCache::new());
228 let ecosystem = BundlerEcosystem::new(cache);
229
230 let long_prefix = "a".repeat(201);
232 let results = ecosystem
233 .complete_package_names(&long_prefix, Range::default())
234 .await;
235 assert!(results.is_empty());
236 }
237
238 #[tokio::test]
239 async fn test_lockfile_provider() {
240 let cache = Arc::new(deps_core::HttpCache::new());
241 let ecosystem = BundlerEcosystem::new(cache);
242 assert!(ecosystem.lockfile_provider().is_some());
243 }
244
245 #[tokio::test]
246 async fn test_parse_manifest() {
247 let cache = Arc::new(deps_core::HttpCache::new());
248 let ecosystem = BundlerEcosystem::new(cache);
249
250 let gemfile = r"source 'https://rubygems.org'
251gem 'rails', '~> 7.0'";
252
253 #[cfg(windows)]
254 let path = "C:/test/Gemfile";
255 #[cfg(not(windows))]
256 let path = "/test/Gemfile";
257 let uri = Uri::from_file_path(path).unwrap();
258
259 let result = ecosystem.parse_manifest(gemfile, &uri).await.unwrap();
260 assert_eq!(result.dependencies().len(), 1);
261 }
262}