doomemacs/lisp/doom-modules.el
Henrik Lissner dda848e089
module: add :config use-package
I intend to phase out the internal usage of use-package in Doom's core
and modules. The macro is too complex and magical for our needs.

That said, until we've fully removed it, this :config use-package is
hardcoded to be enabled-by-default, until use-package has been
refactored out of core and modules. It'd be wise not to add it to your
doom! blocks yet.
2022-09-24 18:46:21 +02:00

439 lines
18 KiB
EmacsLisp

;;; doom-modules.el --- module & package management system -*- lexical-binding: t; -*-
;;; Commentary:
;;; Code:
;;
;;; Variables
(defvar doom-modules (make-hash-table :test 'equal)
"A hash table of enabled modules. Set by `doom-initialize-modules'.")
(defvar doom-modules-dirs
(list (expand-file-name "modules/" doom-user-dir)
doom-modules-dir)
"A list of module root directories. Order determines priority.")
(defvar doom-module-init-file "init"
"The basename of init files for modules.
Init files are loaded early, just after Doom core, and before modules' config
files. They are always loaded, even in non-interactive sessions, and before
`doom-before-modules-init-hook'. Related to `doom-module-config-file'.")
(defvar doom-module-config-file "config"
"The basename of config files for modules.
Config files are loaded later, and almost always in interactive sessions. These
run before `doom-after-modules-config-hook'. Relevant to `doom-module-init-file'.")
(defconst doom-obsolete-modules
'((:feature (version-control (:emacs vc) (:ui vc-gutter))
(spellcheck (:checkers spell))
(syntax-checker (:checkers syntax))
(evil (:editor evil))
(snippets (:editor snippets))
(file-templates (:editor file-templates))
(workspaces (:ui workspaces))
(eval (:tools eval))
(lookup (:tools lookup))
(debugger (:tools debugger)))
(:tools (rotate-text (:editor rotate-text))
(vterm (:term vterm))
(password-store (:tools pass))
(flycheck (:checkers syntax))
(flyspell (:checkers spell))
(macos (:os macos)))
(:emacs (electric-indent (:emacs electric))
(hideshow (:editor fold))
(eshell (:term eshell))
(term (:term term)))
(:ui (doom-modeline (:ui modeline))
(fci (:ui fill-column))
(evil-goggles (:ui ophints))
(tabbar (:ui tabs))
(pretty-code (:ui ligatures)))
(:app (email (:email mu4e))
(notmuch (:email notmuch)))
(:lang (perl (:lang raku))))
"A tree alist that maps deprecated modules to their replacement(s).
Each entry is a three-level tree. For example:
(:feature (version-control (:emacs vc) (:ui vc-gutter))
(spellcheck (:checkers spell))
(syntax-checker (:tools flycheck)))
This marks :feature version-control, :feature spellcheck and :feature
syntax-checker modules obsolete. e.g. If :feature version-control is found in
your `doom!' block, a warning is emitted before replacing it with :emacs vc and
:ui vc-gutter.")
(defvar doom-inhibit-module-warnings (not noninteractive)
"If non-nil, don't emit deprecated or missing module warnings at startup.")
;;; Custom hooks
(defcustom doom-before-modules-init-hook nil
"Hooks run before module init.el files are loaded."
:group 'doom
:type 'hook)
(defcustom doom-after-modules-init-hook nil
"Hooks run after module init.el files are loaded."
:group 'doom
:type 'hook)
(defcustom doom-before-modules-config-hook nil
"Hooks run before module config.el files are loaded."
:group 'doom
:type 'hook)
(defcustom doom-after-modules-config-hook nil
"Hooks run after module config.el files are loaded (but before the user's)."
:group 'doom
:type 'hook)
(defvar doom--current-module nil)
(defvar doom--current-flags nil)
;;
;;; Module API
(defun doom-module-p (category module &optional flag)
"Returns t if CATEGORY MODULE is enabled (ie. present in `doom-modules')."
(declare (pure t) (side-effect-free t))
(when-let (plist (gethash (cons category module) doom-modules))
(or (null flag)
(and (memq flag (plist-get plist :flags))
t))))
(defun doom-module-depth (category module &optional initdepth?)
"Return the depth of CATEGORY MODULE.
If INITDEPTH? is non-nil, use the CAR if a module was given two depths (see
`doom-module-set')."
(if-let (depth (doom-module-get category module :depth))
(or (if initdepth?
(car-safe depth)
(cdr-safe depth))
depth)
0))
(defun doom-module-get (category module &optional property)
"Returns the plist for CATEGORY MODULE. Gets PROPERTY, specifically, if set."
(declare (pure t) (side-effect-free t))
(when-let (plist (gethash (cons category module) doom-modules))
(if property
(plist-get plist property)
plist)))
(defun doom-module-put (category module &rest plist)
"Set a PROPERTY for CATEGORY MODULE to VALUE. PLIST should be additional pairs
of PROPERTY and VALUEs.
\(fn CATEGORY MODULE PROPERTY VALUE &rest [PROPERTY VALUE [...]])"
(puthash (cons category module)
(if-let (old-plist (doom-module-get category module))
(if (null plist)
old-plist
(when (cl-oddp (length plist))
(signal 'wrong-number-of-arguments (list (length plist))))
(while plist
(plist-put old-plist (pop plist) (pop plist)))
old-plist)
plist)
doom-modules))
(defun doom-module-set (category module &rest plist)
"Enables a module by adding it to `doom-modules'.
CATEGORY is a keyword, module is a symbol, PLIST is a plist that accepts the
following properties:
:path STRING
Path to the directory where this module lives.
:depth INT|(INITDEPTH . CONFIGDEPTH)
Determines module load order. If a cons cell, INITDEPTH determines the load
order of the module's init.el, while CONFIGDEPTH determines the same for all
other config files (config.el, packages.el, doctor.el, etc).
:flags (SYMBOL...)
A list of activated flags for this module.
:features (SYMBOL...)
A list of active features, determined from module's metadata. NOT
IMPLEMENTED YET.
If PLIST consists of a single nil, the module is purged from memory instead."
(if (car plist)
(progn
;; PERF: Doom caches module index, flags, and features in symbol plists
;; for fast lookups in `modulep!' and elsewhere. plists are lighter
;; and faster than hash tables for datasets this size, and this
;; information is looked up *very* often.
(put category module
(let ((depth (ensure-list (or (plist-get plist :depth) 0))))
(cl-destructuring-bind (i j)
(with-memoization (get 'doom-modules depth) '(0 0))
(dolist (n (list i j))
(when (> n 999)
;; No one will have more than 999 modules at any single
;; depth enabled, right? ...Right?
(signal 'doom-module-error
(list (cons category module) "Over 999 module limit" n))))
(put 'doom-modules depth (list (1+ i) (1+ j)))
(vector (+ (* (or (cdr depth) (car depth)) 1000) j)
(+ (* (car depth) 1000) i)
(plist-get plist :flags)
(plist-get plist :features)))))
;; But the hash table will always been Doom's formal storage for modules.
(puthash (cons category module) plist doom-modules))
(remhash (cons category module) doom-modules)
(cl-remf (symbol-plist category) module)))
(defun doom-module-list (&optional paths-or-all initorder?)
"Return a list of (:group . name) module keys in order of their :depth.
PATHS-OR-ALL can either be a non-nil value or a list of directories. If given a
list of directories, return a list of module keys for all modules present
underneath it. If non-nil, return the same, but search `doom-modules-dirs'
(includes :core and :user). Modules that are enabled are sorted first by their
:depth, followed by disabled modules in lexicographical order (unless a :depth
is specified in their .doommodule).
If INITORDER? is non-nil, sort modules by their initdepth, rather than their
configdepth. See `doom-module-set' for details."
(sort (if paths-or-all
(delete-dups
(append (seq-remove #'cdr (doom-module-list nil initorder?))
(doom-files-in (if (listp paths-or-all)
paths-or-all
doom-modules-dirs)
:map #'doom-module-from-path
:type 'dirs
:mindepth 1
:depth 1)))
(hash-table-keys doom-modules))
(let ((idx (if initorder? 1 0)))
(lambda! ((groupa . namea) (groupb . nameb))
(let ((a (get groupa namea))
(b (get groupb nameb)))
(or (null b)
(if a (< (aref a idx)
(aref b idx)))))))))
(defun doom-module-expand-path (category module &optional file)
"Expands a path to FILE relative to CATEGORY and MODULE.
CATEGORY is a keyword. MODULE is a symbol. FILE is an optional string path.
If the category isn't enabled this returns nil. For finding disabled modules use
`doom-module-locate-path'."
(when-let (path (doom-module-get category module :path))
(if file
(file-name-concat path file)
path)))
(defun doom-module-locate-path (category &optional module file)
"Searches `doom-modules-dirs' to find the path to a module.
CATEGORY is a keyword (e.g. :lang) and MODULE is a symbol (e.g. 'python). FILE
is a string that will be appended to the resulting path. If no path exists, this
returns nil, otherwise an absolute path."
(let (file-name-handler-alist)
(if-let (path (doom-module-expand-path category module file))
(if (or (null file)
(file-exists-p path))
path)
(let* ((category (doom-keyword-name category))
(module (if module (symbol-name module)))
(path (file-name-concat category module file)))
(if file
;; PERF: locate-file-internal is a little faster for finding files,
;; but its interface for finding directories is clumsy.
(locate-file-internal path doom-modules-dirs '(".elc" ".el"))
(cl-loop for default-directory in doom-modules-dirs
if (file-exists-p path)
return (expand-file-name path)))))))
(defun doom-module-locate-paths (module-list file)
"Return all existing paths to FILE under each module in MODULE-LIST.
MODULE-LIST is a list of cons cells (GROUP . NAME). See `doom-module-list' for
an example."
(cl-loop with file = (file-name-sans-extension file)
for (group . name) in module-list
if (doom-module-locate-path group name file)
collect it))
(defun doom-module-from-path (path &optional enabled-only)
"Returns a cons cell (CATEGORY . MODULE) derived from PATH (a file path).
If ENABLED-ONLY, return nil if the containing module isn't enabled."
(let* ((file-name-handler-alist nil)
(path (expand-file-name path)))
(save-match-data
(cond ((string-match "/modules/\\([^/]+\\)/\\([^/]+\\)\\(?:/.*\\)?$" path)
(when-let* ((category (doom-keyword-intern (match-string 1 path)))
(module (intern (match-string 2 path))))
(and (or (null enabled-only)
(doom-module-p category module))
(cons category module))))
((file-in-directory-p path doom-core-dir)
(cons :core nil))
((file-in-directory-p path doom-user-dir)
(cons :user nil))))))
(defun doom-module-load-path (&optional module-dirs)
"Return a list of file paths to activated modules.
The list is in no particular order and its file paths are absolute. If
MODULE-DIRS is non-nil, include all modules (even disabled ones) available in
those directories."
(declare (pure t) (side-effect-free t))
(cl-loop for (cat . mod) in (doom-module-list module-dirs)
collect (doom-module-locate-path cat mod)))
(defun doom-module-mplist-map (fn mplist)
"Apply FN to each module in MPLIST."
(let ((mplist (copy-sequence mplist))
(inhibit-message doom-inhibit-module-warnings)
obsolete
results
category m)
(while mplist
(setq m (pop mplist))
(cond ((keywordp m)
(setq category m
obsolete (assq m doom-obsolete-modules)))
((null category)
(error "No module category specified for %s" m))
((and (listp m) (keywordp (car m)))
(pcase (car m)
(:cond
(cl-loop for (cond . mods) in (cdr m)
if (eval cond t)
return (prependq! mplist mods)))
(:if (if (eval (cadr m) t)
(push (caddr m) mplist)
(prependq! mplist (cdddr m))))
(test (if (xor (eval (cadr m) t)
(eq test :unless))
(prependq! mplist (cddr m))))))
((catch 'doom-modules
(let* ((module (if (listp m) (car m) m))
(flags (if (listp m) (cdr m))))
(when-let (new (assq module obsolete))
(let ((newkeys (cdr new)))
(if (null newkeys)
(print! (warn "%s module was removed"))
(if (cdr newkeys)
(print! (warn "%s module was removed and split into the %s modules")
(list category module)
(mapconcat #'prin1-to-string newkeys ", "))
(print! (warn "%s module was moved to %s")
(list category module)
(car newkeys)))
(push category mplist)
(dolist (key newkeys)
(push (if flags
(nconc (cdr key) flags)
(cdr key))
mplist)
(push (car key) mplist))
(throw 'doom-modules t))))
(push (funcall fn category module :flags (if (listp m) (cdr m)))
results))))))
(when noninteractive
(setq doom-inhibit-module-warnings t))
(nreverse results)))
;;
;;; Module config macros
(put :if 'lisp-indent-function 2)
(put :when 'lisp-indent-function 'defun)
(put :unless 'lisp-indent-function 'defun)
(defmacro doom! (&rest modules)
"Bootstraps DOOM Emacs and its modules.
If the first item in MODULES doesn't satisfy `keywordp', MODULES is evaluated,
otherwise, MODULES is a multiple-property list (a plist where each key can have
multiple, linear values).
The bootstrap process involves making sure the essential directories exist, core
packages are installed, `doom-autoloads-file' is loaded, `doom-packages-file'
cache exists (and is loaded) and, finally, loads your private init.el (which
should contain your `doom!' block).
Module load order is determined by your `doom!' block. See `doom-modules-dirs'
for a list of all recognized module trees. Order defines precedence (from most
to least)."
`(when noninteractive
(doom-module-mplist-map
(lambda (category module &rest plist)
(let ((path (doom-module-locate-path category module)))
(unless path
(print! (warn "Failed to locate a '%s %s' module") category module))
(apply #'doom-module-set category module
:path path
plist)))
,@(if (keywordp (car modules))
(list (list 'quote modules))
modules))
doom-modules))
;; DEPRECATED Remove in 3.0
(define-obsolete-function-alias 'featurep! 'modulep! "3.0.0")
(defvar doom--empty-module [nil nil nil nil])
(defmacro modulep! (category &optional module flag)
"Return t if :CATEGORY MODULE (and +FLAGS) are enabled.
If FLAG is provided, returns t if CATEGORY MODULE has FLAG enabled.
(modulep! :config default +flag)
CATEGORY and MODULE may be omitted when this macro is used from a Doom module's
source (except your DOOMDIR, which is a special module). Like so:
(modulep! +flag)
For more about modules and flags, see `doom!'."
;; PERF: This macro bypasses the module API to spare startup their runtime
;; cost, as `modulep!' gets called *a lot* during startup. In the future,
;; Doom will byte-compile its core files. At that time, we can use it again.
(and (cond (flag (memq flag (aref (or (get category module) doom--empty-module) 2)))
(module (get category module))
(doom--current-flags (memq category doom--current-flags))
(doom--current-module
(memq category
(aref (or (get (car doom--current-module)
(cdr doom--current-module))
doom--empty-module)
2)))
((if-let (module (doom-module-from-path (macroexpand '(file!))))
(memq category (aref (or (get (car module) (cdr module))
doom--empty-module)
2))
(error "(modulep! %s %s %s) couldn't figure out what module it was called from (in %s)"
category module flag (file!)))))
t))
;;
;;; Defaults
;; Register Doom's two virtual module categories, representing Doom's core and
;; the user's config; which are always enabled.
(doom-module-set :core nil :path doom-core-dir :depth -110)
(doom-module-set :user nil :path doom-user-dir :depth '(-105 . 105))
;; DEPRECATED: I intend to phase out our internal usage of `use-package' and
;; move it to a :config use-package module. The macro is far too complex and
;; magical for our needs, but until this move is done, ':config use-package'
;; will remain a hardcoded module for backwards compatibility.
(doom-module-set :config 'use-package
:path (doom-module-locate-path :config 'use-package)
:depth -111)
(provide 'doom-modules)
;;; doom-modules.el ends here