Skip to content
Open
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 @@ -8,6 +8,7 @@

### New features

- [#2158](https://github.com/bbatsov/projectile/issues/2158): `projectile-run-task` now also offers the recipes of the modules a justfile declares with `mod` (and `mod?`), qualified the way just names them (`just:foo::build`), including nested modules and modules with an explicit path.
- [#2182](https://github.com/bbatsov/projectile/pull/2182): Add `projectile-todos-in-projects` and `projectile-multi-occur-in-projects`, so every sibling command now has a public generic taking any list of projects behind it and a command for a group of your own is a two-line wrapper for all five.
- [#2181](https://github.com/bbatsov/projectile/pull/2181): Sapling and Bazaar projects can now list their own ignored files, via the new `projectile-sapling-ignored-command` and `projectile-bzr-ignored-command`.
- [#2180](https://github.com/bbatsov/projectile/pull/2180): Every shell backend can now open in another window (`s-p x 4 <key>`), not just vterm, eat and ghostel: `shell`, `eshell`, `ielm`, `term` and the backend-dispatching `projectile-run` gained `-other-window` commands.
Expand Down
9 changes: 7 additions & 2 deletions doc/modules/ROOT/pages/tasks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ image::projectile-discovered-tasks.png[projectile-run-task offering npm scripts
| `composer:lint`

| just recipes
| `justfile`, `.justfile`, `Justfile`
| `just:build`
| `justfile`, `.justfile`, `Justfile`, plus the modules it declares with `mod`
| `just:build`, `just:foo::build`

| go-task tasks
| `Taskfile.yml` and friends
Expand All @@ -216,6 +216,11 @@ completion prompt. Tasks are qualified with the `namespace` blocks they sit in
`Gemfile`. A task whose name is computed at runtime (`task type, [:id]`) can't
be known without running rake, so it isn't listed.

The recipes of a just module are qualified with its path the way just itself
names them (`just:foo::build` runs `just foo::build`). Modules are found where
just looks for them - `foo.just`, or `mod.just` or a justfile in the `foo`
directory - or at the path given in the `mod` statement.

Set `projectile-discover-tasks` to nil to turn this off, or customize
`projectile-task-providers` to add your own. A provider is a function taking
the project root and returning an alist of `(TASK-NAME . COMMAND)`; one that
Expand Down
75 changes: 67 additions & 8 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -12141,17 +12141,76 @@ duplicates dropped."
(push name names))))))
(nreverse names)))

