Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ Lithe 在两条启动路径上都用 `-Xmx1024m` 启动内置 Java 语言服务
4. 两端共用同一个引擎(`rust/lithe-core/src/lsp/interface/engine.rs` 的
`start_server`),macOS 和 Windows 同时生效,平台代码不需要改。

5. (2026-10-29 补充)堆上限抬到 2 GB 之后,JVM 默认的 G1 会长期持有接近上限的堆,
导入结束也很少还给系统。`jdt.rs` 的 `JDTLS_TUNING_ARGUMENTS` 因此在两条启动路径
上追加 vscode-java 和 JDT LS 上游 CI 使用的同一组默认参数:
`-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90`(吞吐量
收集器,允许 20% 的 GC 时间,堆在导入完成后会收缩),`-Xlog:disable`(JVM 统一
日志的告警默认写 stdout,而 stdout 是 LSP 通道),以及
`-DDetectVMInstallationsJob.disabled=true`(宿主已经通过
`java.configuration.runtimes` 传入 JDK,不需要 JDT LS 启动时再扫描本机 JVM)。
插件参数里出现同一个键(`-XX:` 选项按名字,`-Xlog` 视为一个键,其余按 `=` 前的
部分)时用插件的值;插件选了任何收集器(`-XX:±Use*GC`)时去掉默认的 ParallelGC,
因为 JVM 拒绝同时指定两个收集器。与默认值完全相同的参数视为宿主所有,重复适配的
结果不变。用例 `java_start_lets_plugin_jvm_options_replace_the_tuning_defaults`
固定这条覆盖规则。

正确做法:把堆上限当成"语言服务容量"的产品决定,集中在 `jdt.rs` 的适配层。

不要这样做:为了降低内存占用把默认上限降回 1.5 GB 或更低,让插件设置绕过 1536 MiB
Expand All @@ -59,6 +73,13 @@ Lithe 在两条启动路径上都用 `-Xmx1024m` 启动内置 Java 语言服务

## 考虑过的备选方案

- **照搬 openidea 的整套 JVM 参数(包括 `-Xms100m`、`-Daether.dependencyCollector.impl=bf`
和 `-Dsun.zip.disableMemoryMapping=true`)**:部分否决。`-Xms` 继续由本 Note 固定为
256m;`aether` 收集器会改变 Maven 依赖解析策略,m2e 是否读取这个系统属性没有核实,
不为可能的收益换未知的解析差异;`sun.zip.disableMemoryMapping` 在 JDK 9 以后不再
生效。顺带确认过:`-Dlog.protocol=true` 在 JDT LS 源码里没有读取方,是无效参数,
删除它没有性能收益,所以保留现状。

- **升级打包的 JDT LS 到含上游修复的版本**:否决。`download.eclipse.org/jdtls/milestones/`
当前最新仍是 1.61.0,官方只有会滚动的 snapshot,无法按 SHA-256 固定,不符合
`third_party/jdtls/manifest.json` 的固定版本加校验要求。上游修复进入下一个里程碑后
Expand All @@ -78,6 +99,9 @@ Lithe 在两条启动路径上都用 `-Xmx1024m` 启动内置 Java 语言服务
自己;JDT LS 同时拿到上游文档建议的堆,索引和编译大型工程的余量更充足。
- 代价:Java 语言服务 JVM 的堆上限从 1 GB 提到 2 GB。`-Xms` 仍是 256m,上限不是预留,
启动开销不变,只有工程确实需要时才会增长到 2 GB;代价是大工作区可能占用更多内存。
- GC 默认值的代价:ParallelGC 的单次停顿比 G1 长,但 JDT LS 是后台服务,编辑器的
界面线程不在这个 JVM 里,换来的是空闲时更低的常驻内存。插件需要别的收集器时直接在
参数里指定即可。
- 残留:坐标完全相同的模块本来就无法都成为独立工程(目录名和坐标都重复),仍然只有
一个进入 Java 模型,其余被 m2e 跳过。1.61.0 不会提示这件事,上游下一个版本会用通知
列出被跳过的 `pom.xml`;用户需要自己修正重复坐标。
Expand All @@ -90,7 +114,8 @@ Lithe 在两条启动路径上都用 `-Xmx1024m` 启动内置 Java 语言服务
- 用例 `java_start_adds_runtime_memory_and_unique_data_arguments` 和
`java_direct_start_builds_complete_shell_free_arguments_and_is_stable` 固定两条启动
路径的完整参数列表,`java_start_keeps_the_heap_above_the_jdtls_constrained_memory_threshold`
固定堆上限与 1.5 GB 阈值的关系。
固定堆上限与 1.5 GB 阈值的关系,`java_start_lets_plugin_jvm_options_replace_the_tuning_defaults`
固定 GC 默认值的覆盖规则。

## 适用范围

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 先说结论

Lithe 打包的 JDT LS 已内置 Buildship,因此 Gradle 工程本就能被导入、索引与补全。本次改动不改这条链路,只把原本依赖 Buildship 默认值的 Gradle 导入显式化:平台侧探测工作区根是否声明 Gradle、是否带 wrapper 描述文件,Rust Core 据此在 `settings.java` 下发 `java.import.gradle`,并把 Gradle 工程的结构文件纳入工作区指纹。下发的两个开关与上游默认值相同,对今天的导入结果是空操作,它固定的是 Lithe 依赖的行为前提,并让 Gradle 与 Maven 走同一套「平台探测 → Core 组装 → JDT LS」结构。
Lithe 打包的 JDT LS 已内置 Buildship,因此 Gradle 工程本就能被导入、索引与补全。本次改动不改这条链路,只把原本依赖 Buildship 默认值的 Gradle 导入显式化:平台侧探测工作区根是否声明 Gradle、是否带 wrapper 描述文件,Rust Core 据此在 `settings.java` 下发 `java.import.gradle`,并把 Gradle 工程的结构文件和 wrapper 的 Gradle 版本纳入工作区指纹。下发的两个开关与上游默认值相同,对今天的导入结果是空操作,它固定的是 Lithe 依赖的行为前提,并让 Gradle 与 Maven 走同一套「平台探测 → Core 组装 → JDT LS」结构。

## 问题

Expand All @@ -20,7 +20,9 @@ Maven 工程有一套完整的显式设置链路:平台侧探测 `pom.xml` 与
- `StartServerRequest` 增加 `java_gradle` 字段,`RuntimeSession` 保存它,并在构造初始化选项与工作区配置时统一复用。wrapper 是工作区根在启动时的属性,会话生命周期内不变,所以只在启动时确定一次。
- 只下发 `enabled` 与 `wrapper.enabled` 这两项可以直接判定的布尔值。`java.home`、`version`、`offline.enabled` 等需要读取 Gradle 脚本或网络状态,Core 无法在启动时确定,一律不下发,交由 Buildship 自行解析。
- 例外是用户自己选的构建选项(2026-10-18):声明 `languageImport.kind: "gradle"` 的构建插件把离线和 Gradle User Home 映射到自己的选项,`build.languageImport` 解析成 `javaGradle.offline` / `userHome`,Core 只在用户设置了它们时下发 `offline.enabled` 和 `user.home`。这些是用户的选择,不是猜测,见[构建系统插件契约](../architecture/2026-10-17-build-system-plugin-contract.md)第 12 节。
- Windows 的构建文件指纹(`windows/tauri/src-tauri/src/lsp/jdt_workspace.rs`)加入 `settings.gradle`、`settings.gradle.kts`(模块结构变化,对应 Maven 的直接模块扫描)与 `gradle/wrapper/gradle-wrapper.properties`(Gradle 版本所在)。`gradle.properties` 刻意不加入:mod 工程几乎每次版本提升都会改写它,一旦纳入指纹,每次改动都会换用新的 JDT 数据目录并冷导入一次。会话中途的构建文件变更由 Core 的 `classify_change` 负责。
- Windows 的构建文件指纹(`windows/tauri/src-tauri/src/lsp/jdt_workspace.rs`)记录 `settings.gradle`、`settings.gradle.kts` 与 `gradle/wrapper/gradle-wrapper.properties` **是否存在**,并单独读出 wrapper 的 `distributionUrl`(也就是 Gradle 版本)交给 Core。`gradle.properties` 刻意不加入。会话中途的构建文件变更由 Core 的 `classify_change` 分类后转发给 JDT LS。
- 指纹(决定 JDT 数据目录 `-data` 用哪个)只看结构,不看构建文件的修改时间和大小(2026-10-29 修订)。原因是 JDT LS 自己会处理内容变化:重启时它把每个 `pom.xml`、`*.gradle`、`*.gradle.kts` 的修改时间和上次保存的状态比较,只更新改过的项目(上游 `MavenProjectImporter.needsMavenUpdate`、`GradleProjectImporter.checkGradlePersistence`);运行中则通过 `workspace/didChangeWatchedFiles` 收到变化,`updateBuildConfiguration` 为 `automatic`。如果把修改时间放进指纹,改一下 pom、切一次分支都会换新目录,整个索引从零重建,这正是打开项目时"又在重新解析"的主要来源。
- 例外是 Gradle 版本:JDT LS 的检查不包含 wrapper 描述文件,所以 wrapper 的 `distributionUrl` 进入指纹,版本变了才换新目录。读值时去掉 properties 格式的转义(`https\://` 读成 `https://`),同一个版本总是得到同一个值。

TS 侧只做透传:`JavaLspLaunch` 增加可选的 `javaGradle`,`resolve-editor-lsp-launch.ts`、`lsp-core-adapter.ts`、`lsp-client.ts` 原样转发到核心的 `lsp.startServer`,不新增判断逻辑。

Expand All @@ -36,16 +38,19 @@ TS 侧只做透传:`JavaLspLaunch` 增加可选的 `javaGradle`,`resolve-edi

把 `gradle.properties` 与 `gradlew` 一并纳入指纹。否决:前者在 mod 工程里变动频繁,纳入后每次版本提升都会冷导入一次;后者并不携带 Gradle 版本信息。收益不足以抵消代价。

用构建文件的修改时间和大小区分工作区状态(2026-10-29 前的做法)。否决:内容变化本来由 JDT LS 判断,再按时间戳换目录等于在上游之外维护第二份"是否过期"的判断,代价是每次编辑都冷导入。只有 JDT LS 看不到的 Gradle 版本由 Lithe 补上。

让 macOS 侧也补 wrapper 探测与指纹扩展。本次不做:改动范围限定为 Rust 内核与 Windows;macOS 依赖 Buildship 默认值即可导入 Gradle 工程,功能不缺,只是在显式声明与指纹覆盖上暂不对称。

## 后果

Gradle 与 Maven 在导入设置上结构对称,行为可预期、可扩展。Windows 下发的两个开关与 Buildship 默认值一致,导入结果不变;真正改变行为的是工作区指纹:`settings.gradle` 或 wrapper 描述文件变化会换用新的 JDT 数据目录并重新导入,而改写 `gradle.properties` 不会。macOS 在 2026-10-18 经构建插件 `languageImport` 接入显式设置:`BuildSystemService` 等待 Core 归一化后,把 `javaGradle` 经语言会话和 Rust Swift 桥传给启动请求,离线和 User Home 选项等下次会话启动生效。没有插件输入时保留 Buildship 默认行为。macOS 尚未扩展指纹(`MacJdtWorkspaceState.buildFileNames` 仍只覆盖 `pom.xml`、`build.gradle`、`build.gradle.kts`),同一工程在两个平台上的指纹含义并不相同;真实 macOS 导入验收也仍待完成。若工作区根未声明 Gradle 构建文件,Core 不会下发任何 `java.import` 设置,纯 Maven 工程不受影响。
Gradle 与 Maven 在导入设置上结构对称,行为可预期、可扩展。Windows 下发的两个开关与 Buildship 默认值一致,导入结果不变;真正改变行为的是工作区指纹:新增或删除 `settings.gradle`、wrapper 描述文件,或 wrapper 的 Gradle 版本变化,会换用新的 JDT 数据目录并重新导入;编辑这些文件的其他内容、改写 `gradle.properties` 都不会,由 JDT LS 自己增量更新。指纹格式在 2026-10-29 修订时变化,升级后每个工作区会重建一次索引。macOS 在 2026-10-18 经构建插件 `languageImport` 接入显式设置:`BuildSystemService` 等待 Core 归一化后,把 `javaGradle` 经语言会话和 Rust Swift 桥传给启动请求,离线和 User Home 选项等下次会话启动生效。没有插件输入时保留 Buildship 默认行为。macOS 尚未扩展指纹(`MacJdtWorkspaceState.buildFileNames` 仍只覆盖 `pom.xml`、`build.gradle`、`build.gradle.kts`),同一工程在两个平台上的指纹含义并不相同;真实 macOS 导入验收也仍待完成。若工作区根未声明 Gradle 构建文件,Core 不会下发任何 `java.import` 设置,纯 Maven 工程不受影响。

## 验证

- `rust/lithe-core` 单测覆盖:初始化选项在导入前即携带 Gradle 设置、无 wrapper 描述文件时省略 wrapper 字段、无 Gradle 上下文时不下发 `java.import`、按 section 查询返回对应 Gradle 取值、叶子合并保留调用方已声明的 `offline` 与 `version`。
- `windows/tauri/src-tauri/src/lsp.rs` 单测覆盖:工作区指纹识别 Gradle 设置文件与 wrapper 描述文件且忽略 `gradle.properties`、根目录缺少构建文件时不判为 Gradle、仅有 `gradlew` 脚本时不认为存在 wrapper、Maven 工作区不产生 Gradle 上下文。
- `windows/tauri/src-tauri/src/lsp.rs` 单测覆盖:工作区指纹识别 Gradle 设置文件与 wrapper 描述文件且忽略 `gradle.properties`、编辑构建文件不改变观测结果、wrapper 版本(含转义写法)被读出且版本变化会改变指纹、根目录缺少构建文件时不判为 Gradle、仅有 `gradlew` 脚本时不认为存在 wrapper、Maven 工作区不产生 Gradle 上下文。
- 共享 fixture `shared/fixtures/lsp/jdt-workspace-fingerprint-v1.json` 由 `rust/lithe-core` 的 `jdt_workspace_fingerprint_matches_the_shared_compatibility_fixture` 校验,含带 wrapper 版本的用例;macOS 的 `MacJdtWorkspaceStateTests` 校验编辑 `pom.xml` 后观测结果不变。
- `windows/tauri/src/features/editor/lsp/resolve-editor-lsp-launch.test.ts` 与 `windows/tauri/src/platform/lsp-core-adapter.test.ts` 覆盖 TS 透传:有 Gradle 上下文时原样转发,无上下文时为 `null`。
- `scripts/rust-core/verify-rust-core.sh`、`scripts/rust-core/verify-rust-core-comments.sh`、`scripts/rust-core/verify-rust-core-layout.sh` 校验 Rust 核心的格式、注释与包布局。
- `scripts/platform-feature-matrix/verify-platform-feature-matrix.sh`、`scripts/platform-feature-matrix/verify-platform-feature-matrix-change.sh` 与 `scripts/agent-notes/verify-agent-notes.sh` 校验功能矩阵与 Note 记录。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ JDT LS 这类语言服务器有很多启动参数,最常见的是堆内存上

### 4. JDT 适配层让出两个参数

直接启动 JDT 时,宿主以前会丢弃插件传来的 `-Xmx` 和 `-Dlog.level=`,再塞自己的默认值。现在这两个参数改为“插件的最后一个合法值优先,没有就用宿主默认值(`-Xmx2048m`、`-Dlog.level=ALL`,即以前的行为)”。合法性很窄:堆大小必须是数字加可选的 `k`/`m`/`g`,并且必须大于 1536 MiB(JDT LS 在不超过 1.5 GB 时会改走“分批导入”,模块坐标重复会让整个 Java 服务失败,见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md`);日志级别必须是字母数字;不合法的值被丢弃而不是传给 JVM。Java Support 的清单因此把 `heapMaxMB` 的下限和默认值都设为 2048。`-Xms`、`--add-*`、Eclipse 产品属性、元数据位置和 Lombok 仍归宿主所有。
直接启动 JDT 时,宿主以前会丢弃插件传来的 `-Xmx` 和 `-Dlog.level=`,再塞自己的默认值。现在这两个参数改为“插件的最后一个合法值优先,没有就用宿主默认值(`-Xmx2048m`、`-Dlog.level=ALL`,即以前的行为)”。合法性很窄:堆大小必须是数字加可选的 `k`/`m`/`g`,并且必须大于 1536 MiB(JDT LS 在不超过 1.5 GB 时会改走“分批导入”,模块坐标重复会让整个 Java 服务失败,见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md`);日志级别必须是字母数字;不合法的值被丢弃而不是传给 JVM。Java Support 的清单因此把 `heapMaxMB` 的下限和默认值都设为 2048。`-Xms`、`--add-*`、Eclipse 产品属性、元数据位置和 Lombok 仍归宿主所有。宿主另外追加一组 GC 与日志默认值,插件参数里同键的值优先(见 `2026-10-09-jdtls-heap-ceiling-and-duplicate-maven-coordinates.md` 决策第 5 条)。

这个读取是幂等的:宿主把自己产出的参数再喂回适配层,得到相同的结果,已有的稳定性测试继续成立。

Expand Down
2 changes: 0 additions & 2 deletions macos/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,6 @@
"Could not prepare the run configuration: %@" = "Could not prepare the run configuration: %@";
"Could not read %@." = "Could not read %@.";
"Could not read %@: %@" = "Could not read %@: %@";
"Could not read Java build-file metadata for %@." = "Could not read Java build-file metadata for %@.";
"Could not read the command line." = "Could not read the command line.";
"Could not read the project folder. Check that it still exists and that Lithe has permission to access it." = "Could not read the project folder. Check that it still exists and that Lithe has permission to access it.";
"Could not read the selected built-in background." = "Could not read the selected built-in background.";
Expand Down Expand Up @@ -5473,7 +5472,6 @@
"native.couldNotPrepareTheRunConfiguration" = "Could not prepare the run configuration: %1$@";
"native.couldNotRead" = "Could not read %1$@: %2$@";
"native.couldNotRead.variant2" = "Could not read %1$@.";
"native.couldNotReadJavaBuildFileMetadataFor" = "Could not read Java build-file metadata for %1$@.";
"native.couldNotReadTheCommandLine" = "Could not read the command line.";
"native.couldNotReadTheProjectFolderCheckThatItStillExistsAndThatLitheHasPermissionToAccessIt" = "Could not read the project folder. Check that it still exists and that Lithe has permission to access it.";
"native.couldNotReadTheSelectedBuiltInBackground" = "Could not read the selected built-in background.";
Expand Down
2 changes: 0 additions & 2 deletions macos/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,6 @@
"Could not prepare the run configuration: %@" = "无法准备运行配置:%@";
"Could not read %@." = "无法读取 %@。";
"Could not read %@: %@" = "无法读取 %@:%@";
"Could not read Java build-file metadata for %@." = "无法读取 %@ 的 Java 构建文件元数据。";
"Could not read the command line." = "无法读取命令行。";
"Could not read the project folder. Check that it still exists and that Lithe has permission to access it." = "无法读取项目文件夹,请确认文件夹仍然存在并允许 Lithe 访问。";
"Could not read the selected built-in background." = "无法读取所选的内置背景。";
Expand Down Expand Up @@ -5473,7 +5472,6 @@
"native.couldNotPrepareTheRunConfiguration" = "无法准备运行配置:%1$@";
"native.couldNotRead" = "无法读取 %1$@:%2$@";
"native.couldNotRead.variant2" = "无法读取 %1$@。";
"native.couldNotReadJavaBuildFileMetadataFor" = "无法读取 %1$@ 的 Java 构建文件元数据。";
"native.couldNotReadTheCommandLine" = "无法读取命令行。";
"native.couldNotReadTheProjectFolderCheckThatItStillExistsAndThatLitheHasPermissionToAccessIt" = "无法读取项目文件夹,请确认文件夹仍然存在并允许 Lithe 访问。";
"native.couldNotReadTheSelectedBuiltInBackground" = "无法读取所选的内置背景。";
Expand Down
2 changes: 0 additions & 2 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1843,8 +1843,6 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {

struct JdtBuildFileObservation: Encodable, Equatable, Sendable {
let path: String
let modifiedUnixMilliseconds: UInt64
let sizeBytes: UInt64
}

private struct JdtWorkspaceFingerprintRequest: Encodable {
Expand Down
Loading
Loading