diff --git a/Cargo.lock b/Cargo.lock index 6a87905c3..02bb0950e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4374,6 +4374,7 @@ dependencies = [ "ide-jsonrpc", "ide-language", "ide-text", + "ide-vfs", "ignore", "lsp-types", "parking_lot", diff --git a/crates/ide-core/src/editor_settings.rs b/crates/ide-core/src/editor_settings.rs index a478558c6..4806bf2cd 100644 --- a/crates/ide-core/src/editor_settings.rs +++ b/crates/ide-core/src/editor_settings.rs @@ -371,6 +371,12 @@ pub struct TextAttributesOverride { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] #[serde(default, rename_all = "camelCase")] pub struct LanguageCodeStyle { + /// Java formatter: `default` | `intellij` | `custom` (other languages ignore it). + #[serde(skip_serializing_if = "Option::is_none")] + pub formatter: Option, + /// Eclipse formatter XML; relative paths start at the project root. + #[serde(skip_serializing_if = "Option::is_none")] + pub formatter_path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub use_tab: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/ide-host/src/ops/project.rs b/crates/ide-host/src/ops/project.rs index 4c17cdde1..98f95a887 100644 --- a/crates/ide-host/src/ops/project.rs +++ b/crates/ide-host/src/ops/project.rs @@ -172,9 +172,12 @@ pub fn set_settings( host.apply_proxy(); } // A new interpreter / toolchain / server executable restarts the language servers it affects. - if ["languages", "tools", "languageServers", "projectStructure"] - .iter() - .any(|k| key == *k || key.starts_with(&format!("{k}."))) + if key == "editor" + || key == "editor.codeStyle" + || key.starts_with("editor.codeStyle.") + || ["languages", "tools", "languageServers", "projectStructure"] + .iter() + .any(|k| key == *k || key.starts_with(&format!("{k}."))) { for project in affected() { if let Some(bridge) = &project.kotlin_bridge { diff --git a/crates/ide-host/src/project.rs b/crates/ide-host/src/project.rs index f28c7cffe..baf7e10ea 100644 --- a/crates/ide-host/src/project.rs +++ b/crates/ide-host/src/project.rs @@ -129,6 +129,8 @@ impl Host { let root = root.clone(); let data_dir = self.data_dir.clone(); let config_dir = self.config_dir.clone(); + let sink = sink.clone(); + let formatter_problem = std::sync::Mutex::new(None); lsp.set_config_hook(Box::new(move |cfg, server_root| { let member = ide_workspace::workspace::Workspace::load(&root).ok().flatten().and_then(|w| { w.projects.into_iter().filter(|p| server_root.starts_with(p)).max_by_key(|p| p.components().count()) @@ -142,6 +144,27 @@ impl Host { ide_sdk::apply::configure_server(cfg, root, server_root, &effective); ide_sdk::java::cap_java_home(cfg, &ide_sdk::java::installed_jdks()); if cfg.name == "jdtls" { + let problem = ide_lsp::jdtls::configure_formatter( + cfg, + effective.editor.code_style.languages.get("java"), + root, + &data_dir, + effective.language_servers.get("jdtls"), + ) + .err() + .map(|e| e.to_string()); + let mut previous = formatter_problem.lock().unwrap(); + if problem != *previous { + if let Some(body) = &problem { + sink.emit(IdeEvent::Notify { + level: ide_core::event::NotifyLevel::Warning, + title: "Java formatter configuration unavailable".into(), + body: Some(body.clone()), + }); + } + *previous = problem; + } + drop(previous); ide_lsp::jdtls::configure_source_paths( cfg, ide_workspace::roots::unmanaged_java_source_paths( diff --git a/crates/ide-lsp/Cargo.toml b/crates/ide-lsp/Cargo.toml index 8b011c795..81cd83810 100644 --- a/crates/ide-lsp/Cargo.toml +++ b/crates/ide-lsp/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] ide-core.workspace = true +ide-vfs.workspace = true dunce.workspace = true ide-text.workspace = true ide-language.workspace = true diff --git a/crates/ide-lsp/src/jdtls.rs b/crates/ide-lsp/src/jdtls.rs index f2b79e0c1..82a52ef41 100644 --- a/crates/ide-lsp/src/jdtls.rs +++ b/crates/ide-lsp/src/jdtls.rs @@ -17,6 +17,9 @@ use std::sync::Arc; use std::time::{Duration, Instant}; const SERVER: &str = "jdtls"; + +mod formatter; +pub use formatter::configure_formatter; /// First start imports the Maven / Gradle model, which can take minutes. const READY_TIMEOUT: Duration = Duration::from_secs(240); /// A workspace build of a large multi-module project can take far longer than diff --git a/crates/ide-lsp/src/jdtls/formatter.rs b/crates/ide-lsp/src/jdtls/formatter.rs new file mode 100644 index 000000000..f7c19dea6 --- /dev/null +++ b/crates/ide-lsp/src/jdtls/formatter.rs @@ -0,0 +1,224 @@ +use ide_core::IdeResult; +use ide_core::editor_settings::LanguageCodeStyle; +use ide_core::settings::LanguageServerSettings; +use ide_language::LanguageServerConfig; +use serde_json::json; +use std::path::Path; + +const PROFILE: &str = "OpenIDEA IntelliJ IDEA"; +const XML: &str = include_str!("intellij.xml"); + +/// Protocol settings only: switching profiles reaches a running jdtls through +/// didChangeConfiguration. Explicit language-server settings are applied later. +pub fn configure_formatter( + cfg: &mut LanguageServerConfig, + style: Option<&LanguageCodeStyle>, + project_root: &Path, + data_dir: &Path, + overrides: Option<&LanguageServerSettings>, +) -> IdeResult<()> { + if cfg.name != "jdtls" { + return Ok(()); + } + // jdtls retains omitted preferences. Send empty values when returning to + // Default, while preserving any profile from the base server configuration. + let inherited = crate::handler::all_settings(cfg); + let defaults = json!({ + "url": inherited.pointer("/java/format/settings/url").cloned().unwrap_or(json!("")), + "profile": inherited.pointer("/java/format/settings/profile").cloned().unwrap_or(json!("")), + }); + let mut settings = cfg.settings.take().unwrap_or_else(|| json!({})); + settings["java"]["format"]["settings"] = formatter_overrides(defaults, overrides); + cfg.settings = Some(settings); + let Some(style) = style else { return Ok(()) }; + let (path, profile) = match style.formatter.as_deref() { + Some("intellij") => { + let path = data_dir.join("lsp/jdtls/intellij.xml"); + std::fs::create_dir_all(path.parent().unwrap())?; + if std::fs::read_to_string(&path).ok().as_deref() != Some(XML) { + ide_vfs::write_atomic(&path, XML.as_bytes())?; + } + (path, PROFILE) + } + Some("custom") => { + let value = style.formatter_path.as_deref().unwrap_or_default().trim(); + if value.is_empty() { + return Err(ide_core::IdeError::invalid("Choose an Eclipse formatter XML file.")); + } + let path = project_root.join(value); + // Fail without poisoning the server launch; code intelligence remains available. + std::fs::File::open(&path).map_err(|error| { + let path = path.display(); + ide_core::IdeError::invalid(format!("Cannot read Java formatter XML {path}: {error}")) + })?; + if !path.is_file() { + return Err(ide_core::IdeError::invalid(format!("Not a formatter XML file: {}", path.display()))); + } + (path, "") + } + _ => return Ok(()), + }; + let mut settings = cfg.settings.take().unwrap_or_else(|| json!({})); + settings["java"]["format"]["settings"] = formatter_overrides( + json!({ + "url": crate::uri::path_to_uri_string(&path), + "profile": profile, + }), + overrides, + ); + cfg.settings = Some(settings); + Ok(()) +} + +fn formatter_overrides(mut format: serde_json::Value, overrides: Option<&LanguageServerSettings>) -> serde_json::Value { + if let Some(overrides) = overrides { + // all_settings merges protocol settings over initializationOptions. + // Mirror explicit formatter keys so generated protocol values cannot + // accidentally mask an advanced initializationOptions override. + for layer in [ + overrides.initialization_options.as_ref().and_then(|v| v.pointer("/settings/java/format/settings")), + overrides.settings.as_ref().and_then(|v| v.pointer("/settings/java/format/settings")), + overrides.settings.as_ref().and_then(|v| v.pointer("/java/format/settings")), + ] + .into_iter() + .flatten() + { + for key in ["url", "profile"] { + if let Some(value) = layer.get(key) { + format[key] = value.clone(); + } + } + } + } + format +} + +#[cfg(test)] +mod tests { + use super::*; + + fn config(name: &str) -> LanguageServerConfig { + serde_json::from_value(json!({"name": name, "command": name})).unwrap() + } + + #[test] + fn profiles_resolve_paths_preserve_settings_and_leave_other_servers_alone() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path().join("project with 空格"); + let data = dir.path().join("data"); + std::fs::create_dir_all(&root).unwrap(); + let custom = root.join("style.xml"); + std::fs::write(&custom, XML).unwrap(); + for name in ["jdtls", "rust-analyzer"] { + for mode in ["default", "intellij", "custom", "unknown"] { + let mut cfg = LanguageServerConfig { + name: name.into(), + settings: Some(json!({"java": {"autobuild": {"enabled": false}}})), + ..config(name) + }; + let initial = cfg.clone(); + let style = LanguageCodeStyle { + formatter: Some(mode.into()), + formatter_path: Some("style.xml".into()), + ..Default::default() + }; + configure_formatter(&mut cfg, Some(&style), &root, &data, None).unwrap(); + if name != "jdtls" { + assert_eq!(cfg, initial); + continue; + } + let settings = crate::handler::all_settings(&cfg); + if !["intellij", "custom"].contains(&mode) { + assert_eq!(settings["java"]["format"]["settings"], json!({"url": "", "profile": ""})); + continue; + } + let path = if mode == "custom" { custom.clone() } else { data.join("lsp/jdtls/intellij.xml") }; + assert_eq!(settings["java"]["format"]["settings"]["url"], crate::uri::path_to_uri_string(&path)); + assert_eq!( + settings["java"]["format"]["settings"]["profile"], + if mode == "custom" { "" } else { PROFILE } + ); + assert_eq!(settings["java"]["autobuild"]["enabled"], false); + assert_eq!(std::fs::read_to_string(path).unwrap(), XML); + } + } + } + + #[test] + fn unavailable_custom_paths_are_nonfatal_and_do_not_set_launch_error() { + let dir = tempfile::tempdir().unwrap(); + for path in ["", "missing.xml", "."] { + let mut cfg = config("jdtls"); + let style = LanguageCodeStyle { + formatter: Some("custom".into()), + formatter_path: Some(path.into()), + ..Default::default() + }; + assert!(configure_formatter(&mut cfg, Some(&style), dir.path(), dir.path(), None).is_err()); + assert!(cfg.launch_error.is_none()); + assert_eq!(crate::handler::all_settings(&cfg)["java"]["format"]["settings"]["url"], ""); + } + } + + #[test] + fn default_keeps_explicit_server_configuration_and_custom_accepts_absolute_paths() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("custom.xml"); + std::fs::write(&path, XML).unwrap(); + let mut cfg = config("jdtls"); + cfg.initialization_options = Some( + toml::Value::try_from(json!({ + "settings": {"java": {"format": {"settings": {"url": "existing.xml", "profile": "Existing"}}}} + })) + .unwrap(), + ); + configure_formatter(&mut cfg, None, dir.path(), dir.path(), None).unwrap(); + assert_eq!( + crate::handler::all_settings(&cfg)["java"]["format"]["settings"], + json!({"url": "existing.xml", "profile": "Existing"}) + ); + let style = LanguageCodeStyle { + formatter: Some("custom".into()), + formatter_path: Some(path.to_string_lossy().into_owned()), + ..Default::default() + }; + configure_formatter(&mut cfg, Some(&style), &dir.path().join("different-root"), dir.path(), None).unwrap(); + assert_eq!( + crate::handler::all_settings(&cfg)["java"]["format"]["settings"]["url"], + crate::uri::path_to_uri_string(&path) + ); + } + #[test] + fn advanced_formatter_options_win_for_each_profile_and_protocol_layer() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("custom.xml"), XML).unwrap(); + for mode in ["default", "intellij", "custom"] { + for layer in ["initialization", "wrapped", "direct"] { + let mut overrides = LanguageServerSettings { + initialization_options: Some(json!({"settings": {"java": {"format": {"settings": { + "url": "advanced.xml", "profile": "Advanced" + }}}}})), + ..Default::default() + }; + if layer != "initialization" { + let value = json!({"java": {"format": {"settings": {"profile": "Protocol"}}}}); + overrides.settings = Some(if layer == "wrapped" { json!({"settings": value}) } else { value }); + } + let mut cfg = config("jdtls"); + let style = LanguageCodeStyle { + formatter: Some(mode.into()), + formatter_path: Some("custom.xml".into()), + ..Default::default() + }; + configure_formatter(&mut cfg, Some(&style), dir.path(), dir.path(), Some(&overrides)).unwrap(); + assert_eq!( + crate::handler::all_settings(&cfg)["java"]["format"]["settings"], + json!({ + "url": "advanced.xml", + "profile": if layer == "initialization" { "Advanced" } else { "Protocol" }, + }) + ); + } + } + } +} diff --git a/crates/ide-lsp/src/jdtls/intellij.xml b/crates/ide-lsp/src/jdtls/intellij.xml new file mode 100644 index 000000000..5dba06392 --- /dev/null +++ b/crates/ide-lsp/src/jdtls/intellij.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/ide-lsp/tests/mock_server.rs b/crates/ide-lsp/tests/mock_server.rs index a636e5e4d..7bfb45ccd 100644 --- a/crates/ide-lsp/tests/mock_server.rs +++ b/crates/ide-lsp/tests/mock_server.rs @@ -1017,6 +1017,70 @@ async fn settings_changes_reach_servers_without_restarts() { assert_eq!(s.launches.load(Ordering::SeqCst), 1); } +#[tokio::test(flavor = "multi_thread")] +async fn java_formatter_profiles_reach_initialization_and_live_configuration_without_restart() { + let h = harness_configured("java", "java", &[("jdtls", incremental_caps())], |cfg| { + cfg.initialization_options = Some( + toml::Value::try_from(json!({ + "settings": {"java": {"autobuild": {"enabled": false}}} + })) + .unwrap(), + ); + }); + let data = tempfile::tempdir().unwrap(); + std::fs::write(h.root.join("custom.xml"), "").unwrap(); + let style = Arc::new(Mutex::new(ide_core::editor_settings::LanguageCodeStyle { + formatter: Some("intellij".into()), + formatter_path: Some("custom.xml".into()), + ..Default::default() + })); + h.mgr.set_config_hook(Box::new({ + let style = style.clone(); + let root = h.root.clone(); + let data = data.path().to_path_buf(); + move |cfg, _| ide_lsp::jdtls::configure_formatter(cfg, Some(&style.lock()), &root, &data, None).unwrap() + })); + h.open("Main.java", "public class Main {}"); + h.wait_status("jdtls", ServerStatus::Ready).await; + let state = h.launcher.state("jdtls"); + let init = state.requested("initialize")[0].clone(); + assert_eq!( + init["initializationOptions"]["settings"]["java"]["format"]["settings"]["profile"], + "OpenIDEA IntelliJ IDEA" + ); + for mode in ["custom", "default", "intellij", "default"] { + style.lock().formatter = Some(mode.into()); + h.mgr.reconfigure().await; + let expected = match mode { + "custom" => uri_of(&h.root.join("custom.xml")), + "intellij" => uri_of(&data.path().join("lsp/jdtls/intellij.xml")), + _ => String::new(), + }; + eventually(|| { + state + .notified("workspace/didChangeConfiguration") + .last() + .is_some_and(|p| p["settings"]["java"]["format"]["settings"]["url"] == expected) + }) + .await; + let result: Value = state + .peer() + .request( + "workspace/configuration", + json!({ + "items": [{"section": "java.format.settings"}, {"section": "java.autobuild.enabled"}] + }), + None, + ) + .await + .unwrap(); + assert_eq!(result[0]["url"], expected); + assert_eq!(result[0]["profile"], if mode == "intellij" { "OpenIDEA IntelliJ IDEA" } else { "" }); + assert_eq!(result[1], false); + assert_eq!(state.launches.load(Ordering::SeqCst), 1); + } +} + #[tokio::test(flavor = "multi_thread")] async fn java_source_paths_reach_initialization_live_configuration_and_builds() { let h = harness_configured("java", "java", &[("jdtls", incremental_caps())], |cfg| { diff --git a/src/bindings.ts b/src/bindings.ts index c3d5811cd..b65240187 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -3903,6 +3903,10 @@ export type LanguageCodeStyle = LanguageCodeStyle_Serialize | LanguageCodeStyle_ /** Editor › Code Style › › Tabs and Indents (unset fields inherit). */ export type LanguageCodeStyle_Deserialize = { + /** Java formatter: `default` | `intellij` | `custom` (other languages ignore it). */ + formatter?: string | null, + /** Eclipse formatter XML; relative paths start at the project root. */ + formatterPath?: string | null, useTab?: boolean | null, tabSize?: number | null, indent?: number | null, @@ -3915,6 +3919,10 @@ export type LanguageCodeStyle_Deserialize = { /** Editor › Code Style › › Tabs and Indents (unset fields inherit). */ export type LanguageCodeStyle_Serialize = { + /** Java formatter: `default` | `intellij` | `custom` (other languages ignore it). */ + formatter?: string | null, + /** Eclipse formatter XML; relative paths start at the project root. */ + formatterPath?: string | null, useTab?: boolean | null, tabSize?: number | null, indent?: number | null, diff --git a/src/features/settings/__tests__/fileTypes.test.ts b/src/features/settings/__tests__/fileTypes.test.ts index 35c6c3261..0da59f0bd 100644 --- a/src/features/settings/__tests__/fileTypes.test.ts +++ b/src/features/settings/__tests__/fileTypes.test.ts @@ -87,6 +87,9 @@ describe("code style scheme", () => { it("copies every Code Style value to the other scheme", () => { stage("editor.codeStyle", "global", "editor.codeStyle.hardWrapAt", 100); stage("editor.codeStyle", "global", "editor.codeStyle.formatterTagsEnabled", true); + stage("editor.codeStyle", "global", "editor.codeStyle.languages", { + java: { formatter: "custom", formatterPath: "config/formatter.xml" }, + }); copyScheme("editor.codeStyle", "global", "project"); const s = useSettingsDialog.getState(); expect(stagedValue(s, "project", "editor.codeStyle.hardWrapAt")).toBe(100); diff --git a/src/features/settings/__tests__/javaFormatter.test.tsx b/src/features/settings/__tests__/javaFormatter.test.tsx new file mode 100644 index 000000000..f019ca2e4 --- /dev/null +++ b/src/features/settings/__tests__/javaFormatter.test.tsx @@ -0,0 +1,68 @@ +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useProject } from "@/platform/project"; +import { PageContext } from "../components/context"; +import { DEFAULT_SETTINGS } from "../model/schema"; +import { openSettings, stagedValue, useSettingsDialog } from "../model/staging"; +import { codeStyleLanguagePages } from "../pages/editor/codeStyle"; + +const mocks = vi.hoisted(() => ({ open: vi.fn() })); +vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.open })); +vi.mock("@/platform/ipc", async (original) => { + const actual = await original(); + return { + ...actual, + isTauri: () => true, + commands: { + ...actual.commands, + languages: async () => [ + { id: "java", name: "Java", indent: { tabSize: 4, insertSpaces: true } }, + { id: "rust", name: "Rust" }, + ], + }, + }; +}); + +beforeEach(() => { + useProject.setState({ settings: structuredClone(DEFAULT_SETTINGS), project: null }); + openSettings("editor.codeStyle.java"); +}); +afterEach(cleanup); + +async function page(language: string) { + const pages = await codeStyleLanguagePages(); + const spec = pages.find((p) => p.id === `editor.codeStyle.${language}`)!; + const Page = spec.component!; + render( + + + , + ); +} +const value = () => stagedValue(useSettingsDialog.getState(), "global", "editor.codeStyle.languages"); + +describe("Java formatting profile", () => { + it("stages profile and relative XML paths, browses for XML, and resets language defaults", async () => { + await page("java"); + fireEvent.click(screen.getByRole("button", { name: "Formatting profile" })); + fireEvent.click(screen.getByRole("option", { name: "Custom Eclipse formatter XML" })); + fireEvent.change(screen.getByLabelText("Eclipse formatter XML"), { target: { value: "config/style.xml" } }); + expect(value()).toEqual({ java: { formatter: "custom", formatterPath: "config/style.xml" } }); + mocks.open.mockResolvedValue("/project/custom.xml"); + await act(async () => fireEvent.click(screen.getByRole("button", { name: "Browse…" }))); + expect(value()).toEqual({ java: { formatter: "custom", formatterPath: "/project/custom.xml" } }); + expect(mocks.open).toHaveBeenCalledWith( + expect.objectContaining({ filters: [{ name: "XML", extensions: ["xml"] }] }), + ); + fireEvent.click(screen.getByRole("button", { name: "Formatting profile" })); + fireEvent.click(screen.getByRole("option", { name: "IntelliJ IDEA" })); + expect(screen.queryByLabelText("Eclipse formatter XML")).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Reset to Language Defaults" })); + expect(value()).toEqual({}); + }); + + it("keeps the profile controls specific to Java", async () => { + await page("rust"); + expect(screen.queryByRole("button", { name: "Formatting profile" })).toBeNull(); + }); +}); diff --git a/src/features/settings/pages/editor/codeStyle.tsx b/src/features/settings/pages/editor/codeStyle.tsx index 3138f68e1..43402d6d2 100644 --- a/src/features/settings/pages/editor/codeStyle.tsx +++ b/src/features/settings/pages/editor/codeStyle.tsx @@ -12,7 +12,7 @@ import { Button, Checkbox, Input, type SelectOption, T } from "@/ui"; import { commands, isTauri, type LanguageInfo_Serialize as LanguageInfo } from "@/platform/ipc"; import { ENCODING_CHOICES, lineCommentDefaults, type LanguageCodeStyle } from "@/platform/editorSettings"; import { NATIVE_TO_ASCII_ACTION } from "../../nativeToAscii"; -import { NumberInput, Row } from "../../components/Form"; +import { NumberInput, Row, Select } from "../../components/Form"; import { useSetting } from "../../components/context"; import type { PageSpec } from "../spec"; import { CODE_STYLE_SCOPE, CodeStyleMain, SchemeRow, formatterLabels } from "./codeStyleScheme"; @@ -159,6 +159,7 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) { return (
+ {lang.id === "java" && }
Tabs and Indents @@ -238,6 +239,62 @@ function LanguageStyle({ lang }: { lang: LanguageInfo }) { ); } +const JAVA_FORMATTERS: SelectOption>[] = [ + { value: "default", label: "Default" }, + { value: "intellij", label: "IntelliJ IDEA" }, + { value: "custom", label: "Custom Eclipse formatter XML" }, +]; + +function JavaFormatter({ + style, + onChange, +}: { + style: LanguageCodeStyle; + onChange: (patch: Partial) => void; +}) { + const id = useId(); + const browse = async () => { + const { open } = await import("@tauri-apps/plugin-dialog"); + const path = await open({ + multiple: false, + title: tx("Eclipse formatter XML"), + filters: [{ name: "XML", extensions: ["xml"] }], + }); + if (typeof path === "string") onChange({ formatterPath: path }); + }; + return ( +
+ + onChange({ formatterPath: e.target.value })} + /> + + + )} +
+ ); +} + const SKIP = new Set(["plaintext", "log", "diff", "ignore", "gitattributes"]); /** One Code Style page per language (registered once the language list is known). */ @@ -261,6 +318,9 @@ export async function codeStyleLanguagePages(): Promise { "Hard wrap at", "Comment Code", "Line comment at first column", + ...(lang.id === "java" + ? ["Formatting profile", "IntelliJ IDEA", "Custom Eclipse formatter XML", "Eclipse formatter XML"] + : []), ], component: () => , })); diff --git a/src/platform/editorSettings.ts b/src/platform/editorSettings.ts index 6471898dd..79b37e144 100644 --- a/src/platform/editorSettings.ts +++ b/src/platform/editorSettings.ts @@ -24,6 +24,9 @@ export interface CustomColorScheme { export type CustomColorSchemes = Record; export interface LanguageCodeStyle { + /** Java formatter; relative XML paths start at the project root. */ + formatter?: "default" | "intellij" | "custom"; + formatterPath?: string; useTab?: boolean; tabSize?: number; indent?: number; diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 47c0c4be9..14f5f86b6 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -5440,5 +5440,15 @@ "copy paths must belong to the comparison roots": "复制路径必须位于比较的根目录内", "cannot copy overlapping paths": "无法在相互包含的路径之间复制", "cannot copy through a destination symlink": "无法通过目标目录内的符号链接复制", - "expected a relative comparison entry path": "需要比较条目的相对路径" + "expected a relative comparison entry path": "需要比较条目的相对路径", + "IntelliJ IDEA": "IntelliJ IDEA", + "Formatting profile": "格式化方案", + "Custom Eclipse formatter XML": "自定义 Eclipse 格式化 XML", + "Eclipse formatter XML": "Eclipse 格式化 XML", + "Changes take effect after Apply without restarting the Java language server.": "应用后即生效,无需重启 Java 语言服务器。", + "Absolute or project-relative path. To reload an edited XML file, switch to Default, apply, then select Custom again.": "使用绝对路径或项目相对路径。修改 XML 文件后,请切换到默认方案并应用,再重新选择自定义方案。", + "Cannot read Java formatter XML {path}: {error}": "无法读取 Java 格式化 XML {path}:{error}", + "Java formatter configuration unavailable": "Java 格式化配置不可用", + "Choose an Eclipse formatter XML file.": "请选择 Eclipse 格式化 XML 文件。", + "Not a formatter XML file: {path}": "不是格式化 XML 文件:{path}" }