Files
doomemacs/lisp/doom-projects.el
Henrik Lissner 65d43ae472 fix: init known projects before projectile-ensure-project
Otherwise, the known projects list could be empty the first time the
project switcher prompt is shown via any command that isn't explicitly
`projectile-switch-project` (like invoking `projectile-find-file` from a
non-project buffer).

Ref: #8330
2025-03-30 16:10:23 -04:00

321 lines
14 KiB
EmacsLisp

;;; doom-projects.el --- defaults for project management in Doom -*- lexical-binding: t; -*-
;;; Commentary:
;;; Code:
(define-obsolete-variable-alias 'doom-projectile-fd-binary 'doom-fd-executable "3.0.0")
(defvar doom-fd-executable (cl-find-if #'executable-find (list "fdfind" "fd"))
"The filename of the fd executable.
On some distros it's fdfind (ubuntu, debian, and derivatives). On most it's fd.
Is nil if no executable is found in your PATH during startup.")
(defvar doom-ripgrep-executable (executable-find "rg")
"The filename of the Ripgrep executable.
Is nil if no executable is found in your PATH during startup.")
(defvar doom-projectile-cache-dir (file-name-concat doom-profile-cache-dir "projectile/")
"The directory where per-project projectile file index caches are stored.
Must end with a slash.")
;;
;;; Packages
(use-package! project
:defer t
:init
(setq project-list-file (file-name-concat doom-profile-state-dir "projects"))
:config
;; Not valid vc backends, but I use it to inform (global) file index
;; exclusions below and elsewhere.
(add-to-list 'project-vc-backend-markers-alist '(Jujutsu . ".jj"))
(add-to-list 'project-vc-backend-markers-alist '(Sapling . ".sl"))
(add-to-list 'project-vc-extra-root-markers ".jj")
;; TODO: Advice or add command for project-wide `find-sibling-file'.
)
;; DEPRECATED: Will be replaced with project.el
(use-package! projectile
:commands (projectile-project-root
projectile-project-name
projectile-project-p
projectile-locate-dominating-file
projectile-relevant-known-projects)
:init
(setq projectile-enable-caching (if noninteractive t 'persistent)
projectile-globally-ignored-files '(".DS_Store" "TAGS")
projectile-globally-ignored-file-suffixes '(".elc" ".pyc" ".o")
projectile-kill-buffers-filter 'kill-only-files
projectile-ignored-projects '("~/")
projectile-known-projects-file (concat doom-projectile-cache-dir "projects.eld")
projectile-ignored-project-function #'doom-project-ignored-p
projectile-fd-executable doom-fd-executable)
(global-set-key [remap evil-jump-to-tag] #'projectile-find-tag)
(global-set-key [remap find-tag] #'projectile-find-tag)
:config
(make-directory doom-projectile-cache-dir t)
;; Projectile runs four functions to determine the root (in this order):
;;
;; + `projectile-root-local' -> checks the `projectile-project-root' variable
;; for an explicit path.
;; + `projectile-root-bottom-up' -> searches from / to your current directory
;; for the paths listed in `projectile-project-root-files-bottom-up'. This
;; includes .git and .project
;; + `projectile-root-top-down' -> searches from the current directory down to
;; / the paths listed in `projectile-root-files', like package.json,
;; setup.py, or Cargo.toml
;; + `projectile-root-top-down-recurring' -> searches from the current
;; directory down to / for a directory that has one of
;; `projectile-project-root-files-top-down-recurring' but doesn't have a
;; parent directory with the same file.
;;
;; In the interest of performance, we reduce the number of project root marker
;; files/directories projectile searches for when resolving the project root.
;;
;; These will be filled by other modules. We build this list manually so
;; projectile doesn't perform so many file checks every time it resolves a
;; project's root -- particularly when a file has no project.
(setq projectile-project-root-files '()
projectile-project-root-files-top-down-recurring '("Makefile"))
;; Adds a more editor/plugin-agnostic project marker.
(add-to-list 'projectile-project-root-files-bottom-up ".project")
(push (abbreviate-file-name doom-local-dir) projectile-globally-ignored-directories)
;; Per-project compilation buffers
(setq compilation-buffer-name-function #'projectile-compilation-buffer-name
compilation-save-buffers-predicate #'projectile-current-project-buffer-p)
;; HACK: Centralize Projectile's per-project cache files, so they don't litter
;; projects with dotfiles.
(defadvice! doom--projectile-centralized-cache-files-a (fn &optional proot)
:around #'projectile-project-cache-file
(let* ((proot (or proot (doom-project-root) default-directory))
(projectile-cache-file
(expand-file-name
(format "%s-%s" (doom-project-name proot) (sha1 proot))
doom-projectile-cache-dir)))
(funcall fn proot)))
;; HACK: `projectile-ensure-project' operates on the current value of
;; `projectile-known-projects' when prompting the using for a project, which
;; may not have been initialized yet, so do so the first time it is called.
;; REVIEW: PR this upstream
(defadvice! doom--projectile-update-known-projects-a (dir)
:before #'projectile-ensure-project
(unless dir
(when (and (eq projectile-require-project-root 'prompt)
(not projectile-known-projects))
(projectile-known-projects))
(advice-remove 'projectile-ensure-project #'doom--projectile-update-known-projects-a)))
;; Support the more generic .project files as an alternative to .projectile
(defadvice! doom--projectile-dirconfig-file-a ()
:override #'projectile-dirconfig-file
(let ((proot (projectile-project-root)))
(cond ((file-exists-p! (or projectile-dirconfig-file ".project") proot))
((expand-file-name ".project" proot)))))
;; Disable commands that won't work, as is, and that Doom already provides a
;; better alternative for.
(put 'projectile-ag 'disabled "Use +default/search-project instead")
(put 'projectile-ripgrep 'disabled "Use +default/search-project instead")
(put 'projectile-grep 'disabled "Use +default/search-project instead")
;; HACK: Some MSYS utilities auto expanded the `/' path separator, so we need
;; to prevent it.
(when doom--system-windows-p
(setenv "MSYS_NO_PATHCONV" "1") ; Fix path in Git Bash
(setenv "MSYS2_ARG_CONV_EXCL" "--path-separator")) ; Fix path in MSYS2
;; HACK: Don't rely on VCS-specific commands to generate our file lists.
;; That's 7 commands to maintain, versus the more generic, reliable, and
;; performant `fd' or `ripgrep'.
(defadvice! doom--only-use-generic-command-a (fn vcs)
"Only use `projectile-generic-command' for indexing project files.
And if it's a function, evaluate it."
:around #'projectile-get-ext-command
(if (and (functionp projectile-generic-command)
(not (file-remote-p default-directory)))
(funcall projectile-generic-command vcs)
(let ((projectile-git-submodule-command
(or projectile-git-submodule-command
(get 'projectile-git-submodule-command 'initial-value))))
(funcall fn vcs))))
;; HACK: `projectile-generic-command' doesn't typically support a function,
;; but my `doom--only-use-generic-command-a' advice allows this. I do it
;; this way to make it easier for folks to undo the change (if not set to a
;; function, projectile will revert to default behavior).
(put 'projectile-git-submodule-command 'initial-value projectile-git-submodule-command)
(setq projectile-git-submodule-command nil
;; Include and follow symlinks in file listings.
projectile-git-fd-args (concat "-tl " projectile-git-fd-args)
projectile-indexing-method 'hybrid
projectile-generic-command
(lambda (_)
;; If fd or ripgrep exists, use it to produce file listings for
;; projectile commands. fd is a rust program that is significantly
;; faster than git ls-files, find, or the various VCS commands
;; projectile is configured to use. Plus, it respects .gitignore.
(cond
((when-let*
((doom-fd-executable)
(projectile-git-use-fd)
;; REVIEW Temporary fix for #6618. Improve me later.
(version (with-memoization (get 'doom-fd-executable 'version)
(cadr (split-string (cdr (doom-call-process doom-fd-executable "--version"))
" " t))))
((ignore-errors (version-to-list version))))
(string-join
(delq
nil (list doom-fd-executable "."
(if (version< version "8.3.0")
(replace-regexp-in-string "--strip-cwd-prefix" "" projectile-git-fd-args t t)
projectile-git-fd-args)
(when project-vc-backend-markers-alist
(mapconcat (fn! (format "-E %s" (shell-quote-argument (cdr %))))
project-vc-backend-markers-alist
" "))
(if doom--system-windows-p " --path-separator=/" "")))
" ")))
;; Otherwise, resort to ripgrep, which is also faster than find
(doom-ripgrep-executable
(string-join
(delq
nil (list doom-ripgrep-executable
"-0 --files --follow --color=never --hidden"
(when project-vc-backend-markers-alist
(mapconcat (fn! (format "-g!%s" (shell-quote-argument (cdr %))))
project-vc-backend-markers-alist
" "))
(if doom--system-windows-p " --path-separator=/")))
" "))
((not doom--system-windows-p) "find . -type f | cut -c3- | tr '\\n' '\\0'")
("find . -type f -print0"))))
(defadvice! doom--projectile-default-generic-command-a (fn &rest args)
"If projectile can't tell what kind of project you're in, it issues an error
when using many of projectile's command, e.g. `projectile-compile-command',
`projectile-run-project', `projectile-test-project', and
`projectile-configure-project', for instance.
This suppresses the error so these commands will still run, but prompt you for
the command instead."
:around #'projectile-default-generic-command
(ignore-errors (apply fn args)))
(projectile-mode +1))
;;
;;; Project-based minor modes
(defvar doom-project-hook nil
"Hook run when a project is enabled. The name of the project's mode and its
state are passed in.")
(cl-defmacro def-project-mode! (name &key
modes
files
when
match
add-hooks
on-load
on-enter
on-exit)
"Define a project minor mode named NAME and where/how it is activated.
Project modes allow you to configure 'sub-modes' for major-modes that are
specific to a folder, project structure, framework or whatever arbitrary context
you define. These project modes can have their own settings, keymaps, hooks,
snippets, etc.
This creates NAME-hook and NAME-map as well.
PLIST may contain any of these properties, which are all checked to see if NAME
should be activated. If they are *all* true, NAME is activated.
:modes MODES -- if buffers are derived from MODES (one or a list of symbols).
:files FILES -- if project contains FILES; takes a string or a form comprised
of nested (and ...) and/or (or ...) forms. Each path is relative to the
project root, however, if prefixed with a '.' or '..', it is relative to the
current buffer.
:match REGEXP -- if file name matches REGEXP
:when PREDICATE -- if PREDICATE returns true (can be a form or the symbol of a
function)
:add-hooks HOOKS -- HOOKS is a list of hooks to add this mode's hook.
:on-load FORM -- FORM to run the first time this project mode is enabled.
:on-enter FORM -- FORM is run each time the mode is activated.
:on-exit FORM -- FORM is run each time the mode is disabled.
Relevant: `doom-project-hook'."
(declare (indent 1))
(let ((init-var (intern (format "%s-init" name))))
(macroexp-progn
(append
(when on-load
`((defvar ,init-var nil)))
`((define-minor-mode ,name
"A project minor mode generated by `def-project-mode!'."
:init-value nil
:lighter ""
:keymap (make-sparse-keymap)
(if (not ,name)
,on-exit
(run-hook-with-args 'doom-project-hook ',name ,name)
,(when on-load
`(unless ,init-var
,on-load
(setq ,init-var t)))
,on-enter))
(dolist (hook ,add-hooks)
(add-hook ',(intern (format "%s-hook" name)) hook)))
(cond ((or files modes when)
(cl-check-type files (or null list string))
(let ((fn
`(lambda ()
(and (not (bound-and-true-p ,name))
(and buffer-file-name (not (file-remote-p buffer-file-name nil t)))
,(or (null match)
`(if buffer-file-name (string-match-p ,match buffer-file-name)))
,(or (null files)
;; Wrap this in `eval' to prevent eager expansion
;; of `project-file-exists-p!' from pulling in
;; autoloaded files prematurely.
`(eval
'(project-file-exists-p!
,(if (stringp (car files)) (cons 'and files) files))))
,(or when t)
(,name 1)))))
(if modes
`((dolist (mode ,modes)
(let ((hook-name
(intern (format "doom--enable-%s%s-h" ',name
(if (eq mode t) "" (format "-in-%s" mode))))))
(fset hook-name #',fn)
(if (eq mode t)
(add-to-list 'auto-minor-mode-magic-alist (cons hook-name #',name))
(add-hook (intern (format "%s-hook" mode)) hook-name)))))
`((add-hook 'change-major-mode-after-body-hook #',fn)))))
(match
`((add-to-list 'auto-minor-mode-alist (cons ,match #',name)))))))))
(provide 'doom-projects)
;;; doom-projects.el ends here