Skip to content

Commit 4e3000b

Browse files
committed
Follow the family into buffers and annotations
Files and searches already work across related projects; buffers and TODOs are the other two things you want when a library and its caller are separate repositories. s-p n b switches to a buffer open in any of them, s-p n o runs multi-occur over those buffers, and s-p n t gathers the family's annotations into the search reviewer. `projectile-project-buffers' already took a project, so the group form is a mapcan over it, de-duplicated because two members can nest. Todos needed a little more: its body now takes a list of projects, and `projectile-todos' is the single-project call of it, the way `projectile-search-review' is for search.
1 parent a13ad93 commit 4e3000b

5 files changed

Lines changed: 166 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- [#2160](https://github.com/bbatsov/projectile/pull/2160): Add `projectile-find-file-in-sibling-projects` (`s-p n f`) and `projectile-search-in-sibling-projects` (`s-p n s`), which work across the family of related projects rather than just the one you're in.
1212
- Both are built on `projectile-find-file-in-projects` and `projectile-search-in-projects`, which take any list of projects, so a command for a group of your own is a two-line wrapper.
1313
- A group search puts every match in one `*projectile-search*` buffer, named relative to the directory containing the group, so each one is labelled with the project it came from.
14+
- [#2163](https://github.com/bbatsov/projectile/pull/2163): Add `projectile-switch-to-buffer-in-sibling-projects` (`s-p n b`), `projectile-multi-occur-in-sibling-projects` (`s-p n o`) and `projectile-todos-in-sibling-projects` (`s-p n t`), so buffers and annotations follow the same family as files and searches.
15+
- `projectile-switch-to-buffer-in-projects` joins the two existing generic commands, and `projectile-todos` now shares one implementation with its group form.
1416

1517
### Changes
1618

doc/modules/ROOT/pages/across_repositories.adoc

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,16 @@ directories is the right one.
2626
| kbd:[s-p n p] | `projectile-switch-sibling-project`
2727
| kbd:[s-p n f] | `projectile-find-file-in-sibling-projects`
2828
| kbd:[s-p n s] | `projectile-search-in-sibling-projects`
29+
| kbd:[s-p n b] | `projectile-switch-to-buffer-in-sibling-projects`
30+
| kbd:[s-p n o] | `projectile-multi-occur-in-sibling-projects`
31+
| kbd:[s-p n t] | `projectile-todos-in-sibling-projects`
2932
|===
3033

34+
kbd:[s-p n] is the prefix for everything that works across related
35+
projects, and each key under it is the project-wide one a level down -
36+
kbd:[s-p f] finds a file here, kbd:[s-p n f] finds one anywhere in the
37+
family.
38+
3139
The two switch commands honour `projectile-switch-project-action`, and both
3240
take a prefix argument to run `projectile-dispatch` instead, exactly like the
3341
other switch commands.
@@ -221,6 +229,16 @@ commands work on the whole group at once, the current project included:
221229
* kbd:[s-p n s] (`projectile-search-in-sibling-projects`) searches them all
222230
and collects the matches in one `*projectile-search*` buffer. A prefix
223231
argument makes the term an Emacs regexp.
232+
* kbd:[s-p n t] (`projectile-todos-in-sibling-projects`) gathers the
233+
family's `TODO`/`FIXME` annotations into that same buffer.
234+
* kbd:[s-p n b] (`projectile-switch-to-buffer-in-sibling-projects`)
235+
completes over the buffers you have open in any of them, so you can
236+
jump between a library and its caller without caring which project a
237+
buffer belongs to.
238+
* kbd:[s-p n o] (`projectile-multi-occur-in-sibling-projects`) runs
239+
`multi-occur` over those same buffers. Note this searches what you have
240+
open, not the projects on disk - kbd:[s-p n s] is the one that reads
241+
files.
224242

225243
The search results are named relative to the innermost directory holding
226244
the group, so each match says which project it came from:
@@ -238,8 +256,10 @@ re-search, exporting to `grep-mode`, and kbd:[r] to turn it into a
238256
reviewable replacement across all of them. See
239257
xref:usage.adoc[the search and replace reviewers].
240258

241-
Both commands take a plain list of projects underneath, so a command for
242-
a group of your own is a two-line wrapper:
259+
These all take a plain list of projects underneath -
260+
`projectile-find-file-in-projects`, `projectile-search-in-projects` and
261+
`projectile-switch-to-buffer-in-projects` - so a command for a group of
262+
your own is a two-line wrapper:
243263

244264
[source,elisp]
245265
----
@@ -250,7 +270,9 @@ a group of your own is a two-line wrapper:
250270
"Find file in infra: "))
251271
----
252272

253-
`projectile-search-in-projects` is the same shape for searching.
273+
`projectile-search-in-projects` and
274+
`projectile-switch-to-buffer-in-projects` are the same shape for
275+
searching and for buffers.
254276

255277
NOTE: A group search skips the ripgrep fast-path, which runs one `rg` over
256278
one directory tree, so it's slower than searching a single project. Each

doc/modules/ROOT/pages/cheatsheet.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ Here's a list of the interactive Emacs Lisp functions, provided by Projectile:
8888
| kbd:[s-p n s]
8989
| Search the current project and the ones related to it, collecting every match in one reviewable buffer. With a prefix argument the term is an Emacs regexp (see xref:across_repositories.adoc[Working across repositories]).
9090

91+
| kbd:[s-p n t]
92+
| Collect the `TODO`/`FIXME`-style annotations of the current project and the ones related to it into the reviewable search buffer.
93+
94+
| kbd:[s-p n b]
95+
| Switch to a buffer open in the current project or in any related to it.
96+
97+
| kbd:[s-p n o]
98+
| Do a `multi-occur` in the buffers open in the current project and the ones related to it. With a prefix argument, show that many lines of context.
99+
91100
| kbd:[s-p v]
92101
| Run `vc-dir` on the root directory of the project.
93102

projectile.el

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11296,7 +11296,15 @@ in the results buffer. The keyword does not have to sit in a comment.
1129611296
With a prefix argument ARG, prompt for which keywords to search for
1129711297
instead of using all of them."
1129811298
(interactive "P")
11299-
(let* ((root (projectile-acquire-root))
11299+
(projectile--todos (list (projectile-acquire-root)) arg))
11300+
11301+
(defun projectile--todos (projects &optional arg)
11302+
"Collect the TODO-style annotations of PROJECTS into the search reviewer.
11303+
With ARG non-nil, prompt for which keywords to search for. PROJECTS is a
11304+
list of project roots, so one project and a whole group take the same
11305+
path; see `projectile-search-in-projects' for what changes when there is
11306+
more than one."
11307+
(let* ((projects (projectile--project-group projects "projects"))
1130011308
(keywords (or (if arg
1130111309
(projectile-todos--read-keywords)
1130211310
projectile-todo-keywords)
@@ -11305,15 +11313,16 @@ instead of using all of them."
1130511313
(rg-pattern (projectile-todos--rg-pattern keywords))
1130611314
;; the term IS the regexp, so the in-buffer toggles keep working
1130711315
(case-fold nil)
11308-
(candidates (projectile-replace--candidates regexp nil case-fold root)))
11316+
(candidates (projectile-replace--candidates regexp nil case-fold projects)))
1130911317
(projectile-replace--open
1131011318
#'projectile-search-mode projectile-search-buffer-name
11311-
root regexp regexp nil nil case-fold candidates
11319+
(or (projectile--common-parent projects) "/")
11320+
regexp regexp nil nil case-fold candidates
1131211321
(projectile-prepend-project-name
1131311322
(format "No %s annotations found" (string-join keywords "/")))
1131411323
;; The pattern is already word-fenced and ends in a delimiter, so the
1131511324
;; whole-word fence could never match; whole-word mode is not seeded here.
11316-
nil rg-pattern)))
11325+
nil rg-pattern projects)))
1131711326

1131811327
(defun projectile--buffer-matches-conditions (buffer conditions)
1131911328
"Return non-nil if BUFFER satisfies any condition in CONDITIONS.
@@ -14711,6 +14720,25 @@ by its own ignore rules, wherever you happen to be sitting."
1471114720
(projectile-prepend-project-name (format "No matches for %s" term))
1471214721
projectile-search-whole-word nil projects)))
1471314722

14723+
(defun projectile-project-group-buffers (projects)
14724+
"Return the live buffers belonging to any of PROJECTS.
14725+
De-duplicated, since two members of a group can nest and a buffer under
14726+
both would otherwise be offered twice."
14727+
(delete-dups (mapcan #'projectile-project-buffers projects)))
14728+
14729+
;;;###autoload
14730+
(defun projectile-switch-to-buffer-in-projects (projects &optional prompt)
14731+
"Switch to a buffer belonging to any of PROJECTS.
14732+
PROMPT overrides the completion prompt. The current buffer is left out
14733+
of the choices, as `projectile-switch-to-buffer' does."
14734+
(switch-to-buffer
14735+
(projectile-completing-read
14736+
(or prompt "Switch to buffer: ")
14737+
(delete (buffer-name (current-buffer))
14738+
(mapcar #'buffer-name (projectile-project-group-buffers projects)))
14739+
:category 'buffer
14740+
:caller 'projectile-read-buffer)))
14741+
1471414742
(defun projectile--sibling-group ()
1471514743
"Return the projects to treat as a group with the current one.
1471614744

@@ -14746,6 +14774,35 @@ literal string."
1474614774
(length siblings) (if (cdr siblings) "s" "")
1474714775
(if regexp " regexp" "")))))
1474814776

14777+
;;;###autoload
14778+
(defun projectile-switch-to-buffer-in-sibling-projects ()
14779+
"Switch to a buffer of the current project or of one related to it.
14780+
Related is what `projectile-switch-sibling-project' means by it."
14781+
(interactive)
14782+
(projectile-switch-to-buffer-in-projects
14783+
(projectile--sibling-group) "Switch to sibling buffer: "))
14784+
14785+
;;;###autoload
14786+
(defun projectile-multi-occur-in-sibling-projects (&optional nlines)
14787+
"Do a `multi-occur' in the buffers of the current project and related ones.
14788+
Related is what `projectile-switch-sibling-project' means by it. With a
14789+
prefix argument, show NLINES of context.
14790+
14791+
Note this searches the buffers you have open, not the projects on disk -
14792+
`projectile-search-in-sibling-projects' is the one that reads files."
14793+
(interactive "P")
14794+
(multi-occur (projectile-project-group-buffers (projectile--sibling-group))
14795+
(car (occur-read-primary-args))
14796+
nlines))
14797+
14798+
;;;###autoload
14799+
(defun projectile-todos-in-sibling-projects ()
14800+
"Collect TODO-style annotations across the current project and related ones.
14801+
Related is what `projectile-switch-sibling-project' means by it. See
14802+
`projectile-todos' for what counts as an annotation."
14803+
(interactive)
14804+
(projectile--todos (projectile--sibling-group)))
14805+
1474914806

1475014807
;;; Project bookmarks
1475114808
;;
@@ -16499,6 +16556,9 @@ Magit that don't trigger `find-file-hook'."
1649916556
(define-key map (kbd "n p") #'projectile-switch-sibling-project)
1650016557
(define-key map (kbd "n f") #'projectile-find-file-in-sibling-projects)
1650116558
(define-key map (kbd "n s") #'projectile-search-in-sibling-projects)
16559+
(define-key map (kbd "n b") #'projectile-switch-to-buffer-in-sibling-projects)
16560+
(define-key map (kbd "n o") #'projectile-multi-occur-in-sibling-projects)
16561+
(define-key map (kbd "n t") #'projectile-todos-in-sibling-projects)
1650216562
(define-key map (kbd "o") #'projectile-multi-occur)
1650316563
(define-key map (kbd "p") #'projectile-switch-project)
1650416564
(define-key map (kbd "q") #'projectile-switch-open-project)
@@ -16843,6 +16903,7 @@ search/replace case-sensitive, `--word' makes it match whole words,
1684316903
("J" "toggle related" projectile-toggle-related-file)]
1684416904
["Buffers"
1684516905
("b" "switch buffer" projectile-dispatch-switch-to-buffer)
16906+
("nb" "buffer in siblings" projectile-switch-to-buffer-in-sibling-projects)
1684616907
("C-o" "display buffer" projectile-display-buffer)
1684716908
("I" "ibuffer" projectile-ibuffer)
1684816909
("k" "kill buffers" projectile-kill-buffers)
@@ -16859,6 +16920,8 @@ search/replace case-sensitive, `--word' makes it match whole words,
1685916920
("sx" "references" projectile-find-references)
1686016921
("sR" "search (review)" projectile-dispatch-search-review)
1686116922
("ns" "search siblings" projectile-dispatch-search-siblings)
16923+
("no" "multi-occur siblings" projectile-multi-occur-in-sibling-projects)
16924+
("nt" "todos in siblings" projectile-todos-in-sibling-projects)
1686216925
("st" "todos" projectile-todos)
1686316926
("o" "multi-occur" projectile-multi-occur)
1686416927
("r" "replace" projectile-replace)
@@ -16960,6 +17023,7 @@ search/replace case-sensitive, `--word' makes it match whole words,
1696017023
["Toggle between related files" projectile-toggle-related-file])
1696117024
("Buffers"
1696217025
["Switch to buffer" projectile-switch-to-buffer]
17026+
["Switch to buffer in sibling projects" projectile-switch-to-buffer-in-sibling-projects]
1696317027
["Kill project buffers" projectile-kill-buffers]
1696417028
["Save project buffers" projectile-save-project-buffers]
1696517029
["Recent files" projectile-recentf]
@@ -17002,6 +17066,8 @@ search/replace case-sensitive, `--word' makes it match whole words,
1700217066
["Search with ripgrep" projectile-ripgrep]
1700317067
["Search with ag" projectile-ag]
1700417068
["Search in sibling projects" projectile-search-in-sibling-projects]
17069+
["TODOs in sibling projects" projectile-todos-in-sibling-projects]
17070+
["Multi-occur in sibling projects" projectile-multi-occur-in-sibling-projects]
1700517071
["Project TODOs (review)" projectile-todos]
1700617072
["Replace in project" projectile-replace]
1700717073
["Replace in project (review)" projectile-replace-review]

test/projectile-project-group-test.el

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,66 @@ the two truename'd roots and `parent' to the directory holding them."
285285
(get-buffer projectile-search-buffer-name))
286286
:to-equal '("alpha/src/a.txt" "beta/lib/b.txt")))))
287287

288+
289+
;;; Buffers across a group
290+
291+
(describe "projectile-project-group-buffers"
292+
(it "collects the buffers of every project in the group"
293+
(projectile-group-test--with-projects
294+
(let ((ba (find-file-noselect (expand-file-name "src/a.txt" alpha)))
295+
(bb (find-file-noselect (expand-file-name "lib/b.txt" beta))))
296+
(expect (projectile-project-group-buffers (list alpha beta))
297+
:to-contain ba)
298+
(expect (projectile-project-group-buffers (list alpha beta))
299+
:to-contain bb))))
300+
301+
(it "offers a buffer once when two members of the group nest"
302+
(projectile-group-test--with-projects
303+
(find-file-noselect (expand-file-name "src/a.txt" alpha))
304+
(let ((buffers (projectile-project-group-buffers (list parent alpha))))
305+
(expect buffers :to-equal (delete-dups (copy-sequence buffers)))))))
306+
307+
(describe "projectile-switch-to-buffer-in-projects"
308+
(it "offers the group's buffers, minus the one you are in"
309+
(projectile-group-test--with-projects
310+
(let ((ba (find-file-noselect (expand-file-name "src/a.txt" alpha))))
311+
(find-file-noselect (expand-file-name "lib/b.txt" beta))
312+
(spy-on 'projectile-completing-read :and-return-value (buffer-name ba))
313+
(spy-on 'switch-to-buffer)
314+
(with-current-buffer ba
315+
(projectile-switch-to-buffer-in-projects (list alpha beta)))
316+
(let ((offered (cadr (spy-calls-args-for 'projectile-completing-read 0))))
317+
(expect offered :to-contain "b.txt")
318+
(expect offered :not :to-contain (buffer-name ba)))))))
319+
320+
321+
;;; TODOs across a group
322+
323+
(describe "projectile-todos-in-sibling-projects"
324+
(it "collects annotations from every project in the group"
325+
(projectile-group-test--with-projects
326+
(with-temp-file (expand-file-name "src/todo.txt" alpha)
327+
(insert "TODO: alpha thing\n"))
328+
(with-temp-file (expand-file-name "lib/todo.txt" beta)
329+
(insert "FIXME: beta thing\n"))
330+
(spy-on 'projectile-sibling-projects :and-return-value (list alpha beta))
331+
(cl-letf (((symbol-function 'pop-to-buffer) #'ignore))
332+
(projectile-todos-in-sibling-projects))
333+
(expect (projectile-test-match-files
334+
(get-buffer projectile-search-buffer-name))
335+
:to-equal '("alpha/src/todo.txt" "beta/lib/todo.txt"))))
336+
337+
(it "still works on a single project, unchanged"
338+
(projectile-group-test--with-projects
339+
(with-temp-file (expand-file-name "src/todo.txt" alpha)
340+
(insert "TODO: alpha thing\n"))
341+
(spy-on 'projectile-acquire-root :and-return-value alpha)
342+
(cl-letf (((symbol-function 'pop-to-buffer) #'ignore))
343+
(projectile-todos))
344+
(expect (projectile-test-match-files
345+
(get-buffer projectile-search-buffer-name))
346+
:to-equal '("src/todo.txt")))))
347+
288348
(provide 'projectile-project-group-test)
289349

290350
;;; projectile-project-group-test.el ends here

0 commit comments

Comments
 (0)