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();