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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

### Bugs fixed

- [#2195](https://github.com/bbatsov/projectile/pull/2195): `projectile-dispatch` can be extended with `transient-insert-suffix`/`transient-append-suffix` as soon as `transient` is loaded, instead of failing with "Not a transient prefix command" until the menu had been opened once ([#2194](https://github.com/bbatsov/projectile/discussions/2194)).
- [#2191](https://github.com/bbatsov/projectile/pull/2191): The test suite passes under native compilation with an empty eln cache, as in distro package builds, instead of failing two dirconfig specs with `native-lisp-load-failed` ([#2100](https://github.com/bbatsov/projectile/issues/2100)).
- [#2190](https://github.com/bbatsov/projectile/pull/2190): Alien indexing no longer hands git exclude pathspecs to an `fd` whose `projectile-git-fd-args` lacks `--strip-cwd-prefix`, which made it fail with `No valid search paths given` ([#2187](https://github.com/bbatsov/projectile/issues/2187)).
- [#2183](https://github.com/bbatsov/projectile/pull/2183): `projectile-run-test-at-point` now builds a usable file path when the project is reached through a symlink (anything under `/tmp` on macOS, or a symlinked `~/src`), instead of one that climbs out of the project and gets rejected by the test runner.
Expand Down
44 changes: 42 additions & 2 deletions doc/modules/ROOT/pages/upgrading_to_projectile_3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,48 @@ The single-key "Commander" (`projectile-commander` and
`projectile-dispatch`, a `transient` menu bound to kbd:[s-p m]. A prefix
argument on project switch (kbd:[C-u s-p p]) also opens it.

If you defined your own commander methods, add them to the dispatch transient
instead (see `transient-append-suffix` and the `projectile-dispatch` definition).
If you defined your own commander methods, add them to the dispatch menu
instead. Say you had this:

[source,elisp]
----
(def-projectile-commander-method ?\C-m
"Find file in project."
(call-interactively #'find-file))
----

The dispatch version is:

[source,elisp]
----
(with-eval-after-load 'projectile
(require 'transient)
(transient-insert-suffix 'projectile-dispatch "f"
'("RET" "find-file" find-file)))
----

`projectile-dispatch` is a transient prefix, so the standard
`transient-insert-suffix` and `transient-append-suffix` work on it. The second
argument says where the new entry goes: the key of an existing entry (`"f"`
here, the menu's "file" entry). `transient-insert-suffix` puts it before that
entry and `transient-append-suffix` after it. A commander method's body becomes
a command of its own:

[source,elisp]
----
(defun my-projectile-magit-log ()
"Show the log of the current project."
(interactive)
(magit-log-head))

(with-eval-after-load 'projectile
(require 'transient)
(transient-append-suffix 'projectile-dispatch "v"
'("L" "magit log" my-projectile-magit-log)))
----

See xref:usage.adoc#extending-the-dispatch-menu[Extending the dispatch menu]
for more on how this works.

=== The idle timer

Expand Down
31 changes: 31 additions & 0 deletions doc/modules/ROOT/pages/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,37 @@ NOTE: `projectile-dispatch` is powered by `transient`, which is bundled with
Emacs 28.1+ (Projectile's minimum), so the menu is always available. It's bound
to kbd:[s-p m], and kbd:[C-u s-p p] opens it when switching projects.

=== Extending the dispatch menu

`projectile-dispatch` is an ordinary transient prefix, so you can add your own
entries with `transient-insert-suffix` (before an existing entry) or
`transient-append-suffix` (after one), naming the existing entry by its key:

[source,elisp]
----
(with-eval-after-load 'projectile
(require 'transient)
;; RET runs a plain `find-file', listed before the "file" entry (f)
(transient-insert-suffix 'projectile-dispatch "f"
'("RET" "find-file" find-file)))
----

The suffix is any interactive command. It runs with the project current, so
`default-directory` is the project root when you open the menu from project
switching (kbd:[C-u s-p p]).

The `(require 'transient)` is there because Projectile defers loading
`transient` until the menu is first opened, and the menu only exists once
`transient` is loaded. If you'd rather keep `transient` out of your startup, put
the customization in `(with-eval-after-load 'transient ...)` inside the
`with-eval-after-load 'projectile` form instead; it then runs whenever
something first loads `transient`.

NOTE: Before Projectile 3.5 loading `transient` wasn't enough; the menu was only
defined on its first use. On 3.1 through 3.4 call
`(projectile--dispatch-define)` after `(require 'transient)` and before
changing the menu.

== Finding changed files

`projectile-find-changed-file` (kbd:[s-p C]) completes over the files you have
Expand Down
27 changes: 18 additions & 9 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -17430,10 +17430,12 @@ Like `projectile-dispatch-search-review', but for the replace reviewer."
;; The menu keys deliberately match the `projectile-command-map' bindings.
;; The transient prefix is defined lazily: loading `transient' costs a few
;; milliseconds and some memory for every session, while the menu is only
;; needed once invoked. `projectile-dispatch' below is a stub that loads
;; `transient', evaluates the real definition (replacing itself), and
;; re-invokes it; `transient' is required at compile time (see the top of
;; the file) so the macro still expands during byte-compilation.
;; needed once invoked. `projectile-dispatch' below is a stub; the real
;; definition replaces it as soon as `transient' is loaded, whether by the
;; stub itself or by anything else, so user code can extend the menu with
;; `transient-append-suffix' right after `(require 'transient)'.
;; `transient' is required at compile time (see the top of the file) so the
;; macro still expands during byte-compilation.
(defun projectile--dispatch-define ()
"Define the `projectile-dispatch' transient prefix, replacing the stub."
(transient-define-prefix projectile-dispatch ()
Expand Down Expand Up @@ -17562,17 +17564,24 @@ search/replace case-sensitive, `--word' makes it match whole words,
(shells), and `--display' opens the result in another window or frame
(file/buffer/project commands)."
(interactive)
;; Loading `transient' is deferred until the menu is first used; this
;; stub is replaced by the real transient prefix on that first call.
;; Loading `transient' is deferred until the menu is first used. Loading
;; it defines the real menu (see the `with-eval-after-load' below); the
;; stub's marker property still being there means that didn't happen.
(require 'transient)
(projectile--dispatch-define)
(when (eq (get 'projectile-dispatch 'transient--prefix) t)
(projectile--dispatch-define))
(call-interactively 'projectile-dispatch))

;; Mark the stub as a transient prefix so `projectile--transient-command-p'
;; recognizes it before the first invocation replaces the stub (and this
;; property) with the real definition.
;; recognizes it before the real definition replaces the stub (and this
;; property).
(put 'projectile-dispatch 'transient--prefix t)

;; Replace the stub once `transient' is loaded. This must come after the
;; stub: it runs immediately when `transient' is already loaded.
(with-eval-after-load 'transient
(projectile--dispatch-define))

(defvar projectile-mode-map
(let ((map (make-sparse-keymap)))
(when projectile-keymap-prefix
Expand Down
19 changes: 19 additions & 0 deletions test/projectile-dispatch-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,25 @@
(expect (plist-get captured :case-fold) :to-be t)
(expect (plist-get captured :word) :to-be nil)))

(describe "projectile-dispatch definition"
(it "is the real transient prefix once transient is loaded"
(require 'transient)
(expect (transient-prefix-p (get 'projectile-dispatch 'transient--prefix))
:to-be-truthy))

(it "can be extended with transient-insert-suffix before its first use"
(require 'transient)
(unwind-protect
(progn
(transient-insert-suffix 'projectile-dispatch "f"
'("RET" "find-file" find-file))
;; The layout entry's shape varies across transient versions,
;; but looking up a missing key signals an error.
(expect (transient-get-suffix 'projectile-dispatch "RET")
:to-be-truthy))
;; Put the stock layout back for the specs that follow.
(projectile--dispatch-define))))

(provide 'projectile-dispatch-test)

;;; projectile-dispatch-test.el ends here
Loading