(defun projectile--just-modules (file)
"Return the modules the justfile FILE declares, as (NAME . PATH) pairs.
PATH is the module's explicit source path, or nil when it has none."
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(let ((case-fold-search nil)
modules)
;; `mod NAME' or `mod? NAME', optionally followed by a quoted path
;; and a comment. Anchoring at the end of the line keeps a recipe
;; called `mod' that takes a parameter from reading as a module.
(while (re-search-forward
(concat "^mod\\??[ \t]+\\([a-zA-Z_][a-zA-Z0-9_-]*\\)"
"\\(?:[ \t]+\\(?:'\\([^'\n]*\\)'\\|\"\\([^\"\n]*\\)\"\\)\\)?"
"[ \t]*\\(?:#.*\\)?$")
nil t)
(push (cons (match-string 1) (or (match-string 2) (match-string 3)))
modules))
(nreverse modules))))

(defun projectile--just-module-file (directory name path)
"Return the source file of the just module NAME declared in DIRECTORY.
PATH is the module's explicit path, or nil. Like just itself, look for
NAME.just, then for `mod.just' or a justfile in the NAME directory; an
explicit PATH that names a directory is searched the same way."
(let* ((names (cons "mod.just" projectile--justfile-names))
(candidates
(if path
(let ((file (expand-file-name path directory)))
(if (file-directory-p file)
(mapcar (lambda (name) (expand-file-name name file)) names)
(list file)))
(cons (expand-file-name (concat name ".just") directory)
(let ((module-directory (expand-file-name name directory)))
(mapcar (lambda (name) (expand-file-name name module-directory))
names))))))
(seq-find (lambda (file)
(and (file-readable-p file) (not (file-directory-p file))))
candidates)))

(defun projectile--just-recipes (file &optional prefix seen)
"Return the recipes of the justfile FILE and of the modules it declares.
The recipes of a module are qualified with its path the way just names
them (`foo::build'). PREFIX is the module path of FILE itself, and SEEN
lists the files already read, so a module that loops back is skipped."
(let ((seen (cons file seen))
(directory (file-name-directory file)))
(append
;; A recipe starts in column zero, may be marked quiet with `@' and
;; may take parameters. The lookahead keeps `:=', which is an
;; assignment, from reading as a recipe.
(mapcar (lambda (recipe) (concat prefix recipe))
(projectile--matches-in-file
file "^@?\\([a-zA-Z_][a-zA-Z0-9_-]*\\)[^:\n]*:\\(?:[^=\n]\\|$\\)"))
(mapcan (lambda (module)
(when-let* ((module-file (projectile--just-module-file
directory (car module) (cdr module)))
((not (member module-file seen))))
(projectile--just-recipes module-file
(concat prefix (car module) "::")
seen)))
(projectile--just-modules file)))))

(defun projectile-tasks-from-just (project-root)
"Return the recipes of the justfile in PROJECT-ROOT as tasks."
"Return the recipes of the justfile in PROJECT-ROOT as tasks.
The recipes of the modules it declares with `mod' are included too,
qualified with the module's name (`just:foo::build')."
(when-let* ((file (projectile--first-task-file
project-root projectile--justfile-names)))
;; A recipe starts in column zero, may be marked quiet with `@' and
;; may take parameters. The lookahead keeps `:=', which is an
;; assignment, from reading as a recipe.
(projectile--tasks-named
(projectile--matches-in-file
file "^@?\\([a-zA-Z_][a-zA-Z0-9_-]*\\)[^:\n]*:\\(?:[^=\n]\\|$\\)")
"just" "just %s")))
(projectile--tasks-named (projectile--just-recipes file) "just" "just %s")))

(defun projectile--taskfile-task-names (file)
"Return the keys of the top-level `tasks:' mapping in the Taskfile FILE.
Expand Down
34 changes: 33 additions & 1 deletion test/projectile-tasks-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,39 @@ test filter=\"\":

(it "does not mistake an assignment for a recipe"
(projectile-test-with-project (("justfile" . "export FOO := \"bar\"\n"))
(expect (projectile-tasks-from-just (projectile-project-root)) :to-be nil))))
(expect (projectile-tasks-from-just (projectile-project-root)) :to-be nil)))

(it "reads the recipes of the modules a justfile declares"
(projectile-test-with-project
(("justfile" . "mod foo
mod? bar 'tools/bar.just'
[private]
mod baz # nested

default:
@just --list
")
("foo.just" . "build:\n echo foo\n")
("tools/bar.just" . "lint:\n echo bar\n")
("baz/mod.just" . "mod qux\n\ntest:\n echo baz\n")
("baz/qux/justfile" . "run:\n echo qux\n"))
(expect (projectile-tasks-from-just (projectile-project-root))
:to-equal '(("just:default" . "just default")
("just:foo::build" . "just foo::build")
("just:bar::lint" . "just bar::lint")
("just:baz::test" . "just baz::test")
("just:baz::qux::run" . "just baz::qux::run")))))

(it "skips a module without a source file and a module that loops back"
(projectile-test-with-project
(("justfile" . "mod? missing\nmod self 'justfile'\n\nbuild:\n cargo build\n"))
(expect (projectile-tasks-from-just (projectile-project-root))
:to-equal '(("just:build" . "just build")))))

(it "does not mistake a recipe called mod for a module"
(projectile-test-with-project (("justfile" . "mod target:\n echo {{target}}\n"))
(expect (projectile-tasks-from-just (projectile-project-root))
:to-equal '(("just:mod" . "just mod"))))))

(describe "projectile-tasks-from-taskfile"
(it "reads the keys of the tasks mapping"
Expand Down