Skip to content

feat: Support multi-module Java projects in tasks#244

Open
drmaniac wants to merge 3 commits into
zed-extensions:mainfrom
drmaniac:multi-module-support
Open

feat: Support multi-module Java projects in tasks#244
drmaniac wants to merge 3 commits into
zed-extensions:mainfrom
drmaniac:multi-module-support

Conversation

@drmaniac
Copy link
Copy Markdown

@drmaniac drmaniac commented May 8, 2026

Previously, Zed's Java tasks for running and testing applications did not correctly support multi-module Maven or Gradle projects. When executing a task from a file within a submodule, the commands would attempt to run at the root level, potentially leading to incorrect execution or failures.

This change introduces logic to dynamically determine the nearest Maven pom.xml or Gradle build.gradle/settings.gradle file relative to the currently active file. It then modifies the Maven and Gradle commands to target the specific module.

For Maven, this involves adding the -pl <module> and -am flags. For Gradle, it prepends the module path (e.g., :module-name:) to the run or test command. This ensures that tasks are executed within the context of the correct module, improving the developer experience for multi-module Java projects.

@cla-bot cla-bot Bot added the cla-signed label May 8, 2026
@tartarughina
Copy link
Copy Markdown
Collaborator

tartarughina commented May 8, 2026

Thanks for the PR, could you provide steps to test these changes or add how you tested them?

@drmaniac drmaniac force-pushed the multi-module-support branch from 5b53433 to 0e45c2c Compare May 8, 2026 20:57
Previously, Zed's Java tasks for running and testing applications
did not correctly support multi-module Maven or Gradle projects.
When executing a task from a file within a submodule, the commands
would attempt to run at the root level, potentially leading to
incorrect execution or failures.

This change introduces logic to dynamically determine the nearest
Maven `pom.xml` or Gradle `build.gradle`/`settings.gradle` file
relative to the currently active file. It then modifies the Maven
and Gradle commands to target the specific module.

For Maven, this involves adding the `-pl <module>` and `-am` flags.
For Gradle, it prepends the module path (e.g., `:module-name:`) to
the `run` or `test` command. This ensures that tasks are executed
within the context of the correct module, improving the developer
experience for multi-module Java projects.
@drmaniac drmaniac force-pushed the multi-module-support branch from 0e45c2c to bb03226 Compare May 8, 2026 21:06
@drmaniac
Copy link
Copy Markdown
Author

drmaniac commented May 8, 2026

Hi, I have added integration tests to this project (I wasn't sure if these are allowed/expected for extensions).

In addition, I apologize that the original PR had an issue with the Maven part; I have fixed it.

I also tested the changes in tasks.json by adding them as custom tasks in my local Zed config. I tried it out on a complex Gradle project with composite and multiple modules, and a Maven project with multiple modules.

Copy link
Copy Markdown
Collaborator

@tartarughina tartarughina May 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never thought of creating a temp project like you are doing here. This is a really nice idea to make testing more robust. I would although expand the temp projects to cover all instances covered by our tasks, for example nesting and single module project

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added more tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Some are missing still though:

  • java-test-all for Maven
  • java-test-class

Let's include them as well

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, as the tests are growing, I have refactored the integration tests. I introduced a builder pattern for the TestProject and TaskRunner so the test methods can focus on what to test instead of setting up the test.
I hope I didn't miss testing any branches!

Comment thread tests/task_verification_test.rs Outdated
Comment thread languages/java/tasks.json Outdated
{
"label": "Run $ZED_CUSTOM_java_class_name",
"command": "pkg=\"${ZED_CUSTOM_java_package_name:}\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; if [ -f pom.xml ]; then [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; $CMD clean compile exec:java -Dexec.mainClass=\"$c\"; elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;",
"command": "pkg=\"$ZED_CUSTOM_java_package_name\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; if [ \"$m\" = \".\" ]; then $CMD clean test-compile exec:java -Dexec.mainClass=\"$c\" -Dexec.classpathScope=test; else $CMD clean test-compile -pl \"$m\" -am && $CMD exec:java -pl \"$m\" -Dexec.mainClass=\"$c\" -Dexec.classpathScope=test; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using test-compile for running the main class alongside providing the tests' scope?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch to test-compile and the test classpath scope increases flexibility. While the previous version only supported classes in src/main/java, this update allows users to run main methods or classes located within the test directory as well.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point but I've rarely seen such situation where a main method is located in the test directory.

If we are going to keep this, let's at least compile-test only when effectively in such situation. If I'm testing my main method I would not love getting slowed down by compiling the tests as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we definitely want to avoid that slowdown. I've added a check so it strictly defaults to compile for normal files, and only falls back to test-compile if you are running a file that is specifically inside the src/test/ directory.

Comment thread languages/java/tasks.json
{
"label": "Run tests",
"command": "if [ -f pom.xml ]; then [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; $CMD clean test; elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD test; else >&2 echo 'No build tool found'; exit 1; fi;",
"command": "f=\"$ZED_FILE\"; p=\"$PWD\"; d=$(dirname \"${f#$p/}\"); if [ -f pom.xml ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/pom.xml\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; [ -f ./mvnw ] && CMD=\"./mvnw\" || CMD=\"mvn\"; if [ \"$m\" = \".\" ]; then $CMD clean test; else $CMD clean test-compile -pl \"$m\" -am && $CMD test -pl \"$m\"; fi; elif [ -f build.gradle ] || [ -f build.gradle.kts ] || [ -f settings.gradle ] || [ -f settings.gradle.kts ]; then m=\".\"; md=\"$d\"; while [ \"$md\" != \".\" ] && [ \"$md\" != \"/\" ]; do if [ -f \"$md/build.gradle\" ] || [ -f \"$md/build.gradle.kts\" ]; then m=\"$md\"; break; fi; md=$(dirname \"$md\"); done; if [ \"$m\" = \".\" ]; then mp=\"\"; else mp=\":$(echo \"$m\" | tr '/' ':')\"; fi; [ -f ./gradlew ] && CMD=\"./gradlew\" || CMD=\"gradle\"; $CMD ${mp}:test; else >&2 echo 'No build tool found'; exit 1; fi;",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we first compiling the test to the run it for maven? Isn't that redundant?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a workaround for Maven multi-module limitations. Since one module's tests may depend on another's, we must force a full build (test-compile -am) first. This ensures all dependencies are ready without wasting time running tests for every sub-module, which Maven (unlike Gradle) can't handle in one step.

drmaniac added 2 commits May 11, 2026 10:01
Introduces a suite of integration tests for Java task commands across
various project configurations and build tools. This includes:

- Added  to tasks in  to allow
  programmatic identification and testing of commands.
- Refactored  to support flexible module paths.
- Introduced  to retrieve task commands
  robustly by their tags.
- Added new tests for Maven projects covering single, multi-module,
  and nested module execution, and nested class test methods.
- Added new tests for Gradle projects covering single, multi-module,
  and nested module execution, and running all tests.
Update the Java task runner to detect if the current file is inside a
test directory ('src/test/'). Depending on the file location, Maven
will now dynamically select 'test-compile' and 'test' classpath scope
or 'compile' and 'runtime' scope. This ensures that main classes are
executed with the correct dependencies and build targets.

Additionally, the Rust test suite has been heavily refactored. It now
utilizes a builder pattern (TestProject and TaskRunner) to simplify
setup, improve readability, and add test coverage for both runtime
and test execution paths across Maven and Gradle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants