diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f02fd42..c950e9b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ ### Bugs fixed +- [#2172](https://github.com/bbatsov/projectile/pull/2172): Fix subproject detection in a Go workspace. A `go.work` names its modules as `./api`, and the leading `./` survived into the subproject name, which then matched none of the project's file paths - so `projectile-find-file-in-subproject` offered nothing at all in a Go monorepo. Members are now spelled relative to the project root whatever the manifest wrote. - [#2171](https://github.com/bbatsov/projectile/pull/2171): The commands that list the known projects now load the persisted list first. They read the `projectile-known-projects` variable, which stays nil until the same-named accessor loads it from disk and which nothing loads at startup - so whichever of them you reached for first in a session offered nothing. `projectile-find-file-in-known-projects` completed over an empty list; `projectile-vc`, `projectile-remove-known-project` and the "Switch to project" prompt of `projectile-require-project-root` were affected too. - [#2170](https://github.com/bbatsov/projectile/pull/2170): Navigating a search results buffer while the scan is still running is no longer undone by the next chunk. Each redraw ended by going to the top of the buffer, so point jumped back on every batch of matches; it now stays where you left it. - [#2167](https://github.com/bbatsov/projectile/pull/2167): The Emacs Lisp search scanner no longer decompresses or decrypts the files it reads. It went through `file-name-handler-alist`, so every archive in the candidate set was run through gzip and every `.gpg` handed to EPA - work discarded a moment later as binary, and on an encrypted file a passphrase prompt in the middle of a project search. diff --git a/projectile.el b/projectile.el index 58129d77a..5ddbae91b 100644 --- a/projectile.el +++ b/projectile.el @@ -12407,7 +12407,14 @@ narrow." ;; passing it makes a glob be read as a regular expression. (dolist (hit (file-expand-wildcards (directory-file-name pattern))) (when (file-directory-p (expand-file-name hit root)) - (push (file-name-as-directory hit) dirs))))) + ;; Re-spell the hit relative to ROOT rather than keeping it as + ;; written. A `go.work' says `./api', and the leading `./' would + ;; otherwise survive into a subproject name that matches none of + ;; the project's file paths - leaving find-file-in-subproject + ;; with nothing to offer. + (push (file-name-as-directory + (file-relative-name (expand-file-name hit root) root)) + dirs))))) (sort (delete-dups dirs) #'string<))) (defun projectile--workspace-member-patterns (root) diff --git a/test/projectile-commands-test.el b/test/projectile-commands-test.el index efad5136f..354006994 100644 --- a/test/projectile-commands-test.el +++ b/test/projectile-commands-test.el @@ -857,6 +857,27 @@ (expect (projectile-subprojects-from-manifest repo) :to-equal '("core/" "util/")))))) + (it "reads the modules a Go workspace puts to use" + (projectile-test-with-sandbox + (projectile-test-with-files + ("repo/api/" "repo/worker/" "repo/scratch/") + (with-temp-file "repo/go.work" + (insert "go 1.22\n\nuse (\n\t./api\n\t// not this one\n\t./worker\n)\n")) + (let ((repo (file-truename (expand-file-name "repo/")))) + ;; the comment inside the block is not a module, and `scratch' is + ;; simply not listed + (expect (projectile-subprojects-from-manifest repo) + :to-equal '("api/" "worker/")))))) + + (it "reads a single-line Go workspace use directive" + (projectile-test-with-sandbox + (projectile-test-with-files + ("repo/only/") + (with-temp-file "repo/go.work" (insert "go 1.22\n\nuse ./only\n")) + (let ((repo (file-truename (expand-file-name "repo/")))) + (expect (projectile-subprojects-from-manifest repo) + :to-equal '("only/")))))) + (it "has no answer when the members are not declared statically" ;; Gradle builds its module list in a Kotlin program and Bazel in ;; Starlark; neither can be read without running the tool, so the scan diff --git a/test/projectile-dispatch-test.el b/test/projectile-dispatch-test.el index 79d5f8a8e..ac6a0c4e4 100644 --- a/test/projectile-dispatch-test.el +++ b/test/projectile-dispatch-test.el @@ -136,6 +136,48 @@ ;; dispatch menu is always available. (expect (get 'projectile-dispatch 'transient--prefix) :to-be-truthy))) +(describe "projectile-dispatch-search-siblings" + ;; The sibling search is not a `projectile--define-display-variants' wrapper + ;; like its neighbours - it translates the switches itself - so it needs its + ;; own coverage. + :var (captured) + (before-each + (setq captured nil) + (spy-on 'projectile-dispatch--args :and-return-value nil) + (spy-on 'projectile-search-in-sibling-projects :and-call-fake + (lambda (&optional regexp) + (setq captured (list :regexp regexp + :case-fold case-fold-search + :word projectile-search-whole-word))))) + + (it "searches for a literal term with no switches active" + (projectile-dispatch-search-siblings) + (expect (plist-get captured :regexp) :to-be nil)) + + (it "reads the term as a regexp when --regexp is active" + (spy-on 'projectile-dispatch--args :and-return-value '("--regexp")) + (projectile-dispatch-search-siblings) + (expect (plist-get captured :regexp) :to-be t)) + + (it "seeds the search case-sensitive when --case-sensitive is active" + (spy-on 'projectile-dispatch--args :and-return-value '("--case-sensitive")) + (let ((case-fold-search t)) + (projectile-dispatch-search-siblings)) + (expect (plist-get captured :case-fold) :to-be nil)) + + (it "seeds whole-word matching when --word is active" + (spy-on 'projectile-dispatch--args :and-return-value '("--word")) + (let ((projectile-search-whole-word nil)) + (projectile-dispatch-search-siblings)) + (expect (plist-get captured :word) :to-be t)) + + (it "leaves the search settings alone when the switches are off" + (let ((case-fold-search t) + (projectile-search-whole-word nil)) + (projectile-dispatch-search-siblings)) + (expect (plist-get captured :case-fold) :to-be t) + (expect (plist-get captured :word) :to-be nil))) + (provide 'projectile-dispatch-test) ;;; projectile-dispatch-test.el ends here diff --git a/test/projectile-project-group-test.el b/test/projectile-project-group-test.el index cf2b4b8cb..86e397ab5 100644 --- a/test/projectile-project-group-test.el +++ b/test/projectile-project-group-test.el @@ -346,6 +346,46 @@ the two truename'd roots and `parent' to the directory holding them." :to-equal '("src/todo.txt"))))) +;;; The sibling wrappers themselves + +(describe "projectile-switch-to-buffer-in-sibling-projects" + (it "offers the buffers of the whole family" + (projectile-group-test--with-projects + (let ((ba (find-file-noselect (expand-file-name "src/a.txt" alpha))) + (bb (find-file-noselect (expand-file-name "lib/b.txt" beta)))) + (ignore ba) + (spy-on 'projectile-sibling-projects :and-return-value (list alpha beta)) + (spy-on 'projectile-completing-read :and-return-value (buffer-name bb)) + (spy-on 'switch-to-buffer) + (projectile-switch-to-buffer-in-sibling-projects) + (let ((offered (cadr (spy-calls-args-for 'projectile-completing-read 0)))) + (expect offered :to-contain "a.txt") + (expect offered :to-contain "b.txt")) + (expect 'switch-to-buffer :to-have-been-called-with (buffer-name bb))))) + + (it "says so when the project has no siblings" + (projectile-group-test--with-projects + (spy-on 'projectile-sibling-projects :and-return-value nil) + (spy-on 'projectile-acquire-root :and-return-value alpha) + (expect (projectile-switch-to-buffer-in-sibling-projects) + :to-throw 'user-error)))) + +(describe "projectile-multi-occur-in-sibling-projects" + (it "runs multi-occur over the family's buffers, not over files on disk" + (projectile-group-test--with-projects + (let ((ba (find-file-noselect (expand-file-name "src/a.txt" alpha))) + (bb (find-file-noselect (expand-file-name "lib/b.txt" beta)))) + (spy-on 'projectile-sibling-projects :and-return-value (list alpha beta)) + (spy-on 'occur-read-primary-args :and-return-value '("needle")) + (spy-on 'multi-occur) + (projectile-multi-occur-in-sibling-projects 3) + (let ((args (spy-calls-args-for 'multi-occur 0))) + (expect (nth 0 args) :to-contain ba) + (expect (nth 0 args) :to-contain bb) + (expect (nth 1 args) :to-equal "needle") + ;; the prefix argument is passed through as the context line count + (expect (nth 2 args) :to-equal 3)))))) + ;;; Ripgrep across a group (defun projectile-group-test--seed-rg (buf root projects term)