diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fccf31d..fc1c68f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## master (unreleased) +### Bugs fixed + +- [#2157](https://github.com/bbatsov/projectile/pull/2157): `projectile-use-comint-mode` now covers the named tasks run by `projectile-run-task`, which were always given a read-only compilation buffer however it was set - so a task that needs to ask for a sudo password had nowhere to type one ([#2156](https://github.com/bbatsov/projectile/issues/2156)). Name `task` in the list, or set the option to `t`. + ## 3.4.0 (2026-08-10) ### New features diff --git a/doc/modules/ROOT/pages/configuration_index.adoc b/doc/modules/ROOT/pages/configuration_index.adoc index dbc9ef5c8..8cf4c42b6 100644 --- a/doc/modules/ROOT/pages/configuration_index.adoc +++ b/doc/modules/ROOT/pages/configuration_index.adoc @@ -372,7 +372,7 @@ and the other reference pages. | Controls whether Projectile will automatically register known projects. | `projectile-use-comint-mode` -| Which lifecycle commands get an interactive output buffer. +| Which of the commands Projectile runs get an interactive output buffer. | `projectile-use-git-grep` | Whether ‘projectile-grep’ delegates to ‘vc-git-grep’ in git projects. diff --git a/doc/modules/ROOT/pages/projects.adoc b/doc/modules/ROOT/pages/projects.adoc index 2eba2e264..a0c9752fe 100644 --- a/doc/modules/ROOT/pages/projects.adoc +++ b/doc/modules/ROOT/pages/projects.adoc @@ -1259,20 +1259,25 @@ your project, you could customize it with the following: By default, compilation buffers are not writable, which allows you to e.g. press `g` to restart the last command. `projectile-use-comint-mode` makes them interactive instead, letting you e.g. test a command-line -program with `projectile-run-project`. Set it to `t` for every lifecycle -command, or to a list of the ones you want: +program with `projectile-run-project`, or answer a task's sudo prompt. +Set it to `t` for everything Projectile runs, or to a list of the ones +you want: [source,elisp] ---- ;; only the compile buffer is interactive (setq projectile-use-comint-mode '(compile)) -;; all of them are +;; the named tasks are too +(setq projectile-use-comint-mode '(compile task)) + +;; everything is (setq projectile-use-comint-mode t) ---- -The phases you can name are `configure`, `compile`, `test`, `install`, -`package` and `run`. +You can name the lifecycle phases - `configure`, `compile`, `test`, +`install`, `package` and `run` - and `task` for the named tasks run by +`projectile-run-task`. == Project buffers diff --git a/projectile.el b/projectile.el index 7386facd2..8a99079ce 100644 --- a/projectile.el +++ b/projectile.el @@ -12448,26 +12448,28 @@ The command actually run is returned." command)) (defcustom projectile-use-comint-mode nil - "Which lifecycle commands get an interactive output buffer. + "Which of the commands Projectile runs get an interactive output buffer. -The lifecycle commands report through `compilation-mode', which is -read-only. For a command covered here Projectile uses `comint-mode' -instead, so a build that asks a question, or a test runner that drops -into a debugger, can be typed at. +Projectile reports through `compilation-mode', which is read-only. For a +command covered here it uses `comint-mode' instead, so a build that asks a +question, a test runner that drops into a debugger, or a task that wants a +sudo password can be typed at. -The value is nil (no command is interactive), t (all of them), or a -list naming the ones that are - `configure', `compile', `test', -`install', `package' and `run'." +The value is nil (nothing is interactive), t (everything is), or a list +naming what is - the lifecycle phases `configure', `compile', `test', +`install', `package' and `run', and `task' for the named tasks run by +`projectile-run-task'." :group 'projectile - :type '(choice (const :tag "No command" nil) - (const :tag "Every command" t) + :type '(choice (const :tag "Nothing" nil) + (const :tag "Everything" t) (set :tag "Selected commands" (const :tag "Configure" configure) (const :tag "Compile" compile) (const :tag "Test" test) (const :tag "Install" install) (const :tag "Package" package) - (const :tag "Run" run))) + (const :tag "Run" run) + (const :tag "Tasks" task))) :package-version '(projectile . "3.4.0")) ;; Remove in 4.0, this block and the fallback in @@ -12501,13 +12503,19 @@ list naming the ones that are - `configure', `compile', `test', (defun projectile-use-comint-mode-p (phase) "Return non-nil when PHASE's output buffer should be interactive. -PHASE is a lifecycle phase symbol such as `compile'. Reads -`projectile-use-comint-mode', falling back to the obsolete per-phase -option it replaced for a configuration that still sets one." - (or (eq projectile-use-comint-mode t) - (memq phase projectile-use-comint-mode) - (when-let* ((var (alist-get phase projectile--obsolete-comint-vars))) - (symbol-value var)))) +PHASE is a lifecycle phase symbol such as `compile', or `task' for the +named tasks - which run through the same machinery and are covered by the +same option (see issue #2156). Reads `projectile-use-comint-mode', +falling back to the obsolete per-phase option it replaced for a +configuration that still sets one; the tasks never had one of those." + ;; Normalized to a boolean: `memq' would otherwise hand the caller the + ;; tail of the option's list, which then travels all the way into + ;; `compile' as its COMINT argument. + (and (or (eq projectile-use-comint-mode t) + (memq phase projectile-use-comint-mode) + (when-let* ((var (alist-get phase projectile--obsolete-comint-vars))) + (symbol-value var))) + t)) (defun projectile--phase-command-dynamic-p (phase) "Non-nil when PHASE's command comes from a function for the current project. @@ -13145,6 +13153,7 @@ the `%p' placeholder still intact." (concat "<" (projectile-project-name project-root) ">"))))) (projectile--run-project-cmd command nil :save-buffers t + :use-comint-mode (projectile-use-comint-mode-p 'task) :buffer-name-function (lambda (_mode) buffer-name))) ;; `command-map' is nil above, so `projectile--run-project-cmd' records ;; nothing; record the command - before `%p' expansion, like the other diff --git a/test/projectile-commands-test.el b/test/projectile-commands-test.el index 8dbfd5bc7..890bcf877 100644 --- a/test/projectile-commands-test.el +++ b/test/projectile-commands-test.el @@ -1061,6 +1061,19 @@ (let ((projectile-use-comint-mode nil) (projectile-test-use-comint-mode t)) (expect (projectile-use-comint-mode-p 'test) :to-be-truthy) - (expect (projectile-use-comint-mode-p 'compile) :to-be nil)))) + (expect (projectile-use-comint-mode-p 'compile) :to-be nil))) + + (it "covers the named tasks too" + ;; Tasks run through the same machinery as the lifecycle phases, so + ;; `t' has to mean them as well (issue #2156). + (let ((projectile-use-comint-mode t)) + (expect (projectile-use-comint-mode-p 'task) :to-be-truthy)) + (let ((projectile-use-comint-mode '(task))) + (expect (projectile-use-comint-mode-p 'task) :to-be-truthy) + (expect (projectile-use-comint-mode-p 'compile) :to-be nil)) + (let ((projectile-use-comint-mode '(compile))) + (expect (projectile-use-comint-mode-p 'task) :to-be nil)) + (let ((projectile-use-comint-mode nil)) + (expect (projectile-use-comint-mode-p 'task) :to-be nil)))) ;;; projectile-commands-test.el ends here diff --git a/test/projectile-tasks-test.el b/test/projectile-tasks-test.el index 69ee42f7a..4b5d65d6b 100644 --- a/test/projectile-tasks-test.el +++ b/test/projectile-tasks-test.el @@ -388,6 +388,42 @@ main.o: main.c (expect (ring-elements (projectile--get-command-history "/proj/")) :to-equal '("make lint")))) + (it "gives a task an interactive buffer when comint mode covers everything" + ;; A task can be something like a sudo rebuild, which is unusable in a + ;; read-only compilation buffer - there's nowhere to type the password + ;; (issue #2156). + (spy-on 'projectile-run-compilation) + (spy-on 'projectile-completing-read :and-return-value "rebuild") + (let ((projectile-use-comint-mode t) + (projectile-project-command-history (make-hash-table :test 'equal)) + (projectile-last-task-map (make-hash-table :test 'equal)) + (projectile-tasks '(("rebuild" . "sudo make install")))) + (projectile-run-task nil) + (expect 'projectile-run-compilation + :to-have-been-called-with "sudo make install" t))) + + (it "gives a task an interactive buffer when the list names `task'" + (spy-on 'projectile-run-compilation) + (spy-on 'projectile-completing-read :and-return-value "rebuild") + (let ((projectile-use-comint-mode '(task)) + (projectile-project-command-history (make-hash-table :test 'equal)) + (projectile-last-task-map (make-hash-table :test 'equal)) + (projectile-tasks '(("rebuild" . "sudo make install")))) + (projectile-run-task nil) + (expect 'projectile-run-compilation + :to-have-been-called-with "sudo make install" t))) + + (it "leaves a task alone when the list names only lifecycle phases" + (spy-on 'projectile-run-compilation) + (spy-on 'projectile-completing-read :and-return-value "rebuild") + (let ((projectile-use-comint-mode '(compile test)) + (projectile-project-command-history (make-hash-table :test 'equal)) + (projectile-last-task-map (make-hash-table :test 'equal)) + (projectile-tasks '(("rebuild" . "sudo make install")))) + (projectile-run-task nil) + (expect 'projectile-run-compilation + :to-have-been-called-with "sudo make install" nil))) + (it "confirms the command before running by default" ;; Task commands can come from a checked-out .dir-locals.el, so the ;; run-time confirmation (like compile's) is a security requirement,