refactor!: module API

BREAKING CHANGE: This backports some architectural choices from v3.0.
This changes Doom's module API, renaming some functions and removing
others, in order to facilitate some new features, prepare to move Doom's
modules into separate repos, and make way for two, much larger breaking
commits coming in the next few days.

This commit won't break anything for users unless they're tinkering with
Doom's internals/using its `doom-module-*` API directly. I am avoiding
broader backwards incompatibilities until the 3.0 release.

What's new:

- Negated flags. (modulep! :editor evil -everywhere) will return non-nil
  if :editor evil is active without its +everywhere flag.
- `modulep!` now takes multiple flags to simplify AND checks. E.g.

    (and (modulep! +foo)
         (modulep! +bar)
         (not (modulep! +baz)))

  Can now be expressed with:

    (modulep! +foo +bar -baz)
- Adds pcase matchers for `doom-module-context` and `doom-module`
  structs, making the following destructuring binds possible:

    (pcase-dolist ((doom-module group name flags features)
                   (hash-table-values doom-modules))
      ...)

  This will be used more in v3.0.
- Adds file cookie support to module init.el and config.el files.

Here's a summary of breaking changes made in this commit:

- `doom-module-context` was changed from a vector to a struct (record).
- `doom-modules` is now a table of `doom-module` structs, rather than
  free-form plists.
- The following macros have been renamed:
  - `doom-context-with` -> `with-doom-context`
  - `doom-module-context-with` -> `with-doom-module`
- The followings functions have been replaced/removed:
  - `doom-module-context`+`doom-module-context-get` -> `doom-module`
  - `doom-module-set` -> `doom-module--put`
  - `doom-module-p` -> `doom-module-active-p`
  - `doom-module-context-key` (is now a getter with the same name)
  - `doom-module-put` (removed)
  - `doom-module--context-field` (removed)
- The signatures for these functions have changed:
  - `doom-module-get CATEGORY &optional MODULE PROP` ->
    `doom-module-get (GROUP . MODULE) &optional PROP`
  - `doom-module-locate-path CATEGORY &optional MODULE FILE` ->
    `doom-module-locate-path (GROUP . MODULE) &optional FILE`
  - `doom-module-expand-path CATEGORY MODULE &optional FILE` ->
    `doom-module-expand-path (GROUP . MODULE) &optional FILE`
- Adds the following functions
  - `doom-module-exists-p`
  - `doom-module-key`
  - `doom-module->context`
  - `doom-module<-context`
- Removes the following variables
  - `doom-module--empty-context`

This commit results in a little redundancy, which I will address in
parts 2/3 and/or v3.0.
This commit is contained in:
Henrik Lissner
2024-10-19 15:29:56 -04:00
parent b9deb35aab
commit 15904349cf
24 changed files with 443 additions and 320 deletions

View File

@ -251,7 +251,7 @@ SEE ALSO:
;; Load $DOOMDIR/init.el, to read the user's `doom!' block and give users an
;; opportunity to customize the CLI environment, if they like. Otherwise, they
;; can do so in .doomrc or .doomproject.
(load! doom-module-init-file doom-user-dir t)
(doom-modules-initialize)
;; There are a lot of CLIs, and some have expensive initialization, so best we
;; load them lazily.
@ -297,9 +297,9 @@ SEE ALSO:
(let ((cli-file "cli.el"))
(defcli-group! "Module commands"
(doom-context-with 'modules
(with-doom-context 'modules
(dolist (key (doom-module-list))
(when-let (path (doom-module-locate-path (car key) (cdr key) cli-file))
(when-let (path (doom-module-locate-path key cli-file))
(defcli-group! :prefix (if (cdr key) (format "+%s" (cdr key)))
(doom-load (file-name-sans-extension path))))))))

View File

@ -317,18 +317,18 @@ in."
(print! (start "Checking your enabled modules..."))
(advice-add #'require :around #'doom-shut-up-a)
(pcase-dolist (`(,group . ,name) (doom-module-list))
(doom-context-with 'doctor
(with-doom-context 'doctor
(let (doom-local-errors
doom-local-warnings)
(let (doom-doctor--errors
doom-doctor--warnings)
(condition-case-unless-debug ex
(doom-module-context-with (cons group name)
(let ((doctor-file (doom-module-expand-path group name "doctor.el"))
(packages-file (doom-module-expand-path group name doom-module-packages-file)))
(with-doom-module (cons group name)
(let ((doctor-file (doom-module-expand-path (cons group name) "doctor.el"))
(packages-file (doom-module-expand-path (cons group name) doom-module-packages-file)))
(when packages-file
(cl-loop with doom-output-indent = 6
for name in (doom-context-with 'packages
for name in (with-doom-context 'packages
(let* (doom-packages
doom-disabled-packages)
(load packages-file 'noerror 'nomessage)

View File

@ -1878,7 +1878,7 @@ errors to `doom-cli-error-file')."
(when doom-cli--context
(error "Cannot nest `run!' calls"))
(doom-run-hooks 'doom-after-init-hook)
(doom-context-with 'cli
(with-doom-context 'cli
(let* ((args (flatten-list args))
(context (make-doom-cli-context :prefix prefix :whole args))
(doom-cli--context context)

View File

@ -51,7 +51,7 @@
(unless absolute?
(append (cons '* (remq t (reverse doom-context)))
(if (bound-and-true-p doom-module-context)
(let ((key (doom-module-context-key)))
(let ((key (doom-module-context-key doom-module-context)))
(delq nil (list (car key) (cdr key)))))))
":")
args)))
@ -617,6 +617,25 @@ See `general-key-dispatch' for what other arguments it accepts in BRANCHES."
(defalias 'λ! #'cmd!)
(defalias 'λ!! #'cmd!!)
(pcase-defmacro doom-struct (type &rest fields)
`(and (pred (cl-struct-p))
;; TODO: Support `&rest', `&key', and `&optional' in FIELDS
,@(mapcar
(lambda (field)
(let ((offset (cl-struct-slot-offset type field)))
`(app (lambda (it)
,(if offset
`(aref it ,offset)
`(,(intern (format "%s-%s" ',type ',field)) it)))
,field)))
fields)))
(pcase-defmacro doom-module-context (&rest fields)
`(doom-struct doom-module-context ,@fields))
(pcase-defmacro doom-module (&rest fields)
`(doom-struct doom-module ,@fields))
;;; Mutation
(defmacro appendq! (sym &rest lists)

View File

@ -5,8 +5,8 @@
;;
;;; Variables
(defvar doom-modules (make-hash-table :test 'equal)
"A hash table of enabled modules. Set by `doom-initialize-modules'.")
(defvar doom-modules nil
"A table of enabled modules and metadata. See `doom-modules-initialize'.")
(define-obsolete-variable-alias 'doom-modules-dirs 'doom-module-load-path "3.0.0")
(defvar doom-module-load-path
@ -120,114 +120,190 @@ your `doom!' block, a warning is emitted before replacing it with :emacs vc and
:type 'hook)
;;
;;; Types
(cl-defstruct doom-module
"TODO"
(index 0 :read-only t)
;; source
group
name
depth
flags
features
;; sources
path
;; disabled-p
;; frozen-p
;; layer-p
;; recipe
;; alist
;; package
;; if
)
(cl-defstruct doom-module-context
"Hot cache object for the containing Doom module."
index key path flags features)
;;
;;; `doom-module-context'
(defvar doom-module--empty-context [nil nil nil nil nil nil nil])
(defvar doom-module-context (make-doom-module-context)
"A `doom-module-context' for the module associated with the current file.
(eval-and-compile
(put 'doom-module-context 'keys '(:index 0 :initdepth 1 :configdepth 2
:group 3 :name 4 :flags 5 :features 6)))
(defvar doom-module-context doom-module--empty-context
"A vector describing the module associated it with the active context.
Never set this variable directly, use `with-doom-module'.")
Contains the following: [INDEX INITDEPTH CONFIGDEPTH :GROUP MODULE FLAGS FEATURES]
Do not directly set this variable, only let-bind it.")
;; DEPRECATED: Remove this when byte-compilation is introduced to Doom core.
(defmacro doom-module--context-field (field)
(plist-get (get 'doom-module-context 'keys) field))
(defun doom-module-context-get (field &optional context)
"Return the FIELD of CONTEXT.
FIELD should be one of `index', `initdepth', `configdepth', `group', `name',
`flags', or `features'. CONTEXT should be a `doom-module-context' vector. If
omitted, defaults to `doom-module-context'."
(aref (or context doom-module-context)
(plist-get (get 'doom-module-context 'keys)
field)))
(defun doom-module-context (group &optional name)
"Create a `doom-module-context' from a module by GROUP and NAME.
If NAME is omitted, GROUP is treated as a module key cons cell: (GROUP . NAME)."
(declare (side-effect-free t))
(let ((key (if name (cons group name) group)))
(or (get (or (car-safe key) key)
(cdr-safe key))
doom-module--empty-context)))
(defun doom-module-context-key (&optional context)
"Return the module of the active `doom-module-context' as a module key."
(declare (side-effect-free t))
(let ((context (or context doom-module-context)))
(cons (aref context (doom-module--context-field :group))
(aref context (doom-module--context-field :name)))))
(defmacro doom-module-context-with (module-key &rest body)
"Evaluate BODY with `doom-module-context' informed by MODULE-KEY."
(defmacro with-doom-module (key &rest body)
"Evaluate BODY with `doom-module-context' informed by KEY."
(declare (indent 1))
`(let ((doom-module-context (doom-module-context ,module-key)))
`(let ((doom-module-context
(let ((key ,key))
(cond ((null key) (make-doom-module-context))
((doom-module-context-p key) key)
((doom-module-p key) (doom-module->context key))
((doom-module (car key) (cdr key)))
((doom-module->context key))
((error "Invalid module: %S" key))))))
(doom-log ":context:module: =%s" doom-module-context)
,@body))
(defun doom-module<-context (context)
"Return a `doom-module' plist from CONTEXT."
(declare (side-effect-free t))
(doom-module-get (doom-module-context-key context)))
(defun doom-module->context (key)
"Change a `doom-module' into a `doom-module-context'."
(pcase-let
(((doom-module index path flags group name)
(if (doom-module-p key)
key (doom-module-get (doom-module-key key)))))
(make-doom-module-context
:index index
:key (cons group name)
:path path
:flags flags)))
(defun doom-module (group name &optional property)
"Return the `doom-module-context' for any active module by GROUP NAME.
Return its PROPERTY, if specified."
(declare (side-effect-free t))
(when-let ((context (get group name)))
(if property
(aref
context
(or (plist-get
(eval-when-compile
(cl-loop with i = 1
for info in (cdr (cl-struct-slot-info 'doom-module-context))
nconc (list (doom-keyword-intern (symbol-name (car info)))
(prog1 i (cl-incf i)))))
property)
(error "Unknown doom-module-context property: %s" property)))
context)))
;;
;;; Module API
(defun doom-module-p (category module &optional flag)
"Returns t if CATEGORY MODULE is enabled (ie. present in `doom-modules')."
(defun doom-modules-initialize (&optional force?)
"Initializes module metadata."
(when (or (null doom-modules) force?)
(setq doom-modules (make-hash-table :test 'equal))
;; Register Doom's two virtual module categories, representing Doom's core
;; and the user's config; which are always enabled.
(doom-module--put '(:core . nil) :path doom-core-dir :depth -110)
(doom-module--put '(: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--put '(:config . use-package)
:path (doom-module-locate-path '(:config . use-package))
:depth -111)
;; Load $DOOMDIR/init.el, where the user's `doom!' lives, which will inform
;; us of all desired modules.
(doom-load (file-name-concat doom-user-dir doom-module-init-file)
'noerror)))
(defun doom-module-key (key)
"Normalize KEY into a (GROUP . MODULE) tuple representing a Doom module key."
(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))))
(cond ((doom-module-p key)
(cons (doom-module-group key) (doom-module-name key)))
((doom-module-context-p key)
(doom-module-context-key key))
((car-safe key)
(if (nlistp (cdr-safe key))
key
(cons (car key) (cadr key))))
((error "Invalid key: %S" key))))
(defun doom-module-depth (category module &optional initdepth?)
"Return the depth of CATEGORY MODULE.
(defun doom-module--has-flag-p (flags wanted-flags)
"Return t if the list of WANTED-FLAGS satisfies the list of FLAGS."
(declare (pure t) (side-effect-free error-free))
(cl-loop with flags = (ensure-list flags)
for flag in (ensure-list wanted-flags)
for flagstr = (symbol-name flag)
if (if (eq ?- (aref flagstr 0))
(memq (intern (concat "+" (substring flagstr 1)))
flags)
(not (memq flag flags)))
return nil
finally return t))
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--fold-flags (flags)
"Returns a collapsed list of FLAGS (a list of +/- prefixed symbols).
(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))
FLAGS is read in sequence, cancelling out negated flags and removing
duplicates."
(declare (pure t) (side-effect-free error-free))
(let (newflags)
(while flags
(let* ((flag (car flags))
(flagstr (symbol-name flag)))
(when-let ((sym (intern-soft
(concat (if (eq ?- (aref flagstr 0)) "+" "-")
(substring flagstr 1)))))
(setq newflags (delq sym newflags)))
(cl-pushnew flag newflags :test 'eq))
(setq flags (cdr flags)))
(nreverse newflags)))
(defun doom-module-get (key &optional property)
"Returns the plist for GROUP MODULE. Gets PROPERTY, specifically, if set."
(declare (side-effect-free t))
(when-let ((m (gethash key doom-modules)))
(if property
(plist-get plist property)
plist)))
(aref
m (or (plist-get
(eval-when-compile
(cl-loop with i = 1
for info in (cdr (cl-struct-slot-info 'doom-module))
nconc (list (doom-keyword-intern (symbol-name (car info)))
(prog1 i (cl-incf i)))))
property)
(error "Unknown doom-module property: %s" property)))
m)))
(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.
(cl-defun doom-module--put ((group . name) &rest plist)
"Enable GROUP NAME and associate PLIST with it.
\(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:
This enables the target module, where GROUP is a keyword, NAME is a symbol, and
PLIST is a property list accepting none, any, or all of the following
properties:
:group KEYWORD
Indicating the group this module is in. This doesn't have to match GROUP, as
it could indicate a module alias.
:name SYMBOL
Indicating the name of this module. This doesn't have to match NAME, as it
could indicate a module alias.
:path STRING
Path to the directory where this module lives.
:depth INT|(INITDEPTH . CONFIGDEPTH)
@ -235,32 +311,66 @@ following properties:
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.
A list of activated flags for this module. Will be collapsed into
pre-existing flags for the module.
:features (SYMBOL...)
A list of active features, determined from module's metadata. NOT
IMPLEMENTED YET.
A list of active features, determined from the module's metadata. Will be
collapsed into any pre-existing features for the module. NOT IMPLEMENTED
YET.
If PLIST consists of a single nil, the module is purged from memory instead."
(if (car plist)
(let* ((depth (ensure-list (or (plist-get plist :depth) 0)))
(idepth (or (cdr depth) (car depth)))
(cdepth (car depth))
(idx (hash-table-count doom-modules)))
\(fn (GROUP . NAME) &key GROUP NAME PATH DEPTH FLAGS FEATURES)"
(let ((module
(make-doom-module
:index (hash-table-count doom-modules)
:group (or (plist-get plist :group) group)
:name (or (plist-get plist :name) name)
:path (plist-get plist :path)
:flags (plist-get plist :flags)
:features () ; TODO
:depth
(if (not (plist-member plist :depth))
'(0 . 0)
(let ((depth (plist-get plist :depth)))
(cl-check-type depth (or integer cons))
(cond ((integerp depth) (cons depth depth))
((consp depth) (cons (or (car depth) 0)
(or (cdr depth) 0)))
((error "Invalid DEPTH value: %S" depth))))))))
(doom-log 2 "module-put: %s" module)
(prog1 (puthash (cons group name) module doom-modules)
;; 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. The structure of this cache
;; should match `doom-module-context's.
(put category module
(vector idx idepth cdepth
category module
(plist-get plist :flags)
(plist-get plist :features)))
;; 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)))
;; 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 group name (doom-module->context module)))))
(defun doom-module-active-p (group module &optional flags)
"Return t if GROUP MODULE is active, and with FLAGS (if given)."
(declare (side-effect-free t))
(when-let ((val (doom-module-get (cons group module) (if flags :flags))))
(or (null flags)
(doom-module--has-flag-p flags val))))
(defun doom-module-exists-p (group module)
"Returns t if GROUP MODULE is present in any active source."
(declare (side-effect-free t))
(if (doom-module-get group module) t))
(cl-defun doom-module--depth< (keya keyb &optional initorder?)
"Return t if module with KEY-A comes before another with KEY-B.
If INITORDER? is non-nil, grab the car of the module's :depth, rather than it's
cdr. See `doom-module-put' for details about the :depth property."
(declare (pure t) (side-effect-free t))
(let* ((adepth (doom-module-get keya :depth))
(bdepth (doom-module-get keyb :depth))
(adepth (if initorder? (car adepth) (cdr adepth)))
(bdepth (if initorder? (car bdepth) (cdr bdepth))))
(if (or (null adepth) (null bdepth)
(= adepth bdepth))
(< (or (doom-module-get keya :index) 0)
(or (doom-module-get keyb :index) 0))
(< adepth bdepth))))
(defun doom-module-list (&optional paths-or-all initorder?)
"Return a list of (:group . name) module keys in order of their :depth.
@ -272,8 +382,7 @@ underneath it. If non-nil, return the same, but search `doom-module-load-path'
: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."
If INITORDER? is non-nil, sort modules by the CAR of that module's :depth."
(sort (if paths-or-all
(delete-dups
(append (seq-remove #'cdr (doom-module-list nil initorder?))
@ -285,72 +394,64 @@ configdepth. See `doom-module-set' for details."
:mindepth 1
:depth 1)))
(hash-table-keys doom-modules))
(let ((idx (if initorder? 1 2)))
(lambda! ((groupa . namea) (groupb . nameb))
(let ((a (get groupa namea))
(b (get groupb nameb)))
(or (null b)
(and
a (let ((adepth (aref a idx))
(bdepth (aref b idx)))
(if (= adepth bdepth)
(< (aref a 0) (aref b 0))
(< adepth bdepth))))))))))
(doom-rpartial #'doom-module--depth< initorder?)))
(defun doom-module-expand-path (category module &optional file)
"Expands a path to FILE relative to CATEGORY and MODULE.
(defun doom-module-expand-path (key &optional file)
"Expands a path to FILE relative to KEY, a cons cell: (GROUP . NAME)
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))
GROUP is a keyword. MODULE is a symbol. FILE is an optional string path.
If the group isn't enabled this returns nil. For finding disabled modules use
`doom-module-locate-path' instead."
(when-let ((path (doom-module-get key :path)))
(if file
(file-name-concat path file)
path)))
(defun doom-module-locate-path (category &optional module file)
"Searches `doom-module-load-path' to find the path to a module.
(defun doom-module-locate-path (key &optional file)
"Searches `doom-module-load-path' to find the path to a module by KEY.
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."
KEY is a cons cell (GROUP . NAME), where GROUP is a keyword (e.g. :lang) and
NAME is a symbol (e.g. \\='python). FILE is a string that will be appended to
the resulting path. If said path doesn't exist, this returns nil, otherwise an
absolute path."
(let (file-name-handler-alist)
(if-let (path (doom-module-expand-path category module file))
(if-let ((path (doom-module-expand-path key file)))
(if (or (null file)
(file-exists-p path))
path)
(let* ((category (doom-keyword-name category))
(cl-destructuring-bind (group . module) (doom-module-key key)
(let* ((group (doom-keyword-name group))
(module (if module (symbol-name module)))
(path (file-name-concat category module file)))
(path (file-name-concat group 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-module-load-path '("" ".elc" ".el"))
(cl-loop for default-directory in doom-module-load-path
if (file-exists-p path)
return (expand-file-name 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 for (group . name) in (or module-list (doom-module-list))
if (doom-module-locate-path group name file)
(cl-loop for key in (or module-list (doom-module-list))
if (doom-module-locate-path key 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."
(defun doom-module-from-path (path &optional enabled-only?)
"Returns a cons cell (GROUP . NAME) 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))))
(when-let* ((group (doom-keyword-intern (match-string 1 path)))
(name (intern (match-string 2 path))))
(and (or (null enabled-only?)
(doom-module-active-p group name))
(cons group name))))
((string-match (concat "^" (regexp-quote doom-core-dir)) path)
(cons :core nil))
((string-match (concat "^" (regexp-quote doom-user-dir)) path)
@ -363,9 +464,8 @@ 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 with module-load-path = (or module-load-path doom-module-load-path)
for (cat . mod) in (doom-module-list module-load-path)
collect (doom-module-locate-path cat mod)))
(mapcar #'doom-module-locate-path
(doom-module-list (or module-load-path doom-module-load-path))))
(defun doom-module-mplist-map (fn mplist)
"Apply FN to each module in MPLIST."
@ -373,14 +473,14 @@ those directories."
(inhibit-message doom-inhibit-module-warnings)
obsolete
results
category m)
group m)
(while mplist
(setq m (pop mplist))
(cond ((keywordp m)
(setq category m
(setq group m
obsolete (assq m doom-obsolete-modules)))
((null category)
(error "No module category specified for %s" m))
((null group)
(error "No module group specified for %s" m))
((and (listp m) (keywordp (car m)))
(pcase (car m)
(:cond
@ -402,12 +502,12 @@ those directories."
(print! (warn "%s module was removed"))
(if (cdr newkeys)
(print! (warn "%s module was removed and split into the %s modules")
(list category module)
(list group module)
(mapconcat #'prin1-to-string newkeys ", "))
(print! (warn "%s module was moved to %s")
(list category module)
(list group module)
(car newkeys)))
(push category mplist)
(push group mplist)
(dolist (key newkeys)
(push (if flags
(nconc (cdr key) flags)
@ -415,7 +515,11 @@ those directories."
mplist)
(push (car key) mplist))
(throw 'doom-modules t))))
(push (funcall fn category module :flags (if (listp m) (cdr m)))
(doom-log "module: %s %s %s -> %s" group module (or flags "")
(doom-module-locate-path (cons group module)))
(push (funcall fn (cons group module)
:flags (if (listp m) (cdr m))
:path (doom-module-locate-path (cons group module)))
results))))))
(when noninteractive
(setq doom-inhibit-module-warnings t))
@ -433,84 +537,66 @@ those directories."
"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).
otherwise, MODULES is a variadic-property list (a plist whose key may be
followed by one or more 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)."
This macro does nothing in interactive sessions, but in noninteractive session
iterates through MODULES, enabling and initializing them. The order of modules
in these blocks dictates their load order (unless given an explicit :depth)."
`(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)))
#'doom-module--put
,@(if (keywordp (car modules))
(list (list 'quote modules))
modules))
doom-modules))
t))
;; DEPRECATED Remove in 3.0
(define-obsolete-function-alias 'featurep! 'modulep! "3.0.0")
(defmacro modulep! (category &optional module flag)
"Return t if :CATEGORY MODULE (and +FLAGS) are enabled.
(defmacro modulep! (group &optional module &rest flags)
"Return t if :GROUP MODULE (and +FLAGS) are enabled.
If FLAG is provided, returns t if CATEGORY MODULE has FLAG enabled.
If FLAGS is provided, returns t if GROUP MODULE has all of FLAGS enabled.
(modulep! :config default +flag)
(modulep! :config default +flag1 +flag2 +flag3)
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:
GROUP 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! +flag3 +flag1 +flag2)
(modulep! +flag)
FLAGS can be negated. E.g. This will return non-nil if ':tools lsp' is enabled
without `+eglot':
(modulep! :tools lsp -eglot)
To interpolate dynamic values, use comma:
(let ((flag '-eglot))
(modulep! :tools lsp ,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-module--empty-context)
(doom-module--context-field :flags))))
(module (get category module))
((aref doom-module-context 0)
(memq category (aref doom-module-context
(doom-module--context-field :flags))))
((let ((file
;; This must be expanded at the call site, not in
;; `modulep!'s definition, to get the file we want.
(macroexpand '(file!))))
(if-let (module (doom-module-from-path file))
(memq category (aref (or (get (car module) (cdr module))
doom-module--empty-context)
(doom-module--context-field :flags)))
(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)
(if (keywordp group)
(if flags
`(doom-module--has-flag-p
(doom-module (backquote ,group) (backquote ,module) :flags)
(backquote ,flags))
`(and (get (backquote ,group) (backquote ,module)) t))
(let ((flags (delq nil (cons group (cons module flags)))))
(if (doom-module-context-index doom-module-context)
`(doom-module--has-flag-p
',(doom-module-context-flags doom-module-context)
(backquote ,flags))
`(let ((file (file!)))
(if-let ((module (doom-module-from-path file)))
(doom-module--has-flag-p
(doom-module (car module) (cdr module) :flags)
(backquote ,flags))
(error "(modulep! %s) couldn't resolve current module from %s"
(backquote ,flags) (abbreviate-file-name file))))))))
(provide 'doom-modules)
;;; doom-modules.el ends here

View File

@ -431,13 +431,13 @@ installed."
(push (cons
name (plist-put
plist :modules
(list (doom-module-context-key))))
(list (doom-module-context-key doom-module-context))))
doom-packages)))))))))
(user-error
(user-error (error-message-string e)))
(error
(signal 'doom-package-error
(list (doom-module-context-key)
(list (doom-module-context-key doom-module-context)
file e)))))
(defun doom-package-list (&optional module-list)
@ -454,11 +454,11 @@ also be a list of module keys."
doom-disabled-packages
doom-packages)
(letf! (defun read-packages (key)
(doom-module-context-with key
(with-doom-module key
(when-let (file (doom-module-locate-path
(car key) (cdr key) doom-module-packages-file))
key doom-module-packages-file))
(doom-packages--read file nil 'noerror))))
(doom-context-with 'packages
(with-doom-context 'packages
(let ((user? (assq :user module-list)))
(when user?
;; We load the private packages file twice to populate

View File

@ -389,50 +389,54 @@ Defaults to the profile at `doom-profile-default'."
(let* ((init-modules-list (doom-module-list nil t))
(config-modules-list (doom-module-list))
(pre-init-modules
(seq-filter (fn! (<= (doom-module-depth (car %) (cdr %) t) -100))
(remove '(:user) init-modules-list)))
(seq-filter (fn! (<= (car (doom-module-get % :depth)) -100))
(remove '(:user . nil) init-modules-list)))
(init-modules
(seq-filter (fn! (<= 0 (doom-module-depth (car %) (cdr %) t) 100))
(seq-filter (fn! (<= 0 (car (doom-module-get % :depth)) 100))
init-modules-list))
(config-modules
(seq-filter (fn! (<= 0 (doom-module-depth (car %) (cdr %)) 100))
(seq-filter (fn! (<= 0 (cdr (doom-module-get % :depth)) 100))
config-modules-list))
(post-config-modules
(seq-filter (fn! (>= (doom-module-depth (car %) (cdr %)) 100))
(seq-filter (fn! (>= (cdr (doom-module-get % :depth)) 100))
config-modules-list))
(init-file doom-module-init-file)
(config-file doom-module-config-file))
(letf! ((defun module-loader (group name file &optional noerror)
(doom-module-context-with (cons group name)
`(let ((doom-module-context ,doom-module-context))
(doom-load
,(pcase (cons group name)
(letf! ((defun module-loader (key file)
(let ((noextfile (file-name-sans-extension file)))
`(with-doom-module ',key
,(pcase key
('(:core . nil)
`(file-name-concat
doom-core-dir ,(file-name-nondirectory (file-name-sans-extension file))))
`(doom-load
(file-name-concat
doom-core-dir ,(file-name-nondirectory noextfile))
t))
('(:user . nil)
`(file-name-concat
doom-user-dir ,(file-name-nondirectory (file-name-sans-extension file))))
(_ (abbreviate-file-name (file-name-sans-extension file))))
t))))
(defun module-list-loader (modules file &optional noerror)
(cl-loop for (cat . mod) in modules
if (doom-module-locate-path cat mod file)
collect (module-loader cat mod it noerror))))
`(doom-load
(file-name-concat
doom-user-dir ,(file-name-nondirectory noextfile))
t))
(_
(when (doom-file-cookie-p file "if" t)
`(doom-load ,(abbreviate-file-name noextfile) t)))))))
(defun module-list-loader (modules file)
(cl-loop for key in modules
if (doom-module-locate-path key file)
collect (module-loader key it))))
;; FIX: Same as above (see `doom-profile--generate-init-vars').
`((if (or (doom-context-p 'init)
(doom-context-p 'reload))
(doom-context-with 'modules
(with-doom-context 'modules
(set 'doom-modules ',doom-modules)
(set 'doom-disabled-packages ',doom-disabled-packages)
;; Cache module state and flags in symbol plists for quick lookup by
;; `modulep!' later.
;; Cache module state and flags in symbol plists for quick lookup
;; by `modulep!' later.
,@(cl-loop
for (category . modules) in (seq-group-by #'car config-modules-list)
collect
`(setplist ',category
(quote ,(cl-loop for (_ . module) in modules
nconc `(,module ,(get category module))))))
nconc `(,module ,(doom-module->context (cons category module)))))))
(let ((old-custom-file custom-file))
,@(module-list-loader pre-init-modules init-file)
(doom-run-hooks 'doom-before-modules-init-hook)
@ -441,7 +445,7 @@ Defaults to the profile at `doom-profile-default'."
(doom-run-hooks 'doom-before-modules-config-hook)
,@(module-list-loader config-modules config-file)
(doom-run-hooks 'doom-after-modules-config-hook)
,@(module-list-loader post-config-modules config-file t)
,@(module-list-loader post-config-modules config-file)
(when (eq custom-file old-custom-file)
(doom-load custom-file 'noerror)))))))))

View File

@ -601,7 +601,7 @@ wasn't active when this was called."
(doom-log ":context: -%s %s" context doom-context)
(setq doom-context (delq context doom-context))))
(defmacro doom-context-with (contexts &rest body)
(defmacro with-doom-context (contexts &rest body)
"Evaluate BODY with CONTEXTS added to `doom-context'."
(declare (indent 1))
`(let ((doom-context doom-context))

View File

@ -148,9 +148,13 @@ hoist buggy forms into autoloads.")
;; So `autoload-generate-file-autoloads' knows where to write it
(target-buffer (current-buffer))
(module (doom-module-from-path file))
(generated-autoload-load-name (abbreviate-file-name (file-name-sans-extension file)))
(module-enabled-p (and (doom-module-p (car module) (cdr module))
(doom-file-cookie-p file "if" t))))
(generated-autoload-load-name
(abbreviate-file-name (file-name-sans-extension file)))
(module-enabled-p
(and (doom-module-active-p (car module) (cdr module))
(doom-file-cookie-p file "if" t)))
;; (load-path (cons doom-modules-dir load-path))
)
(save-excursion
(when module-enabled-p
(quiet! (autoload-generate-file-autoloads file target-buffer)))

View File

@ -104,7 +104,7 @@ Runs `doom-after-reload-hook' afterwards."
(interactive)
(mapc #'require (cdr doom-incremental-packages))
(doom--if-compile doom-reload-command
(doom-context-with '(reload modules)
(with-doom-context '(reload modules)
(doom-run-hooks 'doom-before-reload-hook)
(doom-load (file-name-concat doom-user-dir doom-module-init-file) t)
(with-demoted-errors "PRIVATE CONFIG ERROR: %s"
@ -129,7 +129,7 @@ line."
(interactive)
(require 'doom-profiles)
;; TODO: Make this more robust
(doom-context-with 'reload
(with-doom-context 'reload
(dolist (file (mapcar #'car doom-profile-generators))
(when (string-match-p "/[0-9]+-loaddefs[.-]" file)
(load (doom-path doom-profile-dir doom-profile-init-dir-name file)
@ -145,7 +145,7 @@ Doing so from within Emacs will taint your shell environment.
An envvar file contains a snapshot of your shell environment, which can be
imported into Emacs."
(interactive)
(doom-context-with 'reload
(with-doom-context 'reload
(let ((default-directory doom-emacs-dir))
(with-temp-buffer
(doom-load-envvars-file doom-env-file)

View File

@ -317,8 +317,8 @@ ready to be pasted in a bug report on github."
do (setq lastcat cat)
and collect lastcat
collect
(let* ((flags (doom-module-get lastcat mod :flags))
(path (doom-module-get lastcat mod :path))
(let* ((flags (doom-module-get (cons lastcat mod) :flags))
(path (doom-module-get (cons lastcat mod) :path))
(module
(append
(cond ((null path)

View File

@ -144,7 +144,7 @@ MATCH is a string regexp. Only entries that match it will be included."
result)))
;;;###autoload
(defun doom-file-cookie-p (file &optional cookie null-value)
(defun doom-file-cookie (file &optional cookie null-value)
"Returns the evaluated result of FORM in a ;;;###COOKIE FORM at the top of
FILE.
@ -158,11 +158,24 @@ return NULL-VALUE."
(insert-file-contents file nil 0 256)
(if (re-search-forward (format "^;;;###%s " (regexp-quote (or cookie "if")))
nil t)
(doom-module-context-with (doom-module-from-path file)
(let ((load-file-name file))
(eval (sexp-at-point) t)))
(sexp-at-point)
null-value)))
;;;###autoload
(defun doom-file-cookie-p (file &optional cookie null-value)
"Returns the evaluated result of FORM in a ;;;###COOKIE FORM at the top of
FILE.
If COOKIE doesn't exist, or cookie isn't within the first 256 bytes of FILE,
return NULL-VALUE."
(let ((sexp (doom-file-cookie file cookie null-value)))
(if (equal sexp null-value)
null-value
(with-temp-buffer
(with-doom-module (doom-module-from-path file)
(let ((load-file-name file))
(eval (doom-file-cookie file cookie null-value) t)))))))
;;;###autoload
(defmacro file-exists-p! (files &optional directory)
"Returns non-nil if the FILES in DIRECTORY all exist.

View File

@ -344,10 +344,10 @@ without needing to check if they are available."
(defun doom--help-modules-list ()
(cl-loop for (cat . mod) in (doom-module-list 'all)
for readme-path = (or (doom-module-locate-path cat mod "README.org")
(doom-module-locate-path cat mod))
for readme-path = (or (doom-module-locate-path (cons cat mod) "README.org")
(doom-module-locate-path (cons cat mod)))
for format = (if mod (format "%s %s" cat mod) (format "%s" cat))
if (doom-module-p cat mod)
if (doom-module-active-p cat mod)
collect (list format readme-path)
else if (and cat mod)
collect (list (propertize format 'face 'font-lock-comment-face)
@ -630,8 +630,7 @@ If prefix arg is present, refresh the cache."
(:core doom-core-dir)
(:user doom-user-dir)
(category
(doom-module-locate-path category
(cdr m)))))
(doom-module-locate-path (cons category (cdr m))))))
(readme-path (expand-file-name "README.org" module-path)))
(insert indent)
(doom--help-insert-button

View File

@ -49,8 +49,8 @@
(or buffer-file-name
(bound-and-true-p org-src-source-file-name)))
(package
(doom-context-with 'packages
(doom-module-context-with (doom-module-from-path buffer-file-name)
(with-doom-context 'packages
(with-doom-module (doom-module-from-path buffer-file-name)
(eval (sexp-at-point) t)))))
(list :beg beg
:end end
@ -164,14 +164,14 @@ each package."
(list (intern (car module))
(ignore-errors (intern (cadr module)))
current-prefix-arg)))
(mapc (lambda! ((cat . mod))
(if-let (packages-file (doom-module-locate-path cat mod doom-module-packages-file))
(mapc (lambda! (key)
(if-let (packages-file (doom-module-locate-path key doom-module-packages-file))
(with-current-buffer
(or (get-file-buffer packages-file)
(find-file-noselect packages-file))
(doom/bump-packages-in-buffer select)
(save-buffer))
(message "Module %s has no packages.el file" (cons cat mod))))
(message "Module %s has no packages.el file" key)))
(if module
(list (cons category module))
(cl-remove-if-not (lambda (m) (eq (car m) category))
@ -188,7 +188,7 @@ each package."
(unless modules
(user-error "This package isn't installed by any Doom module"))
(dolist (module modules)
(when (doom-module-locate-path (car module) (cdr module) doom-module-packages-file)
(when (doom-module-locate-path module doom-module-packages-file)
(doom/bump-module (car module) (cdr module))))))

View File

@ -151,7 +151,7 @@ If DIR is not a project, it will be indexed (but not cached)."
;; Intentionally avoid `helm-projectile-find-file', because it runs
;; asynchronously, and thus doesn't see the lexical
;; `default-directory'
(if (doom-module-p :completion 'ivy)
(if (doom-module-active-p :completion 'ivy)
#'counsel-projectile-find-file
#'projectile-find-file)))
((and (bound-and-true-p ivy-mode)
@ -175,9 +175,9 @@ If DIR is not a project, it will be indexed (but not cached)."
"Traverse a file structure starting linearly from DIR."
(let ((default-directory (file-truename (expand-file-name dir))))
(call-interactively
(cond ((doom-module-p :completion 'ivy)
(cond ((doom-module-active-p :completion 'ivy)
#'counsel-find-file)
((doom-module-p :completion 'helm)
((doom-module-active-p :completion 'helm)
#'helm-find-files)
(#'find-file)))))

View File

@ -2,6 +2,6 @@
;;; completion/helm/doctor.el
(dolist (module '(ivy ido vertico))
(when (doom-module-p :completion module)
(when (doom-module-active-p :completion module)
(error! "This module is incompatible with :completion %s; disable one or the other"
module)))

View File

@ -2,6 +2,6 @@
;;; completion/ido/doctor.el
(dolist (module '(helm ivy vertico))
(when (doom-module-p :completion module)
(when (doom-module-active-p :completion module)
(error! "This module is incompatible with :completion %s; disable one or the other"
module)))

View File

@ -2,6 +2,6 @@
;;; completion/ivy/doctor.el
(dolist (module '(helm ido vertico))
(when (doom-module-p :completion module)
(when (doom-module-active-p :completion module)
(error! "This module is incompatible with :completion %s; disable one or the other"
module)))

View File

@ -2,7 +2,7 @@
;;; completion/vertico/doctor.el
(dolist (module '(ivy helm ido))
(when (doom-module-p :completion module)
(when (doom-module-active-p :completion module)
(error! "This module is incompatible with :completion %s; disable one or the other"
module)))

View File

@ -1,4 +1,5 @@
;;; editor/evil/init.el -*- lexical-binding: t; -*-
;;;###if (modulep! +everywhere)
(defvar evil-collection-key-blacklist)
@ -22,9 +23,7 @@
;; disable modules, and to reduce the effort required to maintain our copy of
;; `evil-collection-list' (now I can just copy it from time to time).
(when (and (not noninteractive)
(not (doom-context-p 'reload))
(modulep! +everywhere))
(unless (or noninteractive (doom-context-p 'reload))
(setq evil-collection-company-use-tng (modulep! :completion company +tng))

View File

@ -13,11 +13,11 @@ to a pop up buffer."
(debug-on-error t))
(unwind-protect
(condition-case-unless-debug e
(doom-module-context-with
(with-doom-module
(doom-module-from-path
(or (buffer-file-name (buffer-base-buffer))
default-directory))
(doom-context-with 'eval
(with-doom-context 'eval
(eval-region beg end buffer load-read-function))
(with-current-buffer buffer
(let ((pp-max-width nil))

View File

@ -157,12 +157,11 @@ exist, and `org-link' otherwise."
(cl-destructuring-bind (&key category module flag)
(+org-link--read-module-spec module-path)
(when category
(let ((doom-modules-dirs (list doom-modules-dir)))
(if-let* ((path (doom-module-locate-path category module))
(if-let* ((path (doom-module-locate-path (cons category module)))
(path (or (car (doom-glob path "README.org"))
path)))
(find-file path)
(user-error "Can't find Doom module '%s'" module-path))))
(user-error "Can't find Doom module '%s'" module-path)))
(when flag
(goto-char (point-min))
(when (and (re-search-forward "^\\*+ \\(?:TODO \\)?Module flags")
@ -179,13 +178,13 @@ exist, and `org-link' otherwise."
(cl-destructuring-bind (&key category module flag)
(+org-link--read-module-spec module-path)
(let ((overall-face
(if (and category (doom-module-locate-path category module))
(if (and category (doom-module-locate-path (cons category module)))
'((:underline nil) org-link org-block bold)
'(shadow org-block bold)))
(icon-face
(cond
((doom-module-p category module flag) 'success)
((and category (doom-module-locate-path category module)) 'warning)
((doom-module-active-p category module flag) 'success)
((and category (doom-module-locate-path (cons category module))) 'warning)
(t 'error))))
(add-text-properties
start end
@ -294,9 +293,9 @@ exist, and `org-link' otherwise."
(cl-destructuring-bind (&key category module flag)
(+org-link--read-module-spec (org-element-property :path link))
(cond
((doom-module-p category module)
((doom-module-active-p category module)
(propertize "enabled" 'face 'success))
((and category (doom-module-locate-path category module))
((and category (doom-module-locate-path (cons category module)))
(propertize "disabled" 'face 'error))
(t (propertize "unknown" 'face '(bold error)))))))
("doom-executable"

View File

@ -1301,7 +1301,7 @@ between the two."
))
;;; Custom org modules
(dolist (flag (doom-module-context-get :flags))
(dolist (flag (doom-module :lang 'org :flags))
(load! (concat "contrib/" (substring (symbol-name flag) 1)) nil t))
;; Add our general hooks after the submodules, so that any hooks the

View File

@ -119,7 +119,7 @@
:config
(pcase-dolist (`((,category . ,modules) :after ,after :require ,libs)
+debugger--dap-alist)
(when (doom-module-p category (car modules) (cadr modules))
(when (doom-module-active-p category (car modules) (cadr modules))
(dolist (lib (ensure-list after))
(with-eval-after-load lib
(mapc #'require (ensure-list libs))))))