Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/development/platform-parity-matrix.csv
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ lsp-lifecycle,Java,语言服务,LSP/JDTLS 启动与工作区准备,已实现,待
lsp-completion-hover,Java,语言服务,补全与 Hover,已实现,待验证,已实现,待验证,Language Tooling,在相同 Java fixture 中验证补全、Hover、排序、超时和空结果。,,macos/Sources/Lithe/Core/Language; shared/fixtures/lsp,windows/tauri/src/features; shared/fixtures/lsp
lsp-diagnostics,Java,语言服务,诊断与编译错误定位,已实现,待验证,已实现,待验证,Language Tooling,制造语法和类型错误,比较诊断等级、消息、行列号和清理行为。,,macos/Sources/Lithe/Core/Language; macos/Sources/Lithe/Services/Diagnostics,windows/tauri/src/features/diagnostics; shared/fixtures/lsp
lsp-navigation-edits,Java,语言服务,跳转、引用、语义标记与编辑,已实现,待验证,已实现,待验证,Language Tooling,验证定义、引用、语义标记和 UTF-16 文本编辑的坐标转换。,,macos/Sources/Lithe/Core/Language; shared/contracts/rust-core-api.md,windows/tauri/src/features; shared/fixtures/lsp
run-discovery,运行与调试,运行配置,入口点与运行配置发现,已实现,待验证,已实现,待验证,Run,验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。,,macos/Sources/Lithe/Views/Run; shared/fixtures/run-configuration,windows/tauri/src/features/run; shared/fixtures/run-configuration
run-discovery,运行与调试,运行配置,入口点与运行配置发现,已实现,待验证,已实现,待验证,Run,验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 <pluginManagement> 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。,,macos/Sources/Lithe/Views/Run; shared/fixtures/run-configuration,windows/tauri/src/features/run; rust/lithe-core/src/project/maven.rs; rust/lithe-core/src/execution/detectors/maven.rs; shared/fixtures/run-configuration
run-save-toolchain,运行与调试,运行配置,保存前同步与工具链解析,已实现,待验证,已实现,待验证,Run,修改未保存文件后运行,验证同步、JDK/Maven 选择和版本不匹配诊断。,,macos/Sources/Lithe/Views/Run; macos/Sources/Lithe/Services/Java,windows/tauri/src/features/run; windows/tauri/src/features/maven
run-java-test,运行与调试,运行配置,Java main 与测试运行,已实现,待验证,已实现,待验证,Run,运行 main、单测试和测试类,验证参数、输出、失败状态和终端策略。 Windows Run 运行输出使用 Geist Mono 显示 ====、==>、=>、!=、>=,确认禁用连字后逐字符显示且复制文本与原始输出一致;检查有输出与空态、ANSI 颜色、自动换行切换及横向滚动。,,macos/Sources/Lithe/Views/Run; shared/fixtures/debug,windows/tauri/src/features/run; shared/fixtures/debug; windows/tauri/src/features/run/components/run-pane.tsx; windows/tauri/src/features/run/components/run-output-text.tsx
run-java-long-classpath,运行与调试,运行配置,超长 Java 类路径自动缩短,已实现,待验证,已实现,待验证,Run,使用包含大量依赖的 Java 项目运行 main,确认超长类路径自动写入参数文件、进程可启动且参数文件在退出后清理。 Windows 上分别用 JDK 8 和 JDK 17 运行同一个依赖很多、路径含空格和中文的项目 main:JDK 8 应在临时目录生成只含 META-INF/MANIFEST.MF 的 .classpath.jar 并以 -cp 引用,JDK 17 仍使用 @argfile;两者都能启动,进程退出或启动失败后临时文件被删除,安装目录与 JDK 目录不新增文件。 在慢速网络盘 classpath 准备期间确认工作台可交互;停止或重新运行后,旧准备结果不得启动进程,旧 executionId 的停止请求不得影响新执行。,,macos/Sources/Lithe/Platform/MacOS/RunConfiguration/MacJavaLaunchArgumentPreparer.swift; macos/Sources/LitheExecutionModule/Services/RunService.swift; rust/lithe-core/src/execution/launch_command.rs,windows/tauri/src-tauri/src/run/launch_arguments.rs; rust/lithe-core/src/execution/launch_command.rs; rust/lithe-core/src/execution/classpath_jar.rs; windows/tauri/src-tauri/src/run.rs
Expand Down
2 changes: 1 addition & 1 deletion docs/development/platform-parity-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@

| 功能组 | 能力点 | macOS | Windows | 负责人 | 验证方式 | 备注 |
| --- | --- | --- | --- | --- | --- | --- |
| 运行配置 | **入口点与运行配置发现**<br><sub>run-discovery</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/Run`、`shared/fixtures/run-configuration`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/run`、`shared/fixtures/run-configuration`</sub> | Run | 验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。 | |
| 运行配置 | **入口点与运行配置发现**<br><sub>run-discovery</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/Run`、`shared/fixtures/run-configuration`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/run`、`rust/lithe-core/src/project/maven.rs`、`rust/lithe-core/src/execution/detectors/maven.rs`、`shared/fixtures/run-configuration`</sub> | Run | 验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 <pluginManagement> 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。 | |
| 运行配置 | **保存前同步与工具链解析**<br><sub>run-save-toolchain</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/Run`、`macos/Sources/Lithe/Services/Java`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/run`、`windows/tauri/src/features/maven`</sub> | Run | 修改未保存文件后运行,验证同步、JDK/Maven 选择和版本不匹配诊断。 | |
| 运行配置 | **Java main 与测试运行**<br><sub>run-java-test</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Views/Run`、`shared/fixtures/debug`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src/features/run`、`shared/fixtures/debug`、`windows/tauri/src/features/run/components/run-pane.tsx`、`windows/tauri/src/features/run/components/run-output-text.tsx`</sub> | Run | 运行 main、单测试和测试类,验证参数、输出、失败状态和终端策略。 Windows Run 运行输出使用 Geist Mono 显示 ====、==>、=>、!=、>=,确认禁用连字后逐字符显示且复制文本与原始输出一致;检查有输出与空态、ANSI 颜色、自动换行切换及横向滚动。 | |
| 运行配置 | **超长 Java 类路径自动缩短**<br><sub>run-java-long-classpath</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`macos/Sources/Lithe/Platform/MacOS/RunConfiguration/MacJavaLaunchArgumentPreparer.swift`、`macos/Sources/LitheExecutionModule/Services/RunService.swift`、`rust/lithe-core/src/execution/launch_command.rs`</sub> | ✅ 已实现<br><sub>🔍 待验证</sub><br><sub>`windows/tauri/src-tauri/src/run/launch_arguments.rs`、`rust/lithe-core/src/execution/launch_command.rs`、`rust/lithe-core/src/execution/classpath_jar.rs`、`windows/tauri/src-tauri/src/run.rs`</sub> | Run | 使用包含大量依赖的 Java 项目运行 main,确认超长类路径自动写入参数文件、进程可启动且参数文件在退出后清理。 Windows 上分别用 JDK 8 和 JDK 17 运行同一个依赖很多、路径含空格和中文的项目 main:JDK 8 应在临时目录生成只含 META-INF/MANIFEST.MF 的 .classpath.jar 并以 -cp 引用,JDK 17 仍使用 @argfile;两者都能启动,进程退出或启动失败后临时文件被删除,安装目录与 JDK 目录不新增文件。 在慢速网络盘 classpath 准备期间确认工作台可交互;停止或重新运行后,旧准备结果不得启动进程,旧 executionId 的停止请求不得影响新执行。 | |
Expand Down
38 changes: 35 additions & 3 deletions rust/lithe-core/src/project/maven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,10 @@ struct Descriptor {
group_id: Option<String>,
artifact_id: Option<String>,
version: Option<String>,
/// `artifactId` of `<parent>`, recorded so a module can be matched back to
/// the reactor entry it inherits from. `relativePath` is not needed: the
/// reactor tree already places every parent above its children.
parent_artifact_id: Option<String>,
packaging: String,
build_directory: Option<String>,
/// `<reportsDirectory>` values configured for Surefire or Failsafe, raw.
Expand Down Expand Up @@ -738,7 +742,7 @@ pub fn declared_modules(root: &Path) -> Result<Vec<DeclaredModule>, CoreError> {
};
let mut modules = Vec::new();
let mut visited = vec![root.to_path_buf()];
collect_modules(root, root, root_descriptor, &mut modules, &mut visited);
collect_modules(root, root, root_descriptor, &mut modules, &mut visited, &[]);
Ok(modules)
}

Expand All @@ -752,6 +756,7 @@ fn collect_modules(
current: Descriptor,
modules: &mut Vec<DeclaredModule>,
visited: &mut Vec<PathBuf>,
inherited_plugins: &[String],
) {
let relative_path = directory
.strip_prefix(root)
Expand All @@ -760,6 +765,20 @@ fn collect_modules(
.filter(|value| !value.is_empty())
.unwrap_or_else(|| ".".to_string());
let current_source_roots = source_roots(Some(&current));
// Maven merges a parent's `<build><plugins>` into every child, so a boot
// plugin declared on an aggregator reaches the modules below it. Only the
// ancestor plugins already collected on the way down are merged: a child
// redeclaring the same `artifactId` is already present, which is the same
// answer either way because callers only ask whether a plugin is present.
// `<pluginManagement>` never reaches this set because `descriptor` collects
// `project/build/plugins` only, so a managed-but-unapplied plugin stays
// inert.
let mut effective_plugins = inherited_plugins.to_vec();
for plugin in &current.plugins {
if !effective_plugins.contains(plugin) {
effective_plugins.push(plugin.clone());
}
}
modules.push(DeclaredModule {
relative_path,
artifact_id: current.artifact_id.unwrap_or_else(|| {
Expand All @@ -770,7 +789,7 @@ fn collect_modules(
.to_string()
}),
packaging: current.packaging,
plugins: current.plugins,
plugins: effective_plugins.clone(),
source_roots: current_source_roots,
});
for raw_path in &current.module_paths {
Expand All @@ -785,7 +804,14 @@ fn collect_modules(
continue;
};
visited.push(child.clone());
collect_modules(root, &child, child_descriptor, modules, visited);
collect_modules(
root,
&child,
child_descriptor,
modules,
visited,
&effective_plugins,
);
}
}

Expand Down Expand Up @@ -1904,6 +1930,12 @@ fn descriptor(path: &Path) -> Result<Option<Descriptor>, CoreError> {
}
}
"project/artifactId" => value.artifact_id = non_empty(text.clone()),
// `<parent><artifactId>` is what identifies the reactor entry a
// module inherits from; without it a parent declared only in
// `<build><plugins>` cannot be reached from the child.
"project/parent/artifactId" => {
value.parent_artifact_id = non_empty(text.clone())
}
"project/version" | "project/parent/version" => {
if value.version.is_none() || path == "project/version" {
value.version = non_empty(text.clone());
Expand Down
92 changes: 92 additions & 0 deletions rust/lithe-core/src/tests/detectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,3 +1673,95 @@ fn java_entries_are_exactly_the_classes_jdt_reports() {

fs::remove_dir_all(root).unwrap();
}

/// Maven merges a parent's `<build><plugins>` into every child, so a boot
/// plugin an aggregator declares reaches the modules below it. Without that the
/// child is emitted as a plain application and never offered as a service.
#[test]
fn maven_detector_inherits_the_boot_plugin_from_the_aggregator() {
let root = temporary_root("detect-maven-inherited-boot-plugin");
fs::create_dir_all(root.join("service-web")).unwrap();
fs::write(
root.join("pom.xml"),
"<project><artifactId>parent</artifactId><packaging>pom</packaging><modules><module>service-web</module></modules><build><plugins><plugin><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>",
)
.unwrap();
fs::write(
root.join("service-web/pom.xml"),
"<project><parent><artifactId>parent</artifactId></parent><artifactId>service-web</artifactId></project>",
)
.unwrap();

let ids = generated_configurations(&root)
.into_iter()
.filter(|item| item["provider"] == "spring-boot.maven")
.map(|item| item["id"].as_str().unwrap().to_string())
.collect::<Vec<_>>();

assert_eq!(ids, vec!["spring-boot.maven:service-web".to_string()]);

fs::remove_dir_all(root).unwrap();
}

/// Inheritance crosses more than one level: a plugin declared on the reactor
/// root is what the grandchild actually inherits, and the intermediate
/// aggregator contributes nothing of its own.
#[test]
fn maven_detector_inherits_the_boot_plugin_across_several_levels() {
let root = temporary_root("detect-maven-inherited-boot-plugin-deep");
fs::create_dir_all(root.join("team/service-web")).unwrap();
fs::write(
root.join("pom.xml"),
"<project><artifactId>root</artifactId><packaging>pom</packaging><modules><module>team</module></modules><build><plugins><plugin><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>",
)
.unwrap();
fs::write(
root.join("team/pom.xml"),
"<project><parent><artifactId>root</artifactId><relativePath>../pom.xml</relativePath></parent><artifactId>team</artifactId><packaging>pom</packaging><modules><module>service-web</module></modules></project>",
)
.unwrap();
fs::write(
root.join("team/service-web/pom.xml"),
"<project><parent><artifactId>team</artifactId><relativePath>../pom.xml</relativePath></parent><artifactId>service-web</artifactId></project>",
)
.unwrap();

let ids = generated_configurations(&root)
.into_iter()
.filter(|item| item["provider"] == "spring-boot.maven")
.map(|item| item["id"].as_str().unwrap().to_string())
.collect::<Vec<_>>();

assert_eq!(ids, vec!["spring-boot.maven:service-web".to_string()]);

fs::remove_dir_all(root).unwrap();
}

/// `<pluginManagement>` pins a version for children without applying the plugin,
/// and inheriting a parent must not turn a managed plugin into a service. This is
/// the counterpart to the inheritance above: the same parent shape, but the
/// plugin stays inert.
#[test]
fn maven_detector_does_not_inherit_a_plugin_managed_parent() {
let root = temporary_root("detect-maven-inherited-plugin-management");
fs::create_dir_all(root.join("service-web")).unwrap();
fs::write(
root.join("pom.xml"),
"<project><artifactId>parent</artifactId><packaging>pom</packaging><modules><module>service-web</module></modules><build><pluginManagement><plugins><plugin><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></pluginManagement></build></project>",
)
.unwrap();
fs::write(
root.join("service-web/pom.xml"),
"<project><parent><artifactId>parent</artifactId></parent><artifactId>service-web</artifactId></project>",
)
.unwrap();

assert!(
!generated_configurations(&root)
.iter()
.any(|item| item["provider"] == "spring-boot.maven"),
"a managed plugin must stay inert even when a parent declares it"
);

fs::remove_dir_all(root).unwrap();
}
4 changes: 3 additions & 1 deletion shared/platform-feature-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -1569,13 +1569,15 @@
"windows": {
"evidence": [
"windows/tauri/src/features/run",
"rust/lithe-core/src/project/maven.rs",
"rust/lithe-core/src/execution/detectors/maven.rs",
"shared/fixtures/run-configuration"
],
"implementationStatus": "implemented",
"verificationStatus": "pending"
},
"owner": "Run",
"verification": "验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。"
"verification": "验证 Spring Boot、Java、Maven、Gradle、npm、Cargo、Go、Python 和 Docker Compose 入口识别;框架 plugin 只在父 POM 声明时,子模块(含多级嵌套)仍识别为对应服务,而 <pluginManagement> 声明保持不生效;多模块项目把服务工作目录改成模块目录后,服务仍留在列表中并以该目录启动;填写不存在的目录或 ${workspaceFolder} 等变量时,保存被拒绝并提示原因;在 Windows 确认重新扫描服务使用双箭头刷新图标。"
},
{
"id": "run-save-toolchain",
Expand Down
Loading