Skip to content

Replace shell-based task commands with Rust proxy subcommands #252

Description

@drmaniac

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions