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..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 @@ -126,6 +126,15 @@ 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 接受候选后的位置和自动导入。 +运行会话保留初始化时插件声明的 completion 对象,配置通知和 `java`、`java.completion` 及其叶节点请求均从同一份对象应用 `guessMethodArguments=off`。例如插件声明的 `favoriteStaticMembers`、`filteredTypes` 不得在初始化后被通知或拉取结果清空;引擎测试同时检查真实会话接线,不能只用默认设置证明三个入口一致。 + ## 考虑过的备选方案 ### 只把文件搬进插件包,宿主继续扫描目录 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 5cbc89f2fa..a4a147c02a 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 @@ -238,6 +238,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 8db5a25441..10ae4ad139 100644 --- a/.agents/skills/write-stable-tests/references/windows-and-rust.md +++ b/.agents/skills/write-stable-tests/references/windows-and-rust.md @@ -93,7 +93,15 @@ integration cases create multiple repositories/remotes and run real Git pushes. Windows run 39947608155 reached the default 15-second deadline in `git_workspace_commit_push_confirms_each_repository_separately` at 15.16 seconds without an assertion failure; this replaces the previous single-case allowance -for submodule publication. Other +for submodule publication. That case additionally 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. 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, so the +module's 30-second allowance is not enough for it. 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 0a7a019335..6ef6d3f894 100644 --- a/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 +++ b/.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 @@ -104,6 +104,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 37c787861f..fdb107170a 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -790,7 +790,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=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 ` diff --git a/rust/lithe-core/src/lsp/interface/engine.rs b/rust/lithe-core/src/lsp/interface/engine.rs index 4f891153ad..0daeae5a95 100644 --- a/rust/lithe-core/src/lsp/interface/engine.rs +++ b/rust/lithe-core/src/lsp/interface/engine.rs @@ -715,6 +715,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, @@ -960,6 +963,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, @@ -1552,6 +1556,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( @@ -1564,6 +1574,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, @@ -1595,6 +1606,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(()), @@ -8671,6 +8683,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() @@ -8693,7 +8709,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 @@ -8725,6 +8742,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 0126f7382b..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>, @@ -443,7 +446,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 +470,19 @@ 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.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 // Lithe deliberately never guesses (`java.home`, `version`, // `offline.enabled`, ...) have to survive, so the owned leaves merge @@ -1074,6 +1090,9 @@ 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": java_completion_settings(settings.completion), "inlayHints": { "parameterNames": { "enabled": "all" @@ -1104,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 @@ -1163,6 +1193,15 @@ 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(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" @@ -1424,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(); @@ -1462,6 +1502,7 @@ mod tests { maven: Some(configuration), gradle: None, java_runtimes: &[], + ..Default::default() } } @@ -1470,6 +1511,7 @@ mod tests { maven: None, gradle: Some(configuration), java_runtimes: &[], + ..Default::default() } } @@ -1968,6 +2010,63 @@ 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 settings = JdtSettings { + completion: Some(&fixture["pluginCompletion"]), + ..Default::default() + }; + let options = adapt_initialization_options( + "java", + Some(json!({ "settings": { "java": { "completion": fixture["pluginCompletion"] } } })), + &[], + settings, + ) + .unwrap(); + assert_eq!( + options["settings"]["java"]["completion"]["guessMethodArguments"], + expected["guessMethodArguments"] + ); + assert_eq!( + options["settings"]["java"]["completion"]["favoriteStaticMembers"], + json!(["java.util.Objects.*"]) + ); + + assert_eq!(options["settings"]["java"]["completion"], *expected); + let notification = initialized_notification("java", settings).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, 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] 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 b10c1ac98f..013ba0d65f 100644 --- a/shared/contracts/rust-core-api.md +++ b/shared/contracts/rust-core-api.md @@ -3475,6 +3475,16 @@ 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 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`. + ### 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..224648fec8 --- /dev/null +++ b/shared/fixtures/lsp/java-completion-v1.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "pluginCompletion": { + "guessMethodArguments": "insertParameterNames", + "favoriteStaticMembers": ["java.util.Objects.*"], + "filteredTypes": ["example.internal.*"] + }, + "javaSettings": { + "completion": { + "guessMethodArguments": "off", + "favoriteStaticMembers": ["java.util.Objects.*"], + "filteredTypes": ["example.internal.*"] + } + }, + "workspaceConfigurationSections": [ + "java", + "java.completion", + "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 e5ed25cea1..ff19cc49f2 100644 --- a/shared/platform-feature-matrix/features/lsp-completion-hover.json +++ b/shared/platform-feature-matrix/features/lsp-completion-hover.json @@ -6,6 +6,9 @@ "macos": { "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" ], "implementationStatus": "implemented", @@ -14,11 +17,14 @@ "windows": { "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" ], "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 三个配置入口一致,父级和叶级 section 均保留插件 favoriteStaticMembers、filteredTypes 等其他补全配置,其他语言 snippet、Java 方法补全签名展示及自动导入仍正常。" }