forked from zed-extensions/java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjdtls.rs
More file actions
382 lines (335 loc) · 13.3 KB
/
jdtls.rs
File metadata and controls
382 lines (335 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
use std::{
env::current_dir,
fs::{metadata, read_dir},
path::{Path, PathBuf},
};
use sha1::{Digest, Sha1};
use zed_extension_api::{
self as zed, DownloadedFileType, LanguageServerId, LanguageServerInstallationStatus, Os,
Worktree, current_platform, download_file,
http_client::{HttpMethod, HttpRequest, fetch},
make_file_executable,
serde_json::Value,
set_language_server_installation_status,
};
use crate::{
config::is_java_autodownload,
jdk::try_to_fetch_and_install_latest_jdk,
util::{
create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable,
get_java_major_version, get_latest_versions_from_tag, mark_checked_once, path_to_string,
remove_all_files_except, should_use_local_or_download,
},
};
const JDTLS_INSTALL_PATH: &str = "jdtls";
const JDTLS_REPO: &str = "eclipse-jdtls/eclipse.jdt.ls";
const LOMBOK_INSTALL_PATH: &str = "lombok";
const LOMBOK_REPO: &str = "projectlombok/lombok";
// Errors
const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java version 21 to run. You can either specify a different JDK to use by configuring lsp.jdtls.settings.java_home to point to a different JDK, or set lsp.jdtls.settings.jdk_auto_download to true to let the extension automatically download one for you.";
const JDTLS_VERION_ERROR: &str = "No version to fallback to";
pub fn build_jdtls_launch_args(
jdtls_path: &PathBuf,
configuration: &Option<Value>,
worktree: &Worktree,
jvm_args: Vec<String>,
language_server_id: &LanguageServerId,
) -> zed::Result<Vec<String>> {
if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) {
return Ok(vec![jdtls_launcher]);
}
let mut java_executable = get_java_executable(configuration, worktree, language_server_id)?;
let java_major_version = get_java_major_version(&java_executable)?;
if java_major_version < 21 {
if is_java_autodownload(configuration) {
java_executable =
try_to_fetch_and_install_latest_jdk(language_server_id, configuration)?
.join(get_java_exec_name());
} else {
return Err(JAVA_VERSION_ERROR.to_string());
}
}
let extension_workdir = get_curr_dir()?;
let jdtls_base_path = extension_workdir.join(jdtls_path);
let shared_config_path = get_shared_config_path(&jdtls_base_path);
let jar_path = find_equinox_launcher(&jdtls_base_path)?;
let jdtls_data_path = get_jdtls_data_path(worktree)?;
let mut args = vec![
path_to_string(java_executable)?,
"-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(),
"-Dosgi.bundles.defaultStartLevel=4".to_string(),
"-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(),
"-Dosgi.checkConfiguration=true".to_string(),
format!(
"-Dosgi.sharedConfiguration.area={}",
path_to_string(shared_config_path)?
),
"-Dosgi.sharedConfiguration.area.readOnly=true".to_string(),
"-Dosgi.configuration.cascaded=true".to_string(),
"-Xms1G".to_string(),
"--add-modules=ALL-SYSTEM".to_string(),
"--add-opens".to_string(),
"java.base/java.util=ALL-UNNAMED".to_string(),
"--add-opens".to_string(),
"java.base/java.lang=ALL-UNNAMED".to_string(),
];
args.extend(jvm_args);
args.extend(vec![
"-jar".to_string(),
path_to_string(jar_path)?,
"-data".to_string(),
path_to_string(jdtls_data_path)?,
]);
if java_major_version >= 24 {
args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string());
args.push("-Djdk.xml.totalEntitySizeLimit=0".to_string());
}
Ok(args)
}
pub fn find_latest_local_jdtls() -> Option<PathBuf> {
let prefix = PathBuf::from(JDTLS_INSTALL_PATH);
// walk the dir where we install jdtls
read_dir(&prefix)
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.is_dir())
// get the most recently created subdirectory
.filter_map(|path| {
let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?;
Some((path, created_time))
})
.max_by_key(|&(_, time)| time)
// and return it
.map(|(path, _)| path)
})
.ok()
.flatten()
}
pub fn find_latest_local_lombok() -> Option<PathBuf> {
let prefix = PathBuf::from(LOMBOK_INSTALL_PATH);
// walk the dir where we install lombok
read_dir(&prefix)
.map(|entries| {
entries
.filter_map(Result::ok)
.map(|entry| entry.path())
// get the most recently created jar file
.filter(|path| {
path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar")
})
.filter_map(|path| {
let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?;
Some((path, created_time))
})
.max_by_key(|&(_, time)| time)
.map(|(path, _)| path)
})
.ok()
.flatten()
}
pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option<String> {
let jdtls_executable_filename = match current_platform().0 {
Os::Windows => "jdtls.bat",
_ => "jdtls",
};
worktree.which(jdtls_executable_filename)
}
pub fn try_to_fetch_and_install_latest_jdtls(
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
) -> zed::Result<PathBuf> {
// Use local installation if update mode requires it
if let Some(path) =
should_use_local_or_download(configuration, find_latest_local_jdtls(), JDTLS_INSTALL_PATH)?
{
return Ok(path);
}
// Download latest version
set_language_server_installation_status(
language_server_id,
&LanguageServerInstallationStatus::CheckingForUpdate,
);
let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?;
let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref())
.map_or_else(
|_| {
second_last
.as_ref()
.ok_or(JDTLS_VERION_ERROR.to_string())
.and_then(|fallback| download_jdtls_milestone(fallback))
.map(|milestone| (second_last.unwrap(), milestone.trim_end().to_string()))
},
|milestone| Ok((last, milestone.trim_end().to_string())),
)?;
let prefix = PathBuf::from(JDTLS_INSTALL_PATH);
let build_directory = latest_version_build.replace(".tar.gz", "");
let build_path = prefix.join(&build_directory);
let binary_path = build_path.join("bin").join(get_binary_name());
// If latest version isn't installed,
if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) {
// then download it...
set_language_server_installation_status(
language_server_id,
&LanguageServerInstallationStatus::Downloading,
);
download_file(
&format!(
"https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}"
),
path_to_string(build_path.clone())?.as_str(),
DownloadedFileType::GzipTar,
)?;
make_file_executable(path_to_string(binary_path)?.as_str())?;
// ...and delete other versions
let _ = remove_all_files_except(prefix, build_directory.as_str());
// Mark the downloaded version for "Once" mode tracking
let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version);
}
// return jdtls base path
Ok(build_path)
}
pub fn try_to_fetch_and_install_latest_lombok(
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
) -> zed::Result<PathBuf> {
// Use local installation if update mode requires it
if let Some(path) = should_use_local_or_download(
configuration,
find_latest_local_lombok(),
LOMBOK_INSTALL_PATH,
)? {
return Ok(path);
}
// Download latest version
set_language_server_installation_status(
language_server_id,
&LanguageServerInstallationStatus::CheckingForUpdate,
);
let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO)?;
let prefix = LOMBOK_INSTALL_PATH;
let jar_name = format!("lombok-{latest_version}.jar");
let jar_path = Path::new(prefix).join(&jar_name);
// If latest version isn't installed,
if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) {
// then download it...
set_language_server_installation_status(
language_server_id,
&LanguageServerInstallationStatus::Downloading,
);
create_path_if_not_exists(prefix)?;
download_file(
&format!("https://projectlombok.org/downloads/{jar_name}"),
path_to_string(jar_path.clone())?.as_str(),
DownloadedFileType::Uncompressed,
)?;
// ...and delete other versions
let _ = remove_all_files_except(prefix, jar_name.as_str());
// Mark the downloaded version for "Once" mode tracking
let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version);
}
// else use it
Ok(jar_path)
}
fn download_jdtls_milestone(version: &str) -> zed::Result<String> {
String::from_utf8(
fetch(
&HttpRequest::builder()
.method(HttpMethod::Get)
.url(format!(
"https://download.eclipse.org/jdtls/milestones/{version}/latest.txt"
))
.build()?,
)
.map_err(|err| format!("failed to get latest version's build: {err}"))?
.body,
)
.map_err(|err| {
format!("attempt to get latest version's build resulted in a malformed response: {err}")
})
}
fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result<PathBuf, String> {
let plugins_dir = jdtls_base_directory.join("plugins");
// if we have `org.eclipse.equinox.launcher.jar` use that
let specific_launcher = plugins_dir.join("org.eclipse.equinox.launcher.jar");
if specific_launcher.is_file() {
return Ok(specific_launcher);
}
// else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar'
let entries =
read_dir(&plugins_dir).map_err(|e| format!("Failed to read plugins directory: {e}"))?;
entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.find(|path| {
path.is_file()
&& path.file_name().and_then(|s| s.to_str()).is_some_and(|s| {
s.starts_with("org.eclipse.equinox.launcher_") && s.ends_with(".jar")
})
})
.ok_or_else(|| "Cannot find equinox launcher".to_string())
}
fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result<PathBuf> {
// Note: the JDTLS data path is where JDTLS stores its own caches.
// In the unlikely event we can't find the canonical OS-Level cache-path,
// we fall back to the the extension's workdir, which may never get cleaned up.
// In future we may want to deliberately manage caches to be able to force-clean them.
let env = worktree.shell_env();
let base_cachedir = match current_platform().0 {
Os::Mac => env
.iter()
.find(|(k, _)| k == "XDG_CACHE_HOME")
.map(|(_, v)| PathBuf::from(v))
.or_else(|| {
env.iter()
.find(|(k, _)| k == "HOME")
.map(|(_, v)| PathBuf::from(v).join("Library").join("Caches"))
}),
Os::Linux => env
.iter()
.find(|(k, _)| k == "XDG_CACHE_HOME")
.map(|(_, v)| PathBuf::from(v))
.or_else(|| {
env.iter()
.find(|(k, _)| k == "HOME")
.map(|(_, v)| PathBuf::from(v).join(".cache"))
}),
Os::Windows => env
.iter()
.find(|(k, _)| k == "APPDATA")
.map(|(_, v)| PathBuf::from(v)),
}
.unwrap_or_else(|| {
current_dir()
.expect("should be able to get extension workdir")
.join("caches")
});
// caches are unique per worktree-root-path
let cache_key = worktree.root_path();
let hex_digest = get_sha1_hex(&cache_key);
let unique_dir_name = format!("jdtls-{hex_digest}");
Ok(base_cachedir.join(unique_dir_name))
}
fn get_binary_name() -> &'static str {
match current_platform().0 {
Os::Windows => "jdtls.bat",
_ => "jdtls",
}
}
fn get_sha1_hex(input: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(input.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
fn get_shared_config_path(jdtls_base_directory: &Path) -> PathBuf {
// Note: JDTLS also provides config_linux_arm and config_mac_arm (and others),
// but does not use them in their own launch script. It may be worth investigating if we should use them when appropriate.
let config_to_use = match current_platform().0 {
Os::Linux => "config_linux",
Os::Mac => "config_mac",
Os::Windows => "config_win",
};
jdtls_base_directory.join(config_to_use)
}