Skip to content

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

@drmaniac

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions