1use 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 position_in_range,
10};
11use deps_maven::MavenCentralRegistry;
12
13use crate::formatter::GradleFormatter;
14
15pub struct GradleEcosystem {
16 registry: Arc<MavenCentralRegistry>,
17 formatter: GradleFormatter,
18}
19
20impl GradleEcosystem {
21 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
22 Self {
23 registry: Arc::new(MavenCentralRegistry::new(cache)),
24 formatter: GradleFormatter,
25 }
26 }
27
28 async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> {
29 deps_core::completion::complete_package_names_generic(self.registry.as_ref(), prefix, 20)
30 .await
31 }
32
33 async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
34 deps_core::completion::complete_versions_generic(
35 self.registry.as_ref(),
36 package_name,
37 prefix,
38 &[],
39 )
40 .await
41 }
42
43 fn detect_completion_context<'a>(
47 content: &'a str,
48 position: Position,
49 uri: &Uri,
50 ) -> (&'static str, &'a str) {
51 let path = uri.path().to_string();
52 let lines: Vec<&str> = content.lines().collect();
53 let line_idx = position.line as usize;
54 let col_idx = position.character as usize;
55
56 if line_idx >= lines.len() {
57 return ("", "");
58 }
59
60 let line = lines[line_idx];
61 let before_cursor = &line[..col_idx.min(line.len())];
62
63 if path.ends_with("libs.versions.toml") {
64 detect_catalog_context(before_cursor, line, col_idx)
65 } else if path.ends_with(".gradle.kts") || path.ends_with(".gradle") {
66 detect_dsl_context(before_cursor, line, col_idx)
67 } else {
68 ("", "")
69 }
70 }
71}
72
73fn detect_catalog_context<'a>(
75 before_cursor: &str,
76 line: &'a str,
77 col_idx: usize,
78) -> (&'static str, &'a str) {
79 let cursor = col_idx.min(line.len());
80 if let Some(eq_pos) = before_cursor.rfind("version")
82 && let after = &before_cursor[eq_pos..]
83 && after.contains('=')
84 && let Some(quote_start) = after.rfind('"')
85 {
86 let value_start = eq_pos + quote_start + 1;
87 if value_start <= cursor {
88 return ("version", &line[value_start..cursor]);
89 }
90 }
91
92 if let Some(eq_pos) = before_cursor.rfind("module")
94 && let after = &before_cursor[eq_pos..]
95 && after.contains('=')
96 && let Some(quote_start) = after.rfind('"')
97 {
98 let value_start = eq_pos + quote_start + 1;
99 if value_start <= cursor {
100 return ("package", &line[value_start..cursor]);
101 }
102 }
103
104 ("", "")
105}
106
107fn detect_dsl_context<'a>(
109 before_cursor: &str,
110 line: &'a str,
111 col_idx: usize,
112) -> (&'static str, &'a str) {
113 let cursor = col_idx.min(line.len());
114 let in_string = before_cursor
115 .chars()
116 .filter(|&c| c == '"' || c == '\'')
117 .count()
118 % 2
119 == 1;
120 if !in_string {
121 return ("", "");
122 }
123
124 let colon_count = before_cursor.chars().filter(|&c| c == ':').count();
125 let quote_char = if before_cursor.contains('"') {
126 '"'
127 } else {
128 '\''
129 };
130
131 let Some(open_pos) = before_cursor.rfind(quote_char) else {
132 return ("", "");
133 };
134
135 match colon_count {
136 0 | 1 => ("package", &line[open_pos + 1..cursor]),
137 _ => {
138 let version_start = before_cursor
139 .char_indices()
140 .filter(|(_, c)| *c == ':')
141 .nth(1)
142 .map(|(i, _)| i + 1)
143 .unwrap_or(before_cursor.len());
144 ("version", &line[version_start..cursor])
145 }
146 }
147}
148
149impl deps_core::ecosystem::private::Sealed for GradleEcosystem {}
150
151impl Ecosystem for GradleEcosystem {
152 fn id(&self) -> &'static str {
153 "gradle"
154 }
155
156 fn display_name(&self) -> &'static str {
157 "Gradle (JVM)"
158 }
159
160 fn manifest_filenames(&self) -> &[&'static str] {
161 &[
162 "libs.versions.toml",
163 "build.gradle.kts",
164 "build.gradle",
165 "settings.gradle.kts",
166 "settings.gradle",
167 ]
168 }
169
170 fn lockfile_filenames(&self) -> &[&'static str] {
171 &[]
172 }
173
174 fn parse_manifest<'a>(
175 &'a self,
176 content: &'a str,
177 uri: &'a Uri,
178 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
179 Box::pin(async move {
180 let result =
181 crate::parser::parse_gradle(content, uri).map_err(deps_core::DepsError::from)?;
182 Ok(Box::new(result) as Box<dyn ParseResultTrait>)
183 })
184 }
185
186 fn registry(&self) -> Arc<dyn Registry> {
187 self.registry.clone() as Arc<dyn Registry>
188 }
189
190 fn formatter(&self) -> &dyn EcosystemFormatter {
191 &self.formatter
192 }
193
194 fn generate_completions<'a>(
195 &'a self,
196 parse_result: &'a dyn ParseResultTrait,
197 position: Position,
198 content: &'a str,
199 ) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
200 Box::pin(async move {
201 let uri = parse_result.uri();
202 let (ctx_type, value) = Self::detect_completion_context(content, position, uri);
203
204 match ctx_type {
205 "version" => {
206 let dep = parse_result.dependencies().into_iter().find(|d| {
207 d.version_range()
208 .is_some_and(|r| position_in_range(position, r))
209 || d.name_range().start.line == position.line
210 });
211 if let Some(dep) = dep {
212 self.complete_versions(dep.name(), value).await
213 } else {
214 vec![]
215 }
216 }
217 "package" => self.complete_package_names(value).await,
218 _ => vec![],
219 }
220 })
221 }
222
223 fn as_any(&self) -> &dyn Any {
224 self
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn make_cache() -> Arc<deps_core::HttpCache> {
233 Arc::new(deps_core::HttpCache::new())
234 }
235
236 #[test]
237 fn test_ecosystem_id() {
238 let eco = GradleEcosystem::new(make_cache());
239 assert_eq!(eco.id(), "gradle");
240 }
241
242 #[test]
243 fn test_ecosystem_display_name() {
244 let eco = GradleEcosystem::new(make_cache());
245 assert_eq!(eco.display_name(), "Gradle (JVM)");
246 }
247
248 #[test]
249 fn test_manifest_filenames() {
250 let eco = GradleEcosystem::new(make_cache());
251 assert!(eco.manifest_filenames().contains(&"libs.versions.toml"));
252 assert!(eco.manifest_filenames().contains(&"build.gradle.kts"));
253 assert!(eco.manifest_filenames().contains(&"build.gradle"));
254 assert!(eco.manifest_filenames().contains(&"settings.gradle.kts"));
255 assert!(eco.manifest_filenames().contains(&"settings.gradle"));
256 }
257
258 #[test]
259 fn test_lockfile_filenames_empty() {
260 let eco = GradleEcosystem::new(make_cache());
261 assert!(eco.lockfile_filenames().is_empty());
262 }
263
264 #[test]
265 fn test_lockfile_provider_none() {
266 let eco = GradleEcosystem::new(make_cache());
267 assert!(eco.lockfile_provider().is_none());
268 }
269
270 #[test]
271 fn test_as_any() {
272 let eco = GradleEcosystem::new(make_cache());
273 assert!(eco.as_any().is::<GradleEcosystem>());
274 }
275
276 #[tokio::test]
277 async fn test_complete_package_names_short_prefix() {
278 let eco = GradleEcosystem::new(make_cache());
279 assert!(eco.complete_package_names("a").await.is_empty());
280 assert!(eco.complete_package_names("").await.is_empty());
281 }
282
283 #[tokio::test]
284 async fn test_parse_manifest_kts() {
285 let eco = GradleEcosystem::new(make_cache());
286 let content = "dependencies {\n implementation(\"junit:junit:4.13.2\")\n}\n";
287 let uri = Uri::from_file_path("/project/build.gradle.kts").unwrap();
288 let result = eco.parse_manifest(content, &uri).await.unwrap();
289 assert_eq!(result.dependencies().len(), 1);
290 }
291
292 #[test]
293 fn test_detect_catalog_context_version_cursor_at_start() {
294 let line = r#"version = "1.0.0""#;
296 let col = 11;
298 let before = &line[..col];
299 let (t, v) = detect_catalog_context(before, line, col);
300 assert_eq!(t, "version");
301 assert_eq!(v, "");
302 }
303
304 #[test]
305 fn test_detect_catalog_context_version_cursor_mid() {
306 let line = r#"version = "1.0.0""#;
308 let col = 14;
310 let before = &line[..col];
311 let (t, v) = detect_catalog_context(before, line, col);
312 assert_eq!(t, "version");
313 assert_eq!(v, "1.0");
314 }
315
316 #[test]
317 fn test_detect_catalog_context_version_cursor_at_end() {
318 let line = r#"version = "1.0.0""#;
320 let col = 16;
322 let before = &line[..col];
323 let (t, v) = detect_catalog_context(before, line, col);
324 assert_eq!(t, "version");
325 assert_eq!(v, "1.0.0");
326 }
327
328 #[test]
329 fn test_detect_catalog_context_module_prefix() {
330 let line = r#"module = "com.example:lib""#;
332 let col = 16;
334 let before = &line[..col];
335 let (t, v) = detect_catalog_context(before, line, col);
336 assert_eq!(t, "package");
337 assert_eq!(v, "com.ex");
338 }
339
340 #[test]
341 fn test_detect_dsl_context_package_cursor_mid() {
342 let line = r#"implementation("junit:junit:4.13.2")"#;
344 let col = 21;
347 let before = &line[..col];
348 let (t, v) = detect_dsl_context(before, line, col);
349 assert_eq!(t, "package");
350 assert_eq!(v, "junit");
351 }
352
353 #[test]
354 fn test_detect_dsl_context_version_cursor_mid() {
355 let line = r#"implementation("junit:junit:4.13.2")"#;
357 let col = 31;
359 let before = &line[..col];
360 let (t, v) = detect_dsl_context(before, line, col);
361 assert_eq!(t, "version");
362 assert_eq!(v, "4.1");
363 }
364
365 #[test]
366 fn test_detect_dsl_context_version_cursor_at_start() {
367 let line = r#"implementation("junit:junit:4.13.2")"#;
369 let col = 28;
371 let before = &line[..col];
372 let (t, v) = detect_dsl_context(before, line, col);
373 assert_eq!(t, "version");
374 assert_eq!(v, "");
375 }
376
377 #[tokio::test]
378 async fn test_parse_manifest_groovy() {
379 let eco = GradleEcosystem::new(make_cache());
380 let content = "dependencies {\n implementation 'junit:junit:4.13.2'\n}\n";
381 let uri = Uri::from_file_path("/project/build.gradle").unwrap();
382 let result = eco.parse_manifest(content, &uri).await.unwrap();
383 assert_eq!(result.dependencies().len(), 1);
384 }
385}