;;; core-projects.el -*- lexical-binding: t; -*- (defvar doom-projectile-cache-limit 25000 "If any project cache surpasses this many files it is purged when quitting Emacs.") (defvar doom-projectile-cache-blacklist '("~" "/tmp" "/") "Directories that should never be cached.") (defvar doom-projectile-cache-purge-non-projects nil "If non-nil, non-projects are purged from the cache on `kill-emacs-hook'.") (defvar doom-projectile-fd-binary (or (cl-find-if #'executable-find '("fdfind" "fd")) "fd") "name of `fd-find' executable binary") (defvar doom-projectile-cache-timer-file (concat doom-cache-dir "projectile.timers") "Where to save project file cache timers.") ;; ;;; Packages (use-package! projectile :after-call after-find-file dired-before-readin-hook minibuffer-setup-hook :commands (projectile-project-root projectile-project-name projectile-project-p projectile-locate-dominating-file) :init (setq projectile-cache-file (concat doom-cache-dir "projectile.cache") projectile-enable-caching doom-interactive-mode projectile-globally-ignored-files '(".DS_Store" "Icon " "TAGS") projectile-globally-ignored-file-suffixes '(".elc" ".pyc" ".o") projectile-kill-buffers-filter 'kill-only-files projectile-known-projects-file (concat doom-cache-dir "projectile.projects") projectile-ignored-projects '("~/" "/tmp")) (global-set-key [remap evil-jump-to-tag] #'projectile-find-tag) (global-set-key [remap find-tag] #'projectile-find-tag) :config (projectile-mode +1) ;; Projectile runs four functions to determine the root (in this order): ;; ;; + `projectile-root-local' -> consults the `projectile-project-root' ;; variable for an explicit path. ;; + `projectile-root-bottom-up' -> consults ;; `projectile-project-root-files-bottom-up'; searches from / to your ;; current directory for certain files (including .project and .git) ;; + `projectile-root-top-down' -> consults `projectile-project-root-files'; ;; searches from the current directory down to / for certain project ;; markers, like package.json, setup.py, or Cargo.toml ;; + `projectile-root-top-down-recurring' -> consults ;; `projectile-project-root-files-top-down-recurring'; e.g. searches from ;; the current directory down to / for a directory that has Makefile but ;; doesn't have a parent with one of those files. ;; ;; In the interest of performance, we reduce the number of project root marker ;; files/directories projectile searches for when resolving the project root. (setq projectile-project-root-files-bottom-up (append '(".project" ; doom project marker ".git") ; Git VCS root dir (when (executable-find "hg") '(".hg")) ; Mercurial VCS root dir (when (executable-find "bzr") '(".bzr"))) ; Bazaar VCS root dir ;; This 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. projectile-project-root-files '() projectile-project-root-files-top-down-recurring '("Makefile")) (push (abbreviate-file-name doom-local-dir) projectile-globally-ignored-directories) ;; Disable commands that won't work, as is, and that Doom already provides a ;; better alternative for. (put 'projectile-ag 'disabled "Use +{ivy,helm}/project-search instead") (put 'projectile-ripgrep 'disabled "Use +{ivy,helm}/project-search instead") (put 'projectile-grep 'disabled "Use +{ivy,helm}/project-search instead") ;; Treat current directory in dired as a "file in a project" and track it (add-hook 'dired-before-readin-hook #'projectile-track-known-projects-find-file-hook) ;; Accidentally indexing big directories like $HOME or / will massively bloat ;; projectile's cache (into the hundreds of MBs). This purges those entries ;; when exiting Emacs to prevent slowdowns/freezing when cache files are ;; loaded or written to. (add-hook! 'kill-emacs-hook (defun doom-cleanup-project-cache-h () "Purge projectile cache entries that: a) have too many files (see `doom-projectile-cache-limit'), b) represent blacklisted directories that are too big, change too often or are private. (see `doom-projectile-cache-blacklist'), c) are not valid projectile projects." (when (and (bound-and-true-p projectile-projects-cache) doom-interactive-mode) (cl-loop with blacklist = (mapcar #'file-truename doom-projectile-cache-blacklist) for proot in (hash-table-keys projectile-projects-cache) if (or (not (stringp proot)) (>= (length (gethash proot projectile-projects-cache)) doom-projectile-cache-limit) (member (substring proot 0 -1) blacklist) (and doom-projectile-cache-purge-non-projects (not (doom-project-p proot)))) do (doom-log "Removed %S from projectile cache" proot) and do (remhash proot projectile-projects-cache) and do (remhash proot projectile-projects-cache-time) and do (remhash proot projectile-project-type-cache)) (projectile-serialize-cache)))) ;; It breaks projectile's project root resolution if HOME is a project (e.g. ;; it's a git repo). In that case, we disable bottom-up root searching to ;; prevent issues. This makes project resolution a little slower and less ;; accurate in some cases. (let ((default-directory "~")) (when (cl-find-if #'projectile-file-exists-p projectile-project-root-files-bottom-up) (doom-log "HOME appears to be a project. Disabling bottom-up root search.") (setq projectile-project-root-files (append projectile-project-root-files-bottom-up projectile-project-root-files) projectile-project-root-files-bottom-up nil))) (cond ;; If fd exists, use it for git and generic projects. fd is a rust program ;; that is significantly faster than git ls-files or find, and it respects ;; .gitignore. This is recommended in the projectile docs. ((executable-find doom-projectile-fd-binary) (setq projectile-generic-command (format "%s . --color=never --type f -0 -H -E .git" doom-projectile-fd-binary) projectile-git-command projectile-generic-command projectile-git-submodule-command nil ;; ensure Windows users get fd's benefits projectile-indexing-method 'alien)) ;; Otherwise, resort to ripgrep, which is also faster than find ((executable-find "rg") (setq projectile-generic-command (concat "rg -0 --files --color=never --hidden" (cl-loop for dir in projectile-globally-ignored-directories concat (format " --glob '!%s'" dir))) projectile-git-command projectile-generic-command projectile-git-submodule-command nil ;; ensure Windows users get rg's benefits projectile-indexing-method 'alien)) ;; Fix breakage on windows in git projects with submodules, since Windows ;; doesn't have tr (IS-WINDOWS (setq projectile-git-submodule-command nil))) (defadvice! doom--projectile-default-generic-command-a (orig-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 orig-fn args))) ;; Projectile root-searching functions can cause an infinite loop on TRAMP ;; connections, so disable them. ;; TODO Is this still necessary? (defadvice! doom--projectile-locate-dominating-file-a (file _name) "Don't traverse the file system if on a remote connection." :before-while #'projectile-locate-dominating-file (and (stringp file) (not (file-remote-p file nil t))))) ;; ;;; 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 'core-projects) ;;; core-projects.el ends here