Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/configuration_index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions doc/modules/ROOT/pages/projects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 27 additions & 18 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion test/projectile-commands-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions test/projectile-tasks-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading