diff --git a/crates/ide-lsp/src/jdtls/intellij.xml b/crates/ide-lsp/src/jdtls/intellij.xml
index 5dba06392..18b3cf8de 100644
--- a/crates/ide-lsp/src/jdtls/intellij.xml
+++ b/crates/ide-lsp/src/jdtls/intellij.xml
@@ -10,8 +10,12 @@
https://www.jetbrains.com/help/idea/code-style-java.html describes the corresponding controls.
Each option below names its IDEA setting; JDT has no exact equivalent for every rule.
A formatter profile does not sort/group imports (Organize Imports is a separate action).
- JDT limitation: NO_SPLIT can join existing parameter/call/chain line breaks even with
- join_wrapped_lines=false. Column alignment applies where a break survives (e.g. comments).
+ Preserve IDEA KEEP_LINE_BREAKS=true using Wrap where necessary (WRAP_COMPACT) and
+ join_wrapped_lines=false (Never join already wrapped lines). JDT NO_SPLIT joins some
+ manual parameter/call/chain breaks even when joining is disabled. Preserving manual
+ breaks takes priority: wrappable code exceeding 120 columns will also wrap, a known
+ difference from IDEA defaults (DO_NOT_WRAP / WRAP_LONG_LINES=false).
+ Alignment values: 16 = WRAP_COMPACT + INDENT_DEFAULT; 18 = WRAP_COMPACT + INDENT_ON_COLUMN.
The comment width 999999 approximates WRAP_COMMENTS=false without JDT integer overflow.
JDT has a shared blank-line preservation limit and different empty-body/Javadoc handling.
-->
@@ -27,58 +31,58 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/crates/ide-lsp/tests/fixtures/FormatterSample.input.java b/crates/ide-lsp/tests/fixtures/FormatterSample.input.java
new file mode 100644
index 000000000..6c7121a84
--- /dev/null
+++ b/crates/ide-lsp/tests/fixtures/FormatterSample.input.java
@@ -0,0 +1,25 @@
+package demo;
+import java.util.List;
+import java.util.function.Function;
+@Deprecated @SuppressWarnings("unused") public class FormatterSample {
+@Deprecated @SuppressWarnings("unused") private String first="first";
+private String second="second";
+/** A description which remains on a very long single line to check whether formatting introduces wrapping when the right margin is exceeded with ordinary words in a JavaDoc comment. */
+@Deprecated @SuppressWarnings("unused") public String combine(@Deprecated @SuppressWarnings("unused") String firstParameterWithAVeryLongName, String secondParameterWithAVeryLongName, String thirdParameterWithAVeryLongName){if(firstParameterWithAVeryLongName==null){return "none";}else{try{return List.of(firstParameterWithAVeryLongName,secondParameterWithAVeryLongName,thirdParameterWithAVeryLongName).stream().map(value->value.trim()).filter(value->!value.isEmpty()).findFirst().orElse("none");}catch(RuntimeException problem){throw problem;}finally{System.out.println("done");}}}
+public void multiline(String firstParameter,
+String secondParameter,
+String thirdParameter){
+combine(firstParameter,
+secondParameter,
+thirdParameter);
+List.of(firstParameter, secondParameter)
+.stream()
+.map(value->value.trim())
+.forEach(System.out::println);
+}
+public void retained(String firstParameter, // retain declaration break
+String secondParameter){combine(firstParameter, // retain call break
+secondParameter, "third");}
+public void arrays(){int[] values=new int[]{1,2,3};int[] empty=new int[]{};Function clean=value->{return value.trim();};Runnable run=()->{System.out.println(values.length);};}
+// A comment which intentionally exceeds the right margin and should remain unwrapped with the built-in profile even when it contains many ordinary words and spaces to split on.
+}
diff --git a/crates/ide-lsp/tests/fixtures/FormatterSample.intellij.java b/crates/ide-lsp/tests/fixtures/FormatterSample.intellij.java
new file mode 100644
index 000000000..f1211e24c
--- /dev/null
+++ b/crates/ide-lsp/tests/fixtures/FormatterSample.intellij.java
@@ -0,0 +1,65 @@
+package demo;
+
+import java.util.List;
+import java.util.function.Function;
+
+@Deprecated
+@SuppressWarnings("unused")
+public class FormatterSample {
+ @Deprecated
+ @SuppressWarnings("unused")
+ private String first = "first";
+ private String second = "second";
+
+ /** A description which remains on a very long single line to check whether formatting introduces wrapping when the right margin is exceeded with ordinary words in a JavaDoc comment. */
+ @Deprecated
+ @SuppressWarnings("unused")
+ public String combine(@Deprecated @SuppressWarnings("unused") String firstParameterWithAVeryLongName,
+ String secondParameterWithAVeryLongName, String thirdParameterWithAVeryLongName) {
+ if (firstParameterWithAVeryLongName == null) {
+ return "none";
+ } else {
+ try {
+ return List
+ .of(firstParameterWithAVeryLongName, secondParameterWithAVeryLongName,
+ thirdParameterWithAVeryLongName)
+ .stream().map(value -> value.trim()).filter(value -> !value.isEmpty()).findFirst()
+ .orElse("none");
+ } catch (RuntimeException problem) {
+ throw problem;
+ } finally {
+ System.out.println("done");
+ }
+ }
+ }
+
+ public void multiline(String firstParameter,
+ String secondParameter,
+ String thirdParameter) {
+ combine(firstParameter,
+ secondParameter,
+ thirdParameter);
+ List.of(firstParameter, secondParameter)
+ .stream()
+ .map(value -> value.trim())
+ .forEach(System.out::println);
+ }
+
+ public void retained(String firstParameter, // retain declaration break
+ String secondParameter) {
+ combine(firstParameter, // retain call break
+ secondParameter, "third");
+ }
+
+ public void arrays() {
+ int[] values = new int[]{1, 2, 3};
+ int[] empty = new int[]{};
+ Function clean = value -> {
+ return value.trim();
+ };
+ Runnable run = () -> {
+ System.out.println(values.length);
+ };
+ }
+ // A comment which intentionally exceeds the right margin and should remain unwrapped with the built-in profile even when it contains many ordinary words and spaces to split on.
+}
diff --git a/crates/ide-lsp/tests/real_servers.rs b/crates/ide-lsp/tests/real_servers.rs
index 43c250a20..f25f77745 100644
--- a/crates/ide-lsp/tests/real_servers.rs
+++ b/crates/ide-lsp/tests/real_servers.rs
@@ -62,6 +62,53 @@ fn lang(id: &str, ext: &str, server: LanguageServerConfig) -> LanguageConfig {
}
}
+#[tokio::test(flavor = "multi_thread")]
+#[ignore = "requires jdtls on PATH and a compatible JDK"]
+async fn intellij_java_formatter_preserves_manual_breaks_and_wraps_long_lines() {
+ let dir = tempfile::tempdir().unwrap();
+ let root = dir.path().canonicalize().unwrap();
+ let source = include_str!("fixtures/FormatterSample.input.java");
+ let expected = include_str!("fixtures/FormatterSample.intellij.java");
+ let path = root.join("src/demo/FormatterSample.java");
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(&path, source).unwrap();
+ let mut java = LanguageRegistry::load(None).unwrap().get(&LanguageId::new("java")).unwrap();
+ let server = &mut java.language_servers[0];
+ if let Ok(bin) = std::env::var("OPENIDEA_TEST_JDTLS") {
+ server.command = bin;
+ }
+ server.args.extend(["-data".into(), root.join("jdtls-workspace").to_string_lossy().into_owned()]);
+ server.settings = Some(serde_json::json!({"java": {"autobuild": {"enabled": false}}}));
+ let (mgr, events) = manager(&root, LanguageRegistry::from_configs(vec![java]));
+ mgr.set_config_hook(Box::new({
+ let root = root.clone();
+ move |cfg, _| {
+ ide_lsp::jdtls::configure_source_paths(cfg, Some(vec!["src".into()]));
+ let style = ide_core::editor_settings::LanguageCodeStyle {
+ formatter: Some("intellij".into()),
+ ..Default::default()
+ };
+ ide_lsp::jdtls::configure_formatter(cfg, Some(&style), &root, &root.join("data"), None).unwrap();
+ }
+ }));
+ let doc = DocId::next();
+ mgr.did_open(doc, &path, &LanguageId::new("java"), 1, source);
+ retry("jdtls ready", Duration::from_secs(120), || {
+ let status = last_status(&events, "jdtls");
+ if let Some((ServerStatus::Crashed | ServerStatus::Missing | ServerStatus::Misconfigured, msg)) = &status {
+ panic!("jdtls failed: {msg:?}");
+ }
+ async move { matches!(status, Some((ServerStatus::Ready | ServerStatus::Busy, _))).then_some(()) }
+ })
+ .await;
+ let edits =
+ mgr.format(doc, None, ide_lsp::types::FormatOptions { tab_size: 4, insert_spaces: true, on_save: false }).await;
+ tokio::time::timeout(Duration::from_secs(10), mgr.shutdown_all()).await.unwrap();
+ let mut document = ide_text::Document::new(source);
+ document.apply_edits(&edits.unwrap()).unwrap();
+ assert_eq!(document.text(), expected);
+}
+
#[tokio::test(flavor = "multi_thread")]
async fn missing_binary_is_reported_with_install_hint() {
let dir = tempfile::tempdir().unwrap();