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)
65 } else if path.ends_with(".gradle.kts") || path.ends_with(".gradle") {
66 detect_dsl_context(before_cursor, line)
67 } else {
68 ("", "")
69 }
70 }
71}
72
73fn detect_catalog_context<'a>(before_cursor: &str, line: &'a str) -> (&'static str, &'a str) {
75 if let Some(eq_pos) = before_cursor.rfind("version")
77 && let after = &before_cursor[eq_pos..]
78 && after.contains('=')
79 && let Some(quote_start) = after.rfind('"')
80 {
81 let value_start = eq_pos + quote_start + 1;
82 if value_start <= line.len() {
83 let quote_end = line[value_start..]
84 .find('"')
85 .map_or(line.len(), |i| value_start + i);
86 return ("version", &line[value_start..quote_end]);
87 }
88 }
89
90 if let Some(eq_pos) = before_cursor.rfind("module")
92 && let after = &before_cursor[eq_pos..]
93 && after.contains('=')
94 && let Some(quote_start) = after.rfind('"')
95 {
96 let value_start = eq_pos + quote_start + 1;
97 if value_start <= line.len() {
98 let quote_end = line[value_start..]
99 .find('"')
100 .map_or(line.len(), |i| value_start + i);
101 return ("package", &line[value_start..quote_end]);
102 }
103 }
104
105 ("", "")
106}
107
108fn detect_dsl_context<'a>(before_cursor: &str, line: &'a str) -> (&'static str, &'a str) {
110 let in_string = before_cursor
111 .chars()
112 .filter(|&c| c == '"' || c == '\'')
113 .count()
114 % 2
115 == 1;
116 if !in_string {
117 return ("", "");
118 }
119
120 let colon_count = before_cursor.chars().filter(|&c| c == ':').count();
121 let quote_char = if before_cursor.contains('"') {
122 '"'
123 } else {
124 '\''
125 };
126
127 let Some(open_pos) = before_cursor.rfind(quote_char) else {
128 return ("", "");
129 };
130
131 match colon_count {
132 0 | 1 => {
133 let close = line[open_pos + 1..]
134 .find(['"', '\''])
135 .map_or(line.len(), |i| open_pos + 1 + i);
136 ("package", &line[open_pos + 1..close])
137 }
138 _ => {
139 let version_start = before_cursor
140 .char_indices()
141 .filter(|(_, c)| *c == ':')
142 .nth(1)
143 .map(|(i, _)| i + 1)
144 .unwrap_or(before_cursor.len());
145 let close = line[version_start..]
146 .find(['"', '\''])
147 .map_or(line.len(), |i| version_start + i);
148 ("version", &line[version_start..close])
149 }
150 }
151}
152
153impl deps_core::ecosystem::private::Sealed for GradleEcosystem {}
154
155impl Ecosystem for GradleEcosystem {
156 fn id(&self) -> &'static str {
157 "gradle"
158 }
159
160 fn display_name(&self) -> &'static str {
161 "Gradle (JVM)"
162 }
163
164 fn manifest_filenames(&self) -> &[&'static str] {
165 &[
166 "libs.versions.toml",
167 "build.gradle.kts",
168 "build.gradle",
169 "settings.gradle.kts",
170 "settings.gradle",
171 ]
172 }
173
174 fn lockfile_filenames(&self) -> &[&'static str] {
175 &[]
176 }
177
178 fn parse_manifest<'a>(
179 &'a self,
180 content: &'a str,
181 uri: &'a Uri,
182 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
183 Box::pin(async move {
184 let result =
185 crate::parser::parse_gradle(content, uri).map_err(deps_core::DepsError::from)?;
186 Ok(Box::new(result) as Box<dyn ParseResultTrait>)
187 })
188 }
189
190 fn registry(&self) -> Arc<dyn Registry> {
191 self.registry.clone() as Arc<dyn Registry>
192 }
193
194 fn formatter(&self) -> &dyn EcosystemFormatter {
195 &self.formatter
196 }
197
198 fn generate_completions<'a>(
199 &'a self,
200 parse_result: &'a dyn ParseResultTrait,
201 position: Position,
202 content: &'a str,
203 ) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
204 Box::pin(async move {
205 let uri = parse_result.uri();
206 let (ctx_type, value) = Self::detect_completion_context(content, position, uri);
207
208 match ctx_type {
209 "version" => {
210 let dep = parse_result.dependencies().into_iter().find(|d| {
211 d.version_range()
212 .is_some_and(|r| position_in_range(position, r))
213 || d.name_range().start.line == position.line
214 });
215 if let Some(dep) = dep {
216 self.complete_versions(dep.name(), value).await
217 } else {
218 vec![]
219 }
220 }
221 "package" => self.complete_package_names(value).await,
222 _ => vec![],
223 }
224 })
225 }
226
227 fn as_any(&self) -> &dyn Any {
228 self
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn make_cache() -> Arc<deps_core::HttpCache> {
237 Arc::new(deps_core::HttpCache::new())
238 }
239
240 #[test]
241 fn test_ecosystem_id() {
242 let eco = GradleEcosystem::new(make_cache());
243 assert_eq!(eco.id(), "gradle");
244 }
245
246 #[test]
247 fn test_ecosystem_display_name() {
248 let eco = GradleEcosystem::new(make_cache());
249 assert_eq!(eco.display_name(), "Gradle (JVM)");
250 }
251
252 #[test]
253 fn test_manifest_filenames() {
254 let eco = GradleEcosystem::new(make_cache());
255 assert!(eco.manifest_filenames().contains(&"libs.versions.toml"));
256 assert!(eco.manifest_filenames().contains(&"build.gradle.kts"));
257 assert!(eco.manifest_filenames().contains(&"build.gradle"));
258 assert!(eco.manifest_filenames().contains(&"settings.gradle.kts"));
259 assert!(eco.manifest_filenames().contains(&"settings.gradle"));
260 }
261
262 #[test]
263 fn test_lockfile_filenames_empty() {
264 let eco = GradleEcosystem::new(make_cache());
265 assert!(eco.lockfile_filenames().is_empty());
266 }
267
268 #[test]
269 fn test_lockfile_provider_none() {
270 let eco = GradleEcosystem::new(make_cache());
271 assert!(eco.lockfile_provider().is_none());
272 }
273
274 #[test]
275 fn test_as_any() {
276 let eco = GradleEcosystem::new(make_cache());
277 assert!(eco.as_any().is::<GradleEcosystem>());
278 }
279
280 #[tokio::test]
281 async fn test_complete_package_names_short_prefix() {
282 let eco = GradleEcosystem::new(make_cache());
283 assert!(eco.complete_package_names("a").await.is_empty());
284 assert!(eco.complete_package_names("").await.is_empty());
285 }
286
287 #[tokio::test]
288 async fn test_parse_manifest_kts() {
289 let eco = GradleEcosystem::new(make_cache());
290 let content = "dependencies {\n implementation(\"junit:junit:4.13.2\")\n}\n";
291 let uri = Uri::from_file_path("/project/build.gradle.kts").unwrap();
292 let result = eco.parse_manifest(content, &uri).await.unwrap();
293 assert_eq!(result.dependencies().len(), 1);
294 }
295
296 #[tokio::test]
297 async fn test_parse_manifest_groovy() {
298 let eco = GradleEcosystem::new(make_cache());
299 let content = "dependencies {\n implementation 'junit:junit:4.13.2'\n}\n";
300 let uri = Uri::from_file_path("/project/build.gradle").unwrap();
301 let result = eco.parse_manifest(content, &uri).await.unwrap();
302 assert_eq!(result.dependencies().len(), 1);
303 }
304}