From 7c0308e66ac594d35f46ad1d3470d16673b8778f Mon Sep 17 00:00:00 2001 From: puppy_1 <13323021675@163.com> Date: Fri, 23 Oct 2026 11:50:30 +0800 Subject: [PATCH 1/4] fix(lsp): leave Java completion arguments empty (#1437) --- ...a-language-server-as-declarative-plugin.md | 8 +++ rust/lithe-core/src/lsp/languages/jdt.rs | 65 ++++++++++++++++++- shared/contracts/rust-core-api.md | 8 +++ shared/fixtures/lsp/java-completion-v1.json | 13 ++++ .../features/lsp-completion-hover.json | 6 +- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 shared/fixtures/lsp/java-completion-v1.json diff --git a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md index 99beece36e..4344997716 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md +++ b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md @@ -126,6 +126,14 @@ dev.lithe.plugin.java-support/ - 不要为了恢复某个语言的调试或测试,把能力目录的合并规则改成“插件没声明就保留宿主原有能力”,也不要在目录或注册表里按 `java` 放行。前者让 PHP、Go 的插件停用后宿主重新运行内置工具,后者是按名字的特判。 - 不要往插件包里写任何东西。可写状态用 `${stateDir}`、`${workspaceStateDir}`,或像 JDT 那样由集成放进宿主缓存。 +### Java 调用补全留空实参 + +构造器和方法补全由共享 JDT 适配器设置 `java.completion.guessMethodArguments: "off"`,Windows 和 macOS 一致。输入 `new No` 后接受 Node 候选得到 `Node()`,括号内保留光标,由用户填写实参;方法候选仍可展示签名,自动导入和其他语言的 snippet 不受影响。初始化、配置通知及按 section 拉取配置必须返回相同值,防止 JDT LS 后续恢复参数占位。 + +来源是当前固定的 [JDT LS v1.61.0](https://github.com/eclipse-jdtls/eclipse.jdt.ls/tree/v1.61.0):`Preferences.java` 默认选择 `INSERT_PARAMETER_NAMES`,`CompletionProposalReplacementProvider.appendGuessingCompletion` 在 `OFF` 时只插入光标。正确做法是使用该上游设置,同时保留插件声明的其他 completion 项;不要在 Monaco 中用正则删除参数或禁用通用 snippet。后两种方案会误伤方法体模板、其他语言和补全解析后的自动导入。代价是 Java 默认不再提供可逐个替换的参数名占位,JDT LS 也会将构造器重载候选合并为类型候选;这次不增加新的设置界面。 + +`java_completion_leaves_arguments_empty_across_configuration_channels` 和 `shared/fixtures/lsp/java-completion-v1.json` 验证三个配置入口及插件其他 completion 项的保留;实机验收仍需分别确认两端 Tab 接受候选后的位置和自动导入。 + ## 考虑过的备选方案 ### 只把文件搬进插件包,宿主继续扫描目录 diff --git a/rust/lithe-core/src/lsp/languages/jdt.rs b/rust/lithe-core/src/lsp/languages/jdt.rs index 0126f7382b..9df0ef1049 100644 --- a/rust/lithe-core/src/lsp/languages/jdt.rs +++ b/rust/lithe-core/src/lsp/languages/jdt.rs @@ -443,7 +443,7 @@ pub(crate) fn adapt_initialization_options( /// initialization options. /// /// Catalog keys that the provider does not own are preserved, while the Maven, -/// build, and code-lens values Lithe depends on stay authoritative. +/// build, completion, and code-lens values Lithe depends on stay authoritative. fn merge_java_settings(options: &mut Map, provider_settings: Value) { let settings = options.entry("settings").or_insert_with(|| json!({})); if !settings.is_object() { @@ -467,6 +467,14 @@ fn merge_java_settings(options: &mut Map, provider_settings: Valu return; }; for (key, value) in provider_java { + if key == "completion" { + let completion = java.entry(key).or_insert_with(|| json!({})); + if !completion.is_object() { + *completion = json!({}); + } + completion["guessMethodArguments"] = value["guessMethodArguments"].clone(); + continue; + } // `java.import.gradle` is the only `import` leaf Lithe owns. The options // Lithe deliberately never guesses (`java.home`, `version`, // `offline.enabled`, ...) have to survive, so the owned leaves merge @@ -1074,6 +1082,11 @@ fn java_settings(settings: JdtSettings<'_>) -> Value { "configuration": { "updateBuildConfiguration": "automatic" }, + // JDT LS otherwise inserts parameter names into calls. Its "off" + // mode keeps the argument list empty and the snippet cursor inside. + "completion": { + "guessMethodArguments": "off" + }, "inlayHints": { "parameterNames": { "enabled": "all" @@ -1163,6 +1176,8 @@ fn java_configuration_for_section(section: Option<&str>, settings: JdtSettings<' let gradle_configuration = settings.gradle; match section { Some("java") => java_settings(settings)["java"].clone(), + Some("java.completion") => java_settings(settings)["java"]["completion"].clone(), + Some("java.completion.guessMethodArguments") => json!("off"), Some("java.inlayHints") => json!({ "parameterNames": { "enabled": "all" @@ -1968,6 +1983,54 @@ mod tests { assert_eq!(options["workspace"]["custom"], true); } + #[test] + fn java_completion_leaves_arguments_empty_across_configuration_channels() { + // Initialization, notifications and pull configuration must agree, or + // JDT LS can restore parameter placeholders after the first completion. + let fixture: Value = serde_json::from_str(include_str!( + "../../../../../shared/fixtures/lsp/java-completion-v1.json" + )) + .expect("Java completion fixture should be valid"); + let expected = &fixture["javaSettings"]["completion"]; + let options = adapt_initialization_options( + "java", + Some(json!({ "settings": { "java": { "completion": { + "guessMethodArguments": "insertParameterNames", + "favoriteStaticMembers": ["java.util.Objects.*"] + } } } })), + &[], + JdtSettings::default(), + ) + .unwrap(); + assert_eq!( + options["settings"]["java"]["completion"]["guessMethodArguments"], + expected["guessMethodArguments"] + ); + assert_eq!( + options["settings"]["java"]["completion"]["favoriteStaticMembers"], + json!(["java.util.Objects.*"]) + ); + + let notification = initialized_notification("java", JdtSettings::default()).unwrap(); + assert_eq!( + notification.params["settings"]["java"]["completion"], + *expected + ); + let items = fixture["workspaceConfigurationSections"] + .as_array() + .unwrap() + .iter() + .map(|section| WorkspaceConfigurationItem { + scope_uri: None, + section: Some(section.as_str().unwrap().to_string()), + }) + .collect::>(); + let values = workspace_configuration("java", &items, JdtSettings::default()).unwrap(); + assert_eq!(values[0]["completion"], *expected); + assert_eq!(values[1], *expected); + assert_eq!(values[2], expected["guessMethodArguments"]); + } + #[test] fn java_initialization_options_carry_settings_without_a_maven_context() { let options = diff --git a/shared/contracts/rust-core-api.md b/shared/contracts/rust-core-api.md index c01db8ed90..83f92b0c27 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -3158,6 +3158,14 @@ than being inserted as literal source text. The initialize handshake advertises incomplete items. Core forwards `labelDetails` and omits null `data` so `completionItem/resolve` can still produce import `use`/`import` edits. +The shared JDT adapter sends `java.completion.guessMethodArguments: "off"` +through initialization, configuration notifications and section queries on both +platforms. JDT LS therefore leaves constructor and method argument lists empty +while preserving the snippet cursor inside the parentheses. Other completion +settings declared by the provider are retained. This does not disable snippet +support or strip parameter text from arbitrary completion items; the shared +configuration fixture is `shared/fixtures/lsp/java-completion-v1.json`. + ### Java preparation snapshot Java `projectPreparation` runtime events carry a `result` object with `phase` diff --git a/shared/fixtures/lsp/java-completion-v1.json b/shared/fixtures/lsp/java-completion-v1.json new file mode 100644 index 0000000000..7f8ecd3a4f --- /dev/null +++ b/shared/fixtures/lsp/java-completion-v1.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "javaSettings": { + "completion": { + "guessMethodArguments": "off" + } + }, + "workspaceConfigurationSections": [ + "java", + "java.completion", + "java.completion.guessMethodArguments" + ] +} diff --git a/shared/platform-feature-matrix/features/lsp-completion-hover.json b/shared/platform-feature-matrix/features/lsp-completion-hover.json index e5ed25cea1..4c04bd9971 100644 --- a/shared/platform-feature-matrix/features/lsp-completion-hover.json +++ b/shared/platform-feature-matrix/features/lsp-completion-hover.json @@ -6,6 +6,8 @@ "macos": { "evidence": [ "macos/Sources/Lithe/Core/Language", + "rust/lithe-core/src/lsp/languages/jdt.rs", + "shared/fixtures/lsp/java-completion-v1.json", "shared/fixtures/lsp" ], "implementationStatus": "implemented", @@ -14,11 +16,13 @@ "windows": { "evidence": [ "windows/tauri/src/features", + "rust/lithe-core/src/lsp/languages/jdt.rs", + "shared/fixtures/lsp/java-completion-v1.json", "shared/fixtures/lsp" ], "implementationStatus": "implemented", "verificationStatus": "pending" }, "owner": "Language Tooling", - "verification": "在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。" + "verification": "在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。Java 构造器和方法补全使用 JDT LS 的 guessMethodArguments=off:输入 new No 后接受 Node 候选应得到 Node(),光标在括号内;方法调用同样不插入参数名,构造器重载候选由 JDT LS 合并为类型候选。验证 initialize、didChangeConfiguration 与 workspace/configuration 三个配置入口一致,其他语言 snippet、Java 方法补全签名展示及自动导入仍正常。" } From 3f418d7d0561d2826d18cac7fe4066cb58526460 Mon Sep 17 00:00:00 2001 From: puppy_1 <13323021675@163.com> Date: Fri, 23 Oct 2026 16:24:41 +0800 Subject: [PATCH 2/4] test(windows): await process exit signals and own fixture cleanup --- ...-10-14-child-processes-end-with-the-ide.md | 1 + windows/tauri/src-tauri/src/run.rs | 162 ++++++++++++------ 2 files changed, 107 insertions(+), 56 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md b/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md index ca2fcc3054..a8f5967e8c 100644 --- a/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md +++ b/.agents/notes/implemented/bug-fix/2026-10-14-child-processes-end-with-the-ide.md @@ -100,6 +100,7 @@ Windows 原来有五个漏洞,叠在一起导致“关闭 Lithe 后服务还 - `./scripts/test-macos.sh --filter '(MacProcessRunnerTests|ExecutionModule)'`。 - `rust/lithe-git-host/tests/process.rs`:`process_tree_terminate_ends_the_root_and_its_descendants` 启动一个会再启动后代的进程,确认 `terminate` 返回时两者都已结束;Windows 上 `dropping_a_process_tree_ends_it_like_a_crashed_host` 只丢弃句柄,确认内核结束整组。 - `windows/tauri/src-tauri/src/run.rs` 测试(仅 Windows):关闭一个窗口只结束它自己的进程树、丢弃会话等同 Lithe 崩溃、原生列表按启动顺序包含全部会话。 + 测试在停止前保留后代进程句柄,限时等待 Windows 退出信号,不以 sysinfo 中仍能查到 PID 判断存活;根进程先退出也不代表后代已完成退出。夹具用 Drop 清理本测试的 Job 与进程,断言失败同样执行。启动时仅为发现后代 PID 做最多 5 秒的进程快照轮询(10ms 间隔),不再无限等待 stdout;该原生边界是测试稳定性扫描的明确例外。 - `rust/lithe-core/src/lsp/interface/process.rs` 测试(仅 Windows):结束批处理启动的语言服务器时,脚本启动的子进程一起结束。 - `shared/fixtures/process-lifecycle/exit-plan-v1.json`、`settle-v1.json`:运行服务在退出、关闭项目、直接停止设置和崩溃恢复下的动作;macOS `ProcessLifecycleFixtureTests` 用 Swift 类型解码同一批 fixture,并覆盖提问顺序、取消和规则失败时仍询问。 - `windows/tauri/src/features/window/services/run-services-exit.test.ts`:询问、取消、停止失败后重试、直接停止、后台任务不询问、Core 不可用时询问。 diff --git a/windows/tauri/src-tauri/src/run.rs b/windows/tauri/src-tauri/src/run.rs index ca6a2dfe8e..35d5cdc482 100644 --- a/windows/tauri/src-tauri/src/run.rs +++ b/windows/tauri/src-tauri/src/run.rs @@ -3723,32 +3723,76 @@ mod tests { assert_eq!(left, run_session_key("project-a", "primary")); } - /// A Run session like `cmd -> mvn -> java`, owned the way - /// `start_reserved_process` owns one: the suspended root joins its tree - /// before ping can start. + /// Owns both the registered Job and the root handle, including on assertion + /// failure. Descendant handles identify the exact process, without PID reuse + /// or sysinfo snapshots mistaking an exited process for a running one. + #[cfg(windows)] + struct ProcessTreeFixture { + child: Child, + window: &'static str, + descendant: Option, + } + + #[cfg(windows)] + fn process_wait(handle: &impl std::os::windows::io::AsRawHandle, timeout: Duration) -> u32 { + use windows_sys::Win32::System::Threading::WaitForSingleObject; + // The borrowed handle stays owned by the fixture throughout this wait. + unsafe { WaitForSingleObject(handle.as_raw_handle(), timeout.as_millis() as u32) } + } + + #[cfg(windows)] + impl ProcessTreeFixture { + fn assert_exited(&mut self) { + use windows_sys::Win32::Foundation::WAIT_OBJECT_0; + assert_eq!( + process_wait(&self.child, RUN_STOP_TIMEOUT), + WAIT_OBJECT_0, + "root exits" + ); + assert!(self.child.try_wait().unwrap().is_some()); + assert_eq!( + process_wait( + self.descendant.as_ref().expect("descendant handle"), + RUN_STOP_TIMEOUT + ), + WAIT_OBJECT_0, + "descendant exits", + ); + } + } + + #[cfg(windows)] + impl Drop for ProcessTreeFixture { + fn drop(&mut self) { + // Closing the Job also handles panics before the normal stop call. + drop(take_owned_sessions(|key| key.window_label == self.window)); + let _ = self.child.kill(); + let _ = process_wait(&self.child, RUN_STOP_TIMEOUT); + let _ = self.child.try_wait(); + if let Some(descendant) = &self.descendant { + let _ = process_wait(descendant, RUN_STOP_TIMEOUT); + } + } + } + + /// Starts the same suspended-root ownership sequence as a Run service. + /// Child discovery is bounded; no output reader or blocking pipe can outlive + /// the fixture. The descendant handle is retained before any stop operation. #[cfg(windows)] fn spawn_owned_process_tree( - window: &str, + window: &'static str, session: &str, title: &str, - ) -> (Child, std::process::ChildStdout, u32) { + ) -> ProcessTreeFixture { + use std::os::windows::io::{FromRawHandle, OwnedHandle}; + use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_SYNCHRONIZE}; let mut command = Command::new("cmd"); command .args(["/C", "ping -n 30 127.0.0.1"]) - .stdout(Stdio::piped()); + .stdout(Stdio::null()); ProcessTree::prepare(&mut command); let mut child = command.spawn().expect("spawn service"); let tree = ProcessTree::adopt(&mut child).expect("own service tree"); - // cmd writes nothing itself; the first output byte comes from ping, so - // the descendant is running once it arrives. The pipe stays open for - // the test, otherwise ping would exit on its next write. - let mut stdout = child.stdout.take().expect("service output"); - stdout - .read_exact(&mut [0; 1]) - .expect("service descendant output"); - let descendant = *child_process_ids(child.id()) - .first() - .expect("service descendant"); sessions().lock().unwrap().insert( run_session_key(window, session), RunningSession { @@ -3761,26 +3805,36 @@ mod tests { tree, }, ); - (child, stdout, descendant) - } - - #[cfg(windows)] - fn child_process_ids(parent: u32) -> Vec { - let mut system = sysinfo::System::new(); - system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); - system - .processes() - .values() - .filter(|process| process.parent() == Some(sysinfo::Pid::from_u32(parent))) - .map(|process| process.pid().as_u32()) - .collect() - } - - #[cfg(windows)] - fn process_is_alive(pid: u32) -> bool { - let mut system = sysinfo::System::new(); - system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); - system.process(sysinfo::Pid::from_u32(pid)).is_some() + let mut fixture = ProcessTreeFixture { + child, + window, + descendant: None, + }; + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let mut system = sysinfo::System::new(); + system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + if let Some(process) = system.processes().values().find(|process| { + process.parent() == Some(sysinfo::Pid::from_u32(fixture.child.id())) + }) { + // Open before terminating the Job; querying a PID afterward can + // observe a retained process object or an unrelated reused PID. + let handle = unsafe { OpenProcess(PROCESS_SYNCHRONIZE, 0, process.pid().as_u32()) }; + assert!( + !handle.is_null(), + "open descendant: {}", + std::io::Error::last_os_error() + ); + fixture.descendant = Some(unsafe { OwnedHandle::from_raw_handle(handle) }); + return fixture; + } + assert!( + Instant::now() < deadline, + "service descendant did not start within 5 seconds" + ); + // test-stability: allow(rust-real-sleep) reason: Windows process snapshots expose no child-start event; bounded polling only discovers the fixture PID, exit assertions use kernel signals. + thread::sleep(Duration::from_millis(10)); + } } #[cfg(windows)] @@ -3788,20 +3842,21 @@ mod tests { fn window_release_ends_its_process_trees_and_keeps_other_windows() { // Regression for #1137: closing a window left Run services and their // descendants running. Another window's service must survive the close. - let (mut closed, _closed_output, closed_descendant) = - spawn_owned_process_tree("release-closed-window", "service", "Closed"); - let (mut kept, _kept_output, kept_descendant) = - spawn_owned_process_tree("release-kept-window", "service", "Kept"); + let mut closed = spawn_owned_process_tree("release-closed-window", "service", "Closed"); + let mut kept = spawn_owned_process_tree("release-kept-window", "service", "Kept"); stop_sessions( take_owned_sessions(|key| key.window_label == "release-closed-window"), RUN_STOP_TIMEOUT, ) .expect("the closed window's trees end"); - assert!(closed.wait().is_ok()); - assert!(!process_is_alive(closed_descendant)); - assert!(kept.try_wait().unwrap().is_none()); - assert!(process_is_alive(kept_descendant)); + closed.assert_exited(); + assert!(kept.child.try_wait().unwrap().is_none()); + assert_eq!( + process_wait(kept.descendant.as_ref().unwrap(), Duration::ZERO), + windows_sys::Win32::Foundation::WAIT_TIMEOUT, + "the other window's descendant remains running", + ); // A second destroy/exit pass finds nothing left and must not reap again. assert!(take_owned_sessions(|key| key.window_label == "release-closed-window").is_empty()); @@ -3811,8 +3866,7 @@ mod tests { RUN_STOP_TIMEOUT, ) .expect("the kept window's trees end"); - assert!(kept.wait().is_ok()); - assert!(!process_is_alive(kept_descendant)); + kept.assert_exited(); } #[cfg(windows)] @@ -3820,13 +3874,11 @@ mod tests { fn dropping_a_session_ends_its_tree_like_a_crashed_lithe() { // When Lithe dies, Windows closes its job handles. Dropping the session // closes the handle the same way, so no stop call is involved. - let (mut root, _output, descendant) = - spawn_owned_process_tree("crash-window", "service", "Crash"); + let mut root = spawn_owned_process_tree("crash-window", "service", "Crash"); let owned = take_owned_sessions(|key| key.window_label == "crash-window"); assert_eq!(owned.len(), 1); drop(owned); - assert!(root.wait().is_ok()); - assert!(!process_is_alive(descendant)); + root.assert_exited(); } #[cfg(windows)] @@ -3835,11 +3887,9 @@ mod tests { // Maven goals and services of closed projects have no webview store, // so the close confirmation must come from the native table. let window = "listing-window"; - let (mut first, _first_output, _) = - spawn_owned_process_tree(window, "maven", "clean install"); - let (mut second, _second_output, _) = spawn_owned_process_tree(window, "service", "Api"); - let (mut other, _other_output, _) = - spawn_owned_process_tree("listing-other-window", "service", "Other"); + let mut first = spawn_owned_process_tree(window, "maven", "clean install"); + let mut second = spawn_owned_process_tree(window, "service", "Api"); + let mut other = spawn_owned_process_tree("listing-other-window", "service", "Other"); let preparing = { let mut pending = pending_launches().lock().unwrap(); let reservation = reserve_test_launch(&mut pending, window, "prepare"); @@ -3879,7 +3929,7 @@ mod tests { .expect("listing fixtures end"); } for root in [&mut first, &mut second, &mut other] { - assert!(root.wait().is_ok()); + root.assert_exited(); } } From af1366160f3d38dfd8ec2c6c06e26ba0215b9e6e Mon Sep 17 00:00:00 2001 From: puppy_1 <13323021675@163.com> Date: Fri, 23 Oct 2026 16:37:14 +0800 Subject: [PATCH 3/4] fix(java): retain plugin completion settings across configuration channels --- ...a-language-server-as-declarative-plugin.md | 1 + rust/lithe-core/src/lsp/interface/engine.rs | 61 ++++++++++++++++++- rust/lithe-core/src/lsp/languages/jdt.rs | 60 ++++++++++++++---- shared/contracts/rust-core-api.md | 4 +- shared/fixtures/lsp/java-completion-v1.json | 14 ++++- .../features/lsp-completion-hover.json | 4 +- 6 files changed, 127 insertions(+), 17 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md index 4344997716..657e05ee2b 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md +++ b/.agents/notes/implemented/architecture/2026-10-13-java-language-server-as-declarative-plugin.md @@ -133,6 +133,7 @@ dev.lithe.plugin.java-support/ 来源是当前固定的 [JDT LS v1.61.0](https://github.com/eclipse-jdtls/eclipse.jdt.ls/tree/v1.61.0):`Preferences.java` 默认选择 `INSERT_PARAMETER_NAMES`,`CompletionProposalReplacementProvider.appendGuessingCompletion` 在 `OFF` 时只插入光标。正确做法是使用该上游设置,同时保留插件声明的其他 completion 项;不要在 Monaco 中用正则删除参数或禁用通用 snippet。后两种方案会误伤方法体模板、其他语言和补全解析后的自动导入。代价是 Java 默认不再提供可逐个替换的参数名占位,JDT LS 也会将构造器重载候选合并为类型候选;这次不增加新的设置界面。 `java_completion_leaves_arguments_empty_across_configuration_channels` 和 `shared/fixtures/lsp/java-completion-v1.json` 验证三个配置入口及插件其他 completion 项的保留;实机验收仍需分别确认两端 Tab 接受候选后的位置和自动导入。 +运行会话保留初始化时插件声明的 completion 对象,配置通知和 `java`、`java.completion` 及其叶节点请求均从同一份对象应用 `guessMethodArguments=off`。例如插件声明的 `favoriteStaticMembers`、`filteredTypes` 不得在初始化后被通知或拉取结果清空;引擎测试同时检查真实会话接线,不能只用默认设置证明三个入口一致。 ## 考虑过的备选方案 diff --git a/rust/lithe-core/src/lsp/interface/engine.rs b/rust/lithe-core/src/lsp/interface/engine.rs index 32eb50caa4..ab73aeaacb 100644 --- a/rust/lithe-core/src/lsp/interface/engine.rs +++ b/rust/lithe-core/src/lsp/interface/engine.rs @@ -680,6 +680,9 @@ struct RuntimeSession { plugin_configuration: Mutex>>, /// JDKs JDT LS may bind projects to, one per execution environment. jdt_java_runtimes: Vec, + /// Plugin completion options fixed at initialize, retained for later Java + /// configuration notifications and section pulls through the same adapter. + jdt_completion: Option, /// Gradle import settings JDT LS holds. Fixed for the session's lifetime /// because the wrapper is a property of the workspace root at startup. jdt_gradle_configuration: Option, @@ -925,6 +928,7 @@ impl RuntimeSession { /// Java settings Lithe owns for this session, as sent to JDT LS. fn jdt_settings<'a>(&'a self, maven: Option<&'a JdtMavenConfiguration>) -> JdtSettings<'a> { JdtSettings { + completion: self.jdt_completion.as_ref(), maven, gradle: self.jdt_gradle_configuration.as_ref(), java_runtimes: &self.jdt_java_runtimes, @@ -1505,6 +1509,12 @@ impl LspEngine { let java_build_timeout = Duration::from_millis(request.java_build_timeout_milliseconds); let shutdown_timeout = Duration::from_millis(request.shutdown_timeout_milliseconds); let jdt_java_runtimes = jdt_java_runtimes(&request.java_runtimes); + let jdt_completion = request + .initialization_options + .as_ref() + .and_then(|options| options.pointer("/settings/java/completion")) + .filter(|completion| completion.is_object()) + .cloned(); let workspace_configuration = checked_workspace_configuration(request.workspace_configuration)?; let initialize = client_initialize_for_plugin( @@ -1517,6 +1527,7 @@ impl LspEngine { request.initialization_options, &java_extension_bundle_paths, JdtSettings { + completion: jdt_completion.as_ref(), maven: jdt_maven_configuration.as_deref(), gradle: request.java_gradle.as_ref(), java_runtimes: &jdt_java_runtimes, @@ -1548,6 +1559,7 @@ impl LspEngine { maven_update_order: Mutex::new(()), plugin_configuration: Mutex::new(workspace_configuration.map(Arc::new)), jdt_java_runtimes, + jdt_completion, jdt_gradle_configuration: request.java_gradle, root_uri: request.root_uri, outbound_order: Mutex::new(()), @@ -8478,6 +8490,10 @@ mod tests { #[test] fn java_start_enables_and_normalizes_class_file_navigation() { + let completion_fixture: Value = serde_json::from_str(include_str!( + "../../../../../shared/fixtures/lsp/java-completion-v1.json" + )) + .unwrap(); let root = std::env::temp_dir().join(format!( "lithe-core-java-navigation-tests-{}", std::process::id() @@ -8500,7 +8516,8 @@ mod tests { }); request.initialization_options = Some(json!({ "extendedClientCapabilities": { "customCapability": true }, - "bundles": ["/plugins/catalog.jar"] + "bundles": ["/plugins/catalog.jar"], + "settings": { "java": { "completion": completion_fixture["pluginCompletion"] } } })); }); let initialize = harness @@ -8532,6 +8549,48 @@ mod tests { .server .complete_java_initialize(json!({ "definitionProvider": true })); harness.await_state(LspLifecycleState::Ready); + let expected_completion = &completion_fixture["javaSettings"]["completion"]; + assert_eq!( + initialize["params"]["initializationOptions"]["settings"]["java"]["completion"], + *expected_completion, + ); + assert!(harness + .server + .await_notification("workspace/didChangeConfiguration")); + let notification = harness + .server + .messages() + .into_iter() + .find(|message| message["method"] == "workspace/didChangeConfiguration") + .unwrap(); + assert_eq!( + notification["params"]["settings"]["java"]["completion"], + *expected_completion + ); + harness.server.send(json!({ + "jsonrpc": "2.0", "id": "java-completion-config", "method": "workspace/configuration", + "params": { "items": completion_fixture["workspaceConfigurationSections"] + .as_array().unwrap().iter().map(|section| json!({ "section": section })).collect::>() } + })); + let configuration = await_reply(&harness, "java-completion-config"); + assert_eq!( + configuration["result"][0]["completion"], + *expected_completion + ); + assert_eq!(configuration["result"][1], *expected_completion); + assert_eq!( + configuration["result"][2], + expected_completion["guessMethodArguments"] + ); + assert_eq!( + configuration["result"][3], + expected_completion["favoriteStaticMembers"] + ); + assert_eq!( + configuration["result"][4], + expected_completion["filteredTypes"] + ); + assert_eq!(configuration["result"][5], Value::Null); let uri = "file:///workspace/Main.java"; harness.sync(uri, "class Main { String value; }"); let operation_id = harness.request(LspSemanticOperation::Definition, uri); diff --git a/rust/lithe-core/src/lsp/languages/jdt.rs b/rust/lithe-core/src/lsp/languages/jdt.rs index 9df0ef1049..a396d984fe 100644 --- a/rust/lithe-core/src/lsp/languages/jdt.rs +++ b/rust/lithe-core/src/lsp/languages/jdt.rs @@ -162,6 +162,9 @@ pub(crate) struct JdtJavaRuntime { /// Values Lithe owns in JDT LS's `settings.java`, borrowed from one session. #[derive(Debug, Clone, Copy, Default)] pub(crate) struct JdtSettings<'a> { + /// Plugin-declared completion options from this session's initialization; + /// all leaves survive except the provider-owned guessMethodArguments. + pub completion: Option<&'a Value>, pub maven: Option<&'a JdtMavenConfiguration>, /// Gradle import settings, present only for Gradle workspaces. pub gradle: Option<&'a JdtGradleConfiguration>, @@ -472,7 +475,12 @@ fn merge_java_settings(options: &mut Map, provider_settings: Valu if !completion.is_object() { *completion = json!({}); } - completion["guessMethodArguments"] = value["guessMethodArguments"].clone(); + completion.as_object_mut().unwrap().extend( + value + .as_object() + .expect("completion settings are an object") + .clone(), + ); continue; } // `java.import.gradle` is the only `import` leaf Lithe owns. The options @@ -1084,9 +1092,7 @@ fn java_settings(settings: JdtSettings<'_>) -> Value { }, // JDT LS otherwise inserts parameter names into calls. Its "off" // mode keeps the argument list empty and the snippet cursor inside. - "completion": { - "guessMethodArguments": "off" - }, + "completion": java_completion_settings(settings.completion), "inlayHints": { "parameterNames": { "enabled": "all" @@ -1117,6 +1123,17 @@ fn java_settings(settings: JdtSettings<'_>) -> Value { java } +/// Retains the plugin's completion leaves while applying Lithe's argument policy. +/// Invalid section shapes are normalized without mutating the session input. +fn java_completion_settings(declared: Option<&Value>) -> Value { + let mut completion = declared + .and_then(Value::as_object) + .cloned() + .unwrap_or_default(); + completion.insert("guessMethodArguments".to_string(), json!("off")); + Value::Object(completion) +} + fn java_runtimes_value(runtimes: &[JdtJavaRuntime]) -> Value { Value::Array( runtimes @@ -1177,7 +1194,14 @@ fn java_configuration_for_section(section: Option<&str>, settings: JdtSettings<' match section { Some("java") => java_settings(settings)["java"].clone(), Some("java.completion") => java_settings(settings)["java"]["completion"].clone(), - Some("java.completion.guessMethodArguments") => json!("off"), + Some(section) if section.starts_with("java.completion.") => { + let completion = java_completion_settings(settings.completion); + section["java.completion.".len()..] + .split('.') + .try_fold(&completion, |value, key| value.get(key)) + .cloned() + .unwrap_or(Value::Null) + } Some("java.inlayHints") => json!({ "parameterNames": { "enabled": "all" @@ -1439,6 +1463,7 @@ mod tests { maven: None, gradle: None, java_runtimes: &runtimes, + ..Default::default() }; let expected = json!([{ "name": "JavaSE-25", "path": "C:/Program Files/Java/jdk-25" }]); let options = adapt_initialization_options("java", None, &[], settings).unwrap(); @@ -1477,6 +1502,7 @@ mod tests { maven: Some(configuration), gradle: None, java_runtimes: &[], + ..Default::default() } } @@ -1485,6 +1511,7 @@ mod tests { maven: None, gradle: Some(configuration), java_runtimes: &[], + ..Default::default() } } @@ -1992,14 +2019,15 @@ mod tests { )) .expect("Java completion fixture should be valid"); let expected = &fixture["javaSettings"]["completion"]; + let settings = JdtSettings { + completion: Some(&fixture["pluginCompletion"]), + ..Default::default() + }; let options = adapt_initialization_options( "java", - Some(json!({ "settings": { "java": { "completion": { - "guessMethodArguments": "insertParameterNames", - "favoriteStaticMembers": ["java.util.Objects.*"] - } } } })), + Some(json!({ "settings": { "java": { "completion": fixture["pluginCompletion"] } } })), &[], - JdtSettings::default(), + settings, ) .unwrap(); assert_eq!( @@ -2011,7 +2039,8 @@ mod tests { json!(["java.util.Objects.*"]) ); - let notification = initialized_notification("java", JdtSettings::default()).unwrap(); + assert_eq!(options["settings"]["java"]["completion"], *expected); + let notification = initialized_notification("java", settings).unwrap(); assert_eq!( notification.params["settings"]["java"]["completion"], *expected @@ -2025,10 +2054,17 @@ mod tests { section: Some(section.as_str().unwrap().to_string()), }) .collect::>(); - let values = workspace_configuration("java", &items, JdtSettings::default()).unwrap(); + let values = workspace_configuration("java", &items, settings).unwrap(); assert_eq!(values[0]["completion"], *expected); assert_eq!(values[1], *expected); assert_eq!(values[2], expected["guessMethodArguments"]); + assert_eq!(values[3], expected["favoriteStaticMembers"]); + assert_eq!(values[4], expected["filteredTypes"]); + assert_eq!(values[5], Value::Null); + assert_eq!( + fixture["pluginCompletion"]["guessMethodArguments"], + "insertParameterNames" + ); } #[test] diff --git a/shared/contracts/rust-core-api.md b/shared/contracts/rust-core-api.md index 5198d695e8..16b44056bf 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -3170,7 +3170,9 @@ The shared JDT adapter sends `java.completion.guessMethodArguments: "off"` through initialization, configuration notifications and section queries on both platforms. JDT LS therefore leaves constructor and method argument lists empty while preserving the snippet cursor inside the parentheses. Other completion -settings declared by the provider are retained. This does not disable snippet +settings declared in the provider's initialization options are retained for the +session in initialize, didChangeConfiguration, and parent/leaf section pulls +(including favoriteStaticMembers and filteredTypes). This does not disable snippet support or strip parameter text from arbitrary completion items; the shared configuration fixture is `shared/fixtures/lsp/java-completion-v1.json`. diff --git a/shared/fixtures/lsp/java-completion-v1.json b/shared/fixtures/lsp/java-completion-v1.json index 7f8ecd3a4f..224648fec8 100644 --- a/shared/fixtures/lsp/java-completion-v1.json +++ b/shared/fixtures/lsp/java-completion-v1.json @@ -1,13 +1,23 @@ { "version": 1, + "pluginCompletion": { + "guessMethodArguments": "insertParameterNames", + "favoriteStaticMembers": ["java.util.Objects.*"], + "filteredTypes": ["example.internal.*"] + }, "javaSettings": { "completion": { - "guessMethodArguments": "off" + "guessMethodArguments": "off", + "favoriteStaticMembers": ["java.util.Objects.*"], + "filteredTypes": ["example.internal.*"] } }, "workspaceConfigurationSections": [ "java", "java.completion", - "java.completion.guessMethodArguments" + "java.completion.guessMethodArguments", + "java.completion.favoriteStaticMembers", + "java.completion.filteredTypes", + "java.completion.unknown" ] } diff --git a/shared/platform-feature-matrix/features/lsp-completion-hover.json b/shared/platform-feature-matrix/features/lsp-completion-hover.json index 4c04bd9971..ff19cc49f2 100644 --- a/shared/platform-feature-matrix/features/lsp-completion-hover.json +++ b/shared/platform-feature-matrix/features/lsp-completion-hover.json @@ -7,6 +7,7 @@ "evidence": [ "macos/Sources/Lithe/Core/Language", "rust/lithe-core/src/lsp/languages/jdt.rs", + "rust/lithe-core/src/lsp/interface/engine.rs", "shared/fixtures/lsp/java-completion-v1.json", "shared/fixtures/lsp" ], @@ -17,6 +18,7 @@ "evidence": [ "windows/tauri/src/features", "rust/lithe-core/src/lsp/languages/jdt.rs", + "rust/lithe-core/src/lsp/interface/engine.rs", "shared/fixtures/lsp/java-completion-v1.json", "shared/fixtures/lsp" ], @@ -24,5 +26,5 @@ "verificationStatus": "pending" }, "owner": "Language Tooling", - "verification": "在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。Java 构造器和方法补全使用 JDT LS 的 guessMethodArguments=off:输入 new No 后接受 Node 候选应得到 Node(),光标在括号内;方法调用同样不插入参数名,构造器重载候选由 JDT LS 合并为类型候选。验证 initialize、didChangeConfiguration 与 workspace/configuration 三个配置入口一致,其他语言 snippet、Java 方法补全签名展示及自动导入仍正常。" + "verification": "在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。Java 构造器和方法补全使用 JDT LS 的 guessMethodArguments=off:输入 new No 后接受 Node 候选应得到 Node(),光标在括号内;方法调用同样不插入参数名,构造器重载候选由 JDT LS 合并为类型候选。验证 initialize、didChangeConfiguration 与 workspace/configuration 三个配置入口一致,父级和叶级 section 均保留插件 favoriteStaticMembers、filteredTypes 等其他补全配置,其他语言 snippet、Java 方法补全签名展示及自动导入仍正常。" } From 002adb4dd5c4059a29d8acfefc0a435f6e2f03e8 Mon Sep 17 00:00:00 2001 From: puppy_1 <13323021675@163.com> Date: Fri, 23 Oct 2026 17:14:39 +0800 Subject: [PATCH 4/4] test(windows): budget multi-repository push integration cost --- ...026-09-13-ci-build-cache-and-artifact-strategy.md | 10 ++++++++++ .../references/windows-and-rust.md | 12 ++++++++++++ .../scripts/test-stability-windows.ps1 | 3 +++ .github/workflows/ci-windows.yml | 1 + 4 files changed, 26 insertions(+) diff --git a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md index 6e9ed0e893..016db8b7f3 100644 --- a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md +++ b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md @@ -218,6 +218,16 @@ runner 上可能出现断言都通过、计时工具仍因超过十秒而失败 持久化观察和退出信号仍保持原有短期限,单测试仍受 15 秒计时上限约束。 正确做法是等待明确的就绪事件,不通过固定睡眠同步,也不修改产品进程行为。 +Windows 的真实 Git 集成测试按实测成本设置单例预算,普通用例仍为 15 秒。 +双仓库分别确认推送目标的用例会初始化两个工作仓库与两个裸远端,先检查 +三种确认状态,再提交并推送两个仓库;同一提交在真实 JDT 任务中用时 12.3 秒, +在 SharedRust 任务中触发了 15 秒进程期限。本机跟踪 287 次 Git 进程全部退出, +没有单次 Git 内部操作超过 0.6 秒,但完整测试耗时 49.9 秒,30 秒预算也不足。 +两个任务都仅给这个用例 60 秒, +保留每次 Git 请求的期限、进程树清理和整套测试期限。正确做法是给已测量的 +外部进程成本配置精确用例预算;不要统一放宽所有测试或用清缓存重编译掩盖 +测试超时。具体预算与证据维护在 `write-stable-tests` 的 Windows/Rust 指引中。 + CI 缓存 Cargo fingerprints、build script outputs 和依赖 outputs,不缓存 最终可执行文件。缓存覆盖 `rust/target/macos` 的 Rust Core 和 `rust/target` 的数据库辅助 crate;缓存键必须包含运行器架构、编译器、Xcode/SDK/macOS diff --git a/.agents/skills/write-stable-tests/references/windows-and-rust.md b/.agents/skills/write-stable-tests/references/windows-and-rust.md index effcefd94c..a52bf147ea 100644 --- a/.agents/skills/write-stable-tests/references/windows-and-rust.md +++ b/.agents/skills/write-stable-tests/references/windows-and-rust.md @@ -80,6 +80,18 @@ suite deadline remains unchanged. Both SharedRust and the real-JDT lane use these narrowly scoped budgets. Core rebase requests use a nested 20-second deadline. The workspace commit submodule publication test also gets 30 seconds because it creates two local remotes and runs multiple real Git pushes. Other +workspace coverage includes +`tests::git_workspace_commit::git_workspace_commit_push_confirms_each_repository_separately`, +which gets 60 seconds in both SharedRust and real-JDT. It initializes two working +repositories and two bare remotes, prepares three independent confirmation states, +then commits and pushes both repositories. Windows run 39925982060 reached the +15-second process deadline in SharedRust without an assertion failure; the same +revision passed it in 12.3 seconds in real-JDT. A local Windows profile completed +all assertions in 49.9 seconds across 287 Git processes, with every Git process +exiting and no individual Git operation taking more than 0.6 seconds internally. +The total process and filesystem cost also exceeded a 30-second trial budget. +Each Git request retains its native +deadline and process-tree cleanup, and the suite deadline remains unchanged. Other multi-repository coverage includes `tests::git::git_write_updates_a_noncurrent_branch_without_switching_head`, which also gets 30 seconds in both SharedRust and real-JDT lanes: it builds a diff --git a/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 b/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 index cfa7b41875..e0e4b49929 100644 --- a/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 +++ b/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 @@ -97,6 +97,9 @@ function Invoke-TimedRustTests { # These retained-path Shelf cases also perform four preview/apply cycles. $arguments += @("--test-budget", "tests::git_patch_exchange::shelf_roundtrip_preserves_staged_deletions_with_retained_paths=60000") $arguments += @("--test-budget", "tests::git_patch_exchange::shelf_roundtrip_preserves_staged_rename_with_recreated_source=60000") + # Two repositories, three confirmation previews and two pushes invoke + # 287 Git processes; this integration case measured 49.9s on Windows. + $arguments += @("--test-budget", "tests::git_workspace_commit::git_workspace_commit_push_confirms_each_repository_separately=60000") } return $arguments } diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 99db6ed2db..49a8972d7e 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -784,6 +784,7 @@ jobs: --test-budget tests::git_history_rewrite::native_rebase_skip_omits_conflicted_message_from_later_squash=45000 ` --test-budget tests::git_remote_checkout::=30000 ` --test-budget tests::git_workspace_commit::git_workspace_commit_push_checks_submodule_publication_before_updating_the_remote=30000 ` + --test-budget tests::git_workspace_commit::git_workspace_commit_push_confirms_each_repository_separately=60000 ` --test-budget tests::git::git_write_squashes_=30000 ` --test-budget tests::git::git_write_deletes_a_local_commit_=30000 ` --test-budget tests::git::git_write_edits_a_local_commit_message_=30000 `