From 8b53767bacc98e393a2d6790b0505e5acc1e0c71 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 16 Feb 2026 10:11:10 +0200 Subject: [PATCH] Add subproject compile and test commands (#1653) Add `projectile-compile-subproject` and `projectile-test-subproject` for building/testing individual modules in multi-module projects (e.g. Maven, Gradle). These commands find the nearest build file (project-file marker) between the current directory and the project root, then run the compile/test command in that directory. Keybindings: `c m c` (compile subproject), `c m t` (test subproject). --- CHANGELOG.md | 1 + projectile.el | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60812291..bdabbc2de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#1964](https://github.com/bbatsov/projectile/issues/1964): Implement `project-name` and `project-buffers` methods for the `project.el` integration, so that code using `project.el` APIs returns correct results for Projectile-managed projects. * [#1837](https://github.com/bbatsov/projectile/issues/1837): Add `eat` project terminal commands with keybindings `x x` and `x 4 x`. * Add keybinding `A` (in the projectile command map) and a menu entry for `projectile-add-known-project`. +* [#1653](https://github.com/bbatsov/projectile/issues/1653): Add `projectile-compile-subproject` and `projectile-test-subproject` commands for building/testing individual modules in multi-module projects (e.g. Maven, Gradle). Bound to `c m c` and `c m t`. ### Bugs fixed diff --git a/projectile.el b/projectile.el index f6b7e6356..73df351f0 100644 --- a/projectile.el +++ b/projectile.el @@ -5372,6 +5372,33 @@ project of that type" '(compile-history . 1) 'compile-history)))) +(defun projectile-subproject-root () + "Find the root of the nearest subproject containing the current file. +Walk up from `default-directory' looking for the project type's +`:project-file' marker, stopping at the project root. Returns the +directory containing the nearest marker, or signals an error if no +subproject is found between the current directory and the project root." + (let* ((project-root (projectile-acquire-root)) + (type (projectile-project-type project-root)) + (project-file (projectile-project-type-attribute type 'project-file)) + (markers (if (listp project-file) project-file (list project-file))) + (dir (file-name-directory (or (buffer-file-name) default-directory))) + (result nil)) + (unless markers + (user-error "Project type `%s' has no project-file defined" type)) + ;; Walk up from current directory, stop at (but include) project root. + (while (and (not result) + dir + (string-prefix-p project-root dir)) + (when (seq-some (lambda (m) + (file-exists-p (expand-file-name m dir))) + markers) + (setq result dir)) + (let ((parent (file-name-directory (directory-file-name dir)))) + (setq dir (unless (string= parent dir) parent)))) + (or result + (user-error "No subproject found between current directory and project root")))) + (defun projectile-compilation-dir () "Retrieve the compilation directory for this project." (let* ((project-root (projectile-acquire-root)) @@ -5541,6 +5568,54 @@ with a prefix ARG." :save-buffers t :use-comint-mode projectile-test-use-comint-mode))) +;;;###autoload +(defun projectile-compile-subproject (arg) + "Run compilation in the nearest subproject. +Find the closest build file (e.g. pom.xml, build.gradle) between the +current directory and the project root, then run the project's compile +command there. This is useful for multi-module projects where building +a single module is faster than building the entire project. + +Normally you'll be prompted for a compilation command, unless +variable `compilation-read-command'. You can force the prompt +with a prefix ARG." + (interactive "P") + (let* ((subproject-root (projectile-subproject-root)) + (project-root (projectile-acquire-root)) + (projectile-project-compilation-dir + (file-relative-name subproject-root project-root)) + (command (projectile-compilation-command (projectile-compilation-dir))) + (command-map (if (projectile--cache-project-commands-p) projectile-compilation-cmd-map))) + (projectile--run-project-cmd command command-map + :show-prompt arg + :prompt-prefix "Compile subproject command: " + :save-buffers t + :use-comint-mode projectile-compile-use-comint-mode))) + +;;;###autoload +(defun projectile-test-subproject (arg) + "Run tests in the nearest subproject. +Find the closest build file (e.g. pom.xml, build.gradle) between the +current directory and the project root, then run the project's test +command there. This is useful for multi-module projects where testing +a single module is faster than testing the entire project. + +Normally you'll be prompted for a compilation command, unless +variable `compilation-read-command'. You can force the prompt +with a prefix ARG." + (interactive "P") + (let* ((subproject-root (projectile-subproject-root)) + (project-root (projectile-acquire-root)) + (projectile-project-compilation-dir + (file-relative-name subproject-root project-root)) + (command (projectile-test-command (projectile-compilation-dir))) + (command-map (if (projectile--cache-project-commands-p) projectile-test-cmd-map))) + (projectile--run-project-cmd command command-map + :show-prompt arg + :prompt-prefix "Test subproject command: " + :save-buffers t + :use-comint-mode projectile-test-use-comint-mode))) + ;;;###autoload (defun projectile-install-project (arg) "Run project install command. @@ -6317,6 +6392,8 @@ Magit that don't trigger `find-file-hook'." (define-key map (kbd "c i") #'projectile-install-project) (define-key map (kbd "c t") #'projectile-test-project) (define-key map (kbd "c r") #'projectile-run-project) + (define-key map (kbd "c m c") #'projectile-compile-subproject) + (define-key map (kbd "c m t") #'projectile-test-subproject) ;; TODO: Legacy keybindings that will be removed in Projectile 3 (define-key map (kbd "C") #'projectile-configure-project) (define-key map (kbd "K") #'projectile-package-project)