After I missed the feature of running Java tasks in multi modules java projects I have enhanced the tasks.json commands (Heavy Shell script in a single line). (see #244)
So I have make up my minds and would suggest to move the logic from tasks.json to Rust code.
So my design Idee is:
1. Extend java-lsp-proxy with Task Subcommands
Add a new module to the proxy (proxy/src/task.rs) that provides CLI subcommands:
java-lsp-proxy task run-class <file> <package> <class> [outer_class]
java-lsp-proxy task run-test-method <file> <package> <class> [outer_class] <method>
java-lsp-proxy task run-test-class <file> <package> <class> [outer_class]
java-lsp-proxy task run-all-tests <file>
java-lsp-proxy task clear-cache
Each command outputs JSON to stdout with the resolved command + arguments:
{
"command": "/usr/bin/mvn",
"args": ["clean", "test", "-pl", "backend", "-Dtest=com.example.MyTest"],
"cwd": "/path/to/workspace"
}
2. Main Module Structure (proxy/src/task.rs)
// Core functions the Rust side handles:
enum BuildTool {
Maven,
Gradle,
None,
}
fn detect_build_tool(file_path: &str) -> BuildTool {
// Walk up from file to workspace root
// Check for pom.xml (Maven) or build.gradle* / settings.gradle* (Gradle)
}
fn find_closest_module(file_path: &str, is_maven: bool) -> PathBuf {
// Walk up from file's directory finding nearest pom.xml or build.gradle*
// Returns the module directory path
}
fn which_wrapper(tool: &str) -> String {
// Prefer ./mvnw or ./gradlew in CWD, fall back to PATH
}
struct TaskCommand {
command: String,
args: Vec<String>,
cwd: String,
}
3. Each Task Handler
run-class (replaces the java-main tag):
fn task_run_class(file: &str, package: &str, class: &str, outer: Option<&str>) -> TaskCommand {
let full_class = match outer {
Some(o) => format!("{}.${}", class, o),
None => class.to_string(),
};
let full_name = if package.is_empty() { full_class } else { format!("{}.{}", package, full_class) };
match detect_build_tool(file) {
BuildTool::Maven => {
let module = find_closest_module(file, true);
let cmd = which_wrapper("mvn");
// mvn clean compile exec:java -Dexec.mainClass=...
// or: mvn clean compile -pl module -am && mvn exec:java -pl module ...
}
BuildTool::Gradle => {
let module = find_closest_module(file, false);
let cmd = which_wrapper("gradle");
// gradle :module:run -PmainClass=...
}
BuildTool::None => {
// javac + java fallback
}
}
}
run-test-method (replaces java-test-method / java-test-method-nested) run-all-tests: Same pattern without class/method filtering.:
fn task_run_test_method(file: &str, package: &str, class: &str, outer: Option<&str>, method: &str) -> TaskCommand {
// Similar structure, constructs:
// Maven: mvn test -Dtest="package.Class#method"
// Gradle: gradle test --tests "package.Class.method"
}
clear-cache:
fn task_clear_cache() -> TaskCommand {
// Detect cache dir: XDG_CACHE_HOME, ~/Library/Caches, or ~/.cache
// Construct: rm -rf ~/.cache/jdtls-*
}
4. Wire into main.rs
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
// New: handle task subcommands
if args.first().map(|a| a == "task").unwrap_or(false) {
task::handle_task(&args[1..]);
return;
}
// Existing: LSP proxy mode
// ...
}
5. Simplified tasks.json
Current (complex shell):
"command": "pkg=\"$ZED_CUSTOM_java_package_name\"; cls=\"$ZED_CUSTOM_java_class_name\"; ... 200 chars of shell ..."
After (thin wrapper):
{
"label": "Run $ZED_CUSTOM_java_class_name",
"command": "ZED_EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java/proxy-bin\"; [ \"$(uname)\" = \"Darwin\" ] && ZED_EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java/proxy-bin\"; PROXY_BIN=$(find \"$ZED_EXT\" -type f -name \"java-lsp-proxy*\" | sort | tail -n 1); $PROXY_BIN task run-class \"$ZED_FILE\" \"$ZED_CUSTOM_java_package_name\" \"$ZED_CUSTOM_java_class_name\" \"${ZED_CUSTOM_java_outer_class_name:-}\"",
"tags": ["java-main"],
"shell": { "with_arguments": { "program": "/bin/sh", "args": ["-c"] } }
}
Benefits
| Aspect |
Before (Shell) |
After (Rust) |
| Build tool detection |
Duplicated shell loops |
Single Rust implementation |
| Module resolution |
Duplicated shell loops |
Single Rust implementation |
| Command construction |
Hard to read shell |
Type-safe Rust structs |
| Testing |
Manual shell testing |
Unit tests in Rust |
| Cross-platform |
Shell quirks |
Rust handles platforms |
| Maintainability |
5 nearly-identical scripts |
1 Rust module |
After I missed the feature of running Java tasks in multi modules java projects I have enhanced the
tasks.jsoncommands (Heavy Shell script in a single line). (see #244)So I have make up my minds and would suggest to move the logic from
tasks.jsonto Rust code.So my design Idee is:
1. Extend java-lsp-proxy with Task Subcommands
Add a new module to the proxy (proxy/src/task.rs) that provides CLI subcommands:
Each command outputs JSON to stdout with the resolved command + arguments:
{ "command": "/usr/bin/mvn", "args": ["clean", "test", "-pl", "backend", "-Dtest=com.example.MyTest"], "cwd": "/path/to/workspace" }2. Main Module Structure (proxy/src/task.rs)
3. Each Task Handler
run-class (replaces the java-main tag):
run-test-method (replaces java-test-method / java-test-method-nested) run-all-tests: Same pattern without class/method filtering.:
clear-cache:
4. Wire into main.rs
5. Simplified tasks.json
Current (complex shell):
After (thin wrapper):
{ "label": "Run $ZED_CUSTOM_java_class_name", "command": "ZED_EXT=\"${XDG_DATA_HOME:-$HOME/.local/share}/zed/extensions/work/java/proxy-bin\"; [ \"$(uname)\" = \"Darwin\" ] && ZED_EXT=\"$HOME/Library/Application Support/Zed/extensions/work/java/proxy-bin\"; PROXY_BIN=$(find \"$ZED_EXT\" -type f -name \"java-lsp-proxy*\" | sort | tail -n 1); $PROXY_BIN task run-class \"$ZED_FILE\" \"$ZED_CUSTOM_java_package_name\" \"$ZED_CUSTOM_java_class_name\" \"${ZED_CUSTOM_java_outer_class_name:-}\"", "tags": ["java-main"], "shell": { "with_arguments": { "program": "/bin/sh", "args": ["-c"] } } }Benefits