feat(java): Separate the jdtls JDK and add Settings for New Projects (#21)

- Languages & Frameworks › Java › Language Server › JDK (languages.java.jdtlsJdk) runs jdtls on its own JDK, like IDEA's JDK for importer; the project keeps compiling and running with the Project JDK.
- Toolchain pages get the Global / This project switch (default This project); Global shows and edits the global layer.
- File › New Projects Setup › Settings for New Projects… edits only the global layer, which new projects inherit.
- Configuration errors (launch_error) show as "Not started" instead of "Crashed".

Fixes #18
Fixes #19

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-24 18:26:31 +08:00
1 джерело a765959d48
коміт 059f25f13e
38 files changed
+486 -93

No files matched your search

+8
Переглянути файл
@@ -109,6 +109,14 @@ pub fn settings_get(window: Window, state: State<'_, AppState>) -> Settings {
}
}
/// Defaults plus the global layer, without any project's values: what new
/// projects (and projects without their own value) get.
#[tauri::command]
#[specta::specta]
pub fn settings_get_global(state: State<'_, AppState>) -> Settings {
state.global_settings.effective()
}
/// `value` is JSON text, e.g. `14` or `"dark"`. Every window (for a project
/// value: that project's windows) is told (`IdeEvent::SettingsChanged`) so it
/// re-reads its effective settings.
+1
Переглянути файл
@@ -48,6 +48,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
app::close_project,
app::languages,
app::settings_get,
app::settings_get_global,
app::settings_set,
app::project_roots,
app::cancel_request,
+4 -2
Переглянути файл
@@ -24,7 +24,8 @@ pub async fn window_new(app: AppHandle) -> IdeResult<String> {
windows::create_window(&app, None).map(|w| w.label().to_owned())
}
/// Opens this window's Settings window on `page` (or focuses it and navigates).
/// Opens this window's Settings window on `page` (or focuses it and navigates);
/// `new_projects` edits the global layer only ("Settings for New Projects").
/// `background` (`#rrggbb`) paints a new window until its page has loaded.
#[tauri::command]
#[specta::specta]
@@ -32,9 +33,10 @@ pub async fn window_open_settings(
app: AppHandle,
window: WebviewWindow,
page: Option<String>,
new_projects: bool,
background: Option<String>,
) -> IdeResult<()> {
windows::open_settings_window(&app, &window, page, background)
windows::open_settings_window(&app, &window, page, new_projects, background)
}
/// Focuses the next / previous window (⌘` / ⇧⌘`).
+6 -2
Переглянути файл
@@ -79,7 +79,7 @@ pub fn run() {
if let Ok(cwd) = std::env::current_dir()
&& let Some(path) = windows::cli_path(&std::env::args().collect::<Vec<_>>(), &cwd)
{
windows.set_launch("main", windows::Launch { project: Some(path.into()), settings_page: None });
windows.set_launch("main", windows::Launch { project: Some(path.into()), ..Default::default() });
}
let menu_state = menu::MenuState::default();
menu::install(app.handle(), &menu_state, &state.recent.list());
@@ -135,7 +135,11 @@ fn on_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) {
Some(w) => {
// Settings is modal for its project window: only app-level items get through, to the parent.
let label = match windows::settings_parent(w.label()) {
Some(parent) if matches!(route.action.as_str(), "app.quit" | "file.settings") => parent,
Some(parent)
if matches!(route.action.as_str(), "app.quit" | "file.settings" | "file.newProjectsSettings") =>
{
parent
}
Some(_) => return,
None => w.label(),
};
+6
Переглянути файл
@@ -208,6 +208,11 @@ pub fn spec(os: Os) -> Vec<Entry> {
act("file.save", "Save All"),
sub("fileProperties", "File Properties", vec![act("file.toggleReadOnly", "Make File Read-only / Writable")]),
SEP,
sub(
"newProjectsSetup",
"New Projects Setup",
vec![act("file.newProjectsSettings", "Settings for New Projects…")],
),
act("file.invalidateCaches", "Invalidate Caches…"),
];
if !mac {
@@ -972,6 +977,7 @@ mod tests {
"file.closeProject",
"file.save",
"file.settings",
"file.newProjectsSettings",
"window.new",
"window.next",
"window.previous",
+5
Переглянути файл
@@ -259,12 +259,17 @@ impl AppState {
if let Some(problem) = lombok.problem() {
tracing::warn!("{problem}");
}
let server_jdk = ide_sdk::apply::jdtls_jdk(&root, &effective).unwrap_or_else(|e| {
cfg.launch_error = Some(e);
None
});
ide_sdk::java::configure_jdtls(
cfg,
server_root,
&data_dir,
&jdks,
ide_sdk::apply::project_jdk(&root, &effective).as_ref(),
server_jdk.as_ref(),
min_java,
&bundles,
lombok.jar(),
+9 -6
Переглянути файл
@@ -39,6 +39,8 @@ pub struct Launch {
pub project: Option<ide_core::IpcPath>,
/// Settings windows: the page to show.
pub settings_page: Option<String>,
/// Settings windows: edit the global layer only ("Settings for New Projects").
pub new_projects: bool,
}
#[derive(Default)]
@@ -271,7 +273,7 @@ pub fn create_window(app: &AppHandle, project: Option<PathBuf>) -> IdeResult<Web
config.y = Some(pos.y + CASCADE);
config.center = false;
}
app.state::<WindowsState>().set_launch(&label, Launch { project: project.map(Into::into), settings_page: None });
app.state::<WindowsState>().set_launch(&label, Launch { project: project.map(Into::into), ..Default::default() });
let window = build_project_window(app, config)?;
focus(&window);
Ok(window)
@@ -338,7 +340,7 @@ pub fn open_path(app: &AppHandle, path: &Path) {
match free {
Some(w) => {
app.state::<WindowsState>()
.set_launch(w.label(), Launch { project: Some(root.into()), settings_page: None });
.set_launch(w.label(), Launch { project: Some(root.into()), ..Default::default() });
let _ = app.emit_to(w.label(), LAUNCH_EVENT, ());
focus(&w);
}
@@ -375,6 +377,7 @@ pub fn open_settings_window(
app: &AppHandle,
parent: &WebviewWindow,
page: Option<String>,
new_projects: bool,
background: Option<String>,
) -> IdeResult<()> {
// Asked from a settings window itself: navigate it.
@@ -382,8 +385,8 @@ pub fn open_settings_window(
let label = settings_label(&parent_label);
let windows = app.state::<WindowsState>();
if let Some(existing) = app.get_webview_window(&label) {
if page.is_some() {
windows.set_launch(&label, Launch { project: None, settings_page: page });
if page.is_some() || new_projects {
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
let _ = app.emit_to(label.as_str(), LAUNCH_EVENT, ());
}
focus(&existing);
@@ -406,7 +409,7 @@ pub fn open_settings_window(
if let Some(color) = background.and_then(|c| serde_json::from_value(serde_json::Value::String(c)).ok()) {
config.background_color = Some(color);
}
windows.set_launch(&label, Launch { project: None, settings_page: page });
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
let window = WebviewWindowBuilder::from_config(app, &config)
.and_then(|b| b.parent(&parent))
.and_then(|b| b.build())
@@ -485,7 +488,7 @@ mod tests {
#[test]
fn launches_are_taken_once() {
let st = WindowsState::default();
st.set_launch("project-1", Launch { project: Some("/p".into()), settings_page: None });
st.set_launch("project-1", Launch { project: Some("/p".into()), ..Default::default() });
assert_eq!(st.take_launch("project-1").and_then(|l| l.project), Some("/p".into()));
assert_eq!(st.take_launch("project-1"), None);
}