refactor!: restructure Doom core

BREAKING CHANGE: This restructures the project in preparation for Doom
to be split into two repos. Users that have reconfigured Doom's CLI
stand a good chance of seeing breakage, especially if they've referred
to any core-* feature, e.g.

  (after! core-cli-ci ...)

To fix it, simply s/core-/doom-/, i.e.

  (after! doom-cli-ci ...)

What this commit specifically changes is:
- Renames all core features from core-* to doom-*
- Moves core/core-* -> lisp/doom-*
- Moves core/autoloads/* -> lisp/lib/*
- Moves core/templates -> templates/

Ref: #4273
This commit is contained in:
Henrik Lissner 2022-07-30 21:49:00 +02:00
parent a9866e37e4
commit b9933e6637
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
69 changed files with 147 additions and 145 deletions

393
lisp/lib/buffers.el Normal file
View file

@ -0,0 +1,393 @@
;;; lisp/lib/buffers.el -*- lexical-binding: t; -*-
;;;###autoload
(defvar doom-real-buffer-functions
'(doom-dired-buffer-p)
"A list of predicate functions run to determine if a buffer is real, unlike
`doom-unreal-buffer-functions'. They are passed one argument: the buffer to be
tested.
Should any of its function returns non-nil, the rest of the functions are
ignored and the buffer is considered real.
See `doom-real-buffer-p' for more information.")
;;;###autoload
(defvar doom-unreal-buffer-functions
'(minibufferp doom-special-buffer-p doom-non-file-visiting-buffer-p)
"A list of predicate functions run to determine if a buffer is *not* real,
unlike `doom-real-buffer-functions'. They are passed one argument: the buffer to
be tested.
Should any of these functions return non-nil, the rest of the functions are
ignored and the buffer is considered unreal.
See `doom-real-buffer-p' for more information.")
;;;###autoload
(defvar-local doom-real-buffer-p nil
"If non-nil, this buffer should be considered real no matter what. See
`doom-real-buffer-p' for more information.")
;;;###autoload
(defvar doom-fallback-buffer-name "*scratch*"
"The name of the buffer to fall back to if no other buffers exist (will create
it if it doesn't exist).")
;;
;;; Functions
;;;###autoload
(defun doom-buffer-frame-predicate (buf)
"To be used as the default frame buffer-predicate parameter. Returns nil if
BUF should be skipped over by functions like `next-buffer' and `other-buffer'."
(or (doom-real-buffer-p buf)
(eq buf (doom-fallback-buffer))))
;;;###autoload
(defun doom-fallback-buffer ()
"Returns the fallback buffer, creating it if necessary. By default this is the
scratch buffer. See `doom-fallback-buffer-name' to change this."
(let (buffer-list-update-hook)
(get-buffer-create doom-fallback-buffer-name)))
;;;###autoload
(defalias 'doom-buffer-list #'buffer-list)
;;;###autoload
(defun doom-project-buffer-list (&optional project)
"Return a list of buffers belonging to the specified PROJECT.
If PROJECT is nil, default to the current project.
If no project is active, return all buffers."
(let ((buffers (doom-buffer-list)))
(if-let* ((project-root
(if project (expand-file-name project)
(doom-project-root))))
(cl-loop for buf in buffers
if (projectile-project-buffer-p buf project-root)
collect buf)
buffers)))
;;;###autoload
(defun doom-open-projects ()
"Return a list of projects with open buffers."
(cl-loop with projects = (make-hash-table :test 'equal :size 8)
for buffer in (doom-buffer-list)
if (buffer-live-p buffer)
if (doom-real-buffer-p buffer)
if (with-current-buffer buffer (doom-project-root))
do (puthash (abbreviate-file-name it) t projects)
finally return (hash-table-keys projects)))
;;;###autoload
(defun doom-dired-buffer-p (buf)
"Returns non-nil if BUF is a dired buffer."
(provided-mode-derived-p (buffer-local-value 'major-mode buf)
'dired-mode))
;;;###autoload
(defun doom-special-buffer-p (buf)
"Returns non-nil if BUF's name starts and ends with an *."
(equal (substring (buffer-name buf) 0 1) "*"))
;;;###autoload
(defun doom-temp-buffer-p (buf)
"Returns non-nil if BUF is temporary."
(equal (substring (buffer-name buf) 0 1) " "))
;;;###autoload
(defun doom-visible-buffer-p (buf)
"Return non-nil if BUF is visible."
(get-buffer-window buf))
;;;###autoload
(defun doom-buried-buffer-p (buf)
"Return non-nil if BUF is not visible."
(not (doom-visible-buffer-p buf)))
;;;###autoload
(defun doom-non-file-visiting-buffer-p (buf)
"Returns non-nil if BUF does not have a value for `buffer-file-name'."
(not (buffer-file-name buf)))
;;;###autoload
(defun doom-real-buffer-list (&optional buffer-list)
"Return a list of buffers that satisfy `doom-real-buffer-p'."
(cl-remove-if-not #'doom-real-buffer-p (or buffer-list (doom-buffer-list))))
;;;###autoload
(defun doom-real-buffer-p (buffer-or-name)
"Returns t if BUFFER-OR-NAME is a 'real' buffer.
A real buffer is a useful buffer; a first class citizen in Doom. Real ones
should get special treatment, because we will be spending most of our time in
them. Unreal ones should be low-profile and easy to cast aside, so we can focus
on real ones.
The exact criteria for a real buffer is:
1. A non-nil value for the buffer-local value of the `doom-real-buffer-p'
variable OR
2. Any function in `doom-real-buffer-functions' returns non-nil OR
3. None of the functions in `doom-unreal-buffer-functions' must return
non-nil.
If BUFFER-OR-NAME is omitted or nil, the current buffer is tested."
(or (bufferp buffer-or-name)
(stringp buffer-or-name)
(signal 'wrong-type-argument (list '(bufferp stringp) buffer-or-name)))
(when-let (buf (get-buffer buffer-or-name))
(when-let (basebuf (buffer-base-buffer buf))
(setq buf basebuf))
(and (buffer-live-p buf)
(not (doom-temp-buffer-p buf))
(or (buffer-local-value 'doom-real-buffer-p buf)
(run-hook-with-args-until-success 'doom-real-buffer-functions buf)
(not (run-hook-with-args-until-success 'doom-unreal-buffer-functions buf))))))
;;;###autoload
(defun doom-unreal-buffer-p (buffer-or-name)
"Return t if BUFFER-OR-NAME is an 'unreal' buffer.
See `doom-real-buffer-p' for details on what that means."
(not (doom-real-buffer-p buffer-or-name)))
;;;###autoload
(defun doom-buffers-in-mode (modes &optional buffer-list derived-p)
"Return a list of buffers whose `major-mode' is `eq' to MODE(S).
If DERIVED-P, test with `derived-mode-p', otherwise use `eq'."
(let ((modes (doom-enlist modes)))
(cl-remove-if-not (if derived-p
(lambda (buf)
(apply #'provided-mode-derived-p
(buffer-local-value 'major-mode buf)
modes))
(lambda (buf)
(memq (buffer-local-value 'major-mode buf) modes)))
(or buffer-list (doom-buffer-list)))))
;;;###autoload
(defun doom-visible-windows (&optional window-list)
"Return a list of the visible, non-popup (dedicated) windows."
(cl-loop for window in (or window-list (window-list))
when (or (window-parameter window 'visible)
(not (window-dedicated-p window)))
collect window))
;;;###autoload
(defun doom-visible-buffers (&optional buffer-list)
"Return a list of visible buffers (i.e. not buried)."
(let ((buffers (delete-dups (mapcar #'window-buffer (window-list)))))
(if buffer-list
(cl-delete-if (lambda (b) (memq b buffer-list))
buffers)
(delete-dups buffers))))
;;;###autoload
(defun doom-buried-buffers (&optional buffer-list)
"Get a list of buffers that are buried."
(cl-remove-if #'get-buffer-window (or buffer-list (doom-buffer-list))))
;;;###autoload
(defun doom-matching-buffers (pattern &optional buffer-list)
"Get a list of all buffers that match the regex PATTERN."
(cl-loop for buf in (or buffer-list (doom-buffer-list))
when (string-match-p pattern (buffer-name buf))
collect buf))
;;;###autoload
(defun doom-set-buffer-real (buffer flag)
"Forcibly mark BUFFER as FLAG (non-nil = real).
See `doom-real-buffer-p' for an explanation for real buffers."
(with-current-buffer buffer
(setq doom-real-buffer-p flag)))
;;;###autoload
(defun doom-kill-buffer-and-windows (buffer)
"Kill the buffer and delete all the windows it's displayed in."
(dolist (window (get-buffer-window-list buffer))
(unless (one-window-p t)
(delete-window window)))
(kill-buffer buffer))
;;;###autoload
(defun doom-fixup-windows (windows)
"Ensure that each of WINDOWS is showing a real buffer or the fallback buffer."
(dolist (window windows)
(with-selected-window window
(when (doom-unreal-buffer-p (window-buffer))
(previous-buffer)
(when (doom-unreal-buffer-p (window-buffer))
(switch-to-buffer (doom-fallback-buffer)))))))
;;;###autoload
(defun doom-kill-buffer-fixup-windows (buffer)
"Kill the BUFFER and ensure all the windows it was displayed in have switched
to a real buffer or the fallback buffer."
(let ((windows (get-buffer-window-list buffer)))
(kill-buffer buffer)
(doom-fixup-windows (cl-remove-if-not #'window-live-p windows))))
;;;###autoload
(defun doom-kill-buffers-fixup-windows (buffers)
"Kill the BUFFERS and ensure all the windows they were displayed in have
switched to a real buffer or the fallback buffer."
(let ((seen-windows (make-hash-table :test 'eq :size 8)))
(dolist (buffer buffers)
(let ((windows (get-buffer-window-list buffer)))
(kill-buffer buffer)
(dolist (window (cl-remove-if-not #'window-live-p windows))
(puthash window t seen-windows))))
(doom-fixup-windows (hash-table-keys seen-windows))))
;;;###autoload
(defun doom-kill-matching-buffers (pattern &optional buffer-list)
"Kill all buffers (in current workspace OR in BUFFER-LIST) that match the
regex PATTERN. Returns the number of killed buffers."
(let ((buffers (doom-matching-buffers pattern buffer-list)))
(dolist (buf buffers (length buffers))
(kill-buffer buf))))
;;
;; Hooks
;;;###autoload
(defun doom-mark-buffer-as-real-h ()
"Hook function that marks the current buffer as real.
See `doom-real-buffer-p' for an explanation for real buffers."
(doom-set-buffer-real (current-buffer) t))
;;
;; Interactive commands
;;;###autoload
(defun doom/save-and-kill-buffer ()
"Save the current buffer to file, then kill it."
(interactive)
(save-buffer)
(kill-current-buffer))
;;;###autoload
(defun doom/kill-this-buffer-in-all-windows (buffer &optional dont-save)
"Kill BUFFER globally and ensure all windows previously showing this buffer
have switched to a real buffer or the fallback buffer.
If DONT-SAVE, don't prompt to save modified buffers (discarding their changes)."
(interactive
(list (current-buffer) current-prefix-arg))
(cl-assert (bufferp buffer) t)
(when (and (buffer-modified-p buffer) dont-save)
(with-current-buffer buffer
(set-buffer-modified-p nil)))
(doom-kill-buffer-fixup-windows buffer))
(defun doom--message-or-count (interactive message count)
(if interactive
(message message count)
count))
;;;###autoload
(defun doom/kill-all-buffers (&optional buffer-list interactive)
"Kill all buffers and closes their windows.
If the prefix arg is passed, doesn't close windows and only kill buffers that
belong to the current project."
(interactive
(list (if current-prefix-arg
(doom-project-buffer-list)
(doom-buffer-list))
t))
(if (null buffer-list)
(message "No buffers to kill")
(save-some-buffers)
(delete-other-windows)
(when (memq (current-buffer) buffer-list)
(switch-to-buffer (doom-fallback-buffer)))
(mapc #'kill-buffer buffer-list)
(doom--message-or-count
interactive "Killed %d buffers"
(- (length buffer-list)
(length (cl-remove-if-not #'buffer-live-p buffer-list))))))
;;;###autoload
(defun doom/kill-other-buffers (&optional buffer-list interactive)
"Kill all other buffers (besides the current one).
If the prefix arg is passed, kill only buffers that belong to the current
project."
(interactive
(list (delq (current-buffer)
(if current-prefix-arg
(doom-project-buffer-list)
(doom-buffer-list)))
t))
(mapc #'doom-kill-buffer-and-windows buffer-list)
(doom--message-or-count
interactive "Killed %d other buffers"
(- (length buffer-list)
(length (cl-remove-if-not #'buffer-live-p buffer-list)))))
;;;###autoload
(defun doom/kill-matching-buffers (pattern &optional buffer-list interactive)
"Kill buffers that match PATTERN in BUFFER-LIST.
If the prefix arg is passed, only kill matching buffers in the current project."
(interactive
(list (read-regexp "Buffer pattern: ")
(if current-prefix-arg
(doom-project-buffer-list)
(doom-buffer-list))
t))
(doom-kill-matching-buffers pattern buffer-list)
(when interactive
(message "Killed %d buffer(s)"
(- (length buffer-list)
(length (cl-remove-if-not #'buffer-live-p buffer-list))))))
;;;###autoload
(defun doom/kill-buried-buffers (&optional buffer-list interactive)
"Kill buffers that are buried.
If PROJECT-P (universal argument), only kill buried buffers belonging to the
current project."
(interactive
(list (doom-buried-buffers
(if current-prefix-arg (doom-project-buffer-list)))
t))
(mapc #'kill-buffer buffer-list)
(doom--message-or-count
interactive "Killed %d buried buffers"
(- (length buffer-list)
(length (cl-remove-if-not #'buffer-live-p buffer-list)))))
;;;###autoload
(defun doom/kill-project-buffers (project &optional interactive)
"Kill buffers for the specified PROJECT."
(interactive
(list (if-let (open-projects (doom-open-projects))
(completing-read
"Kill buffers for project: " open-projects
nil t nil nil
(if-let* ((project-root (doom-project-root))
(project-root (abbreviate-file-name project-root))
((member project-root open-projects)))
project-root))
(message "No projects are open!")
nil)
t))
(when project
(let ((buffer-list (doom-project-buffer-list project)))
(doom-kill-buffers-fixup-windows buffer-list)
(doom--message-or-count
interactive "Killed %d project buffers"
(- (length buffer-list)
(length (cl-remove-if-not #'buffer-live-p buffer-list)))))))

126
lisp/lib/config.el Normal file
View file

@ -0,0 +1,126 @@
;;; lisp/lib/config.el -*- lexical-binding: t; -*-
(defvar doom-bin-dir (expand-file-name "bin/" doom-emacs-dir))
(defvar doom-bin (expand-file-name "doom" doom-bin-dir))
;;;###autoload
(defvar doom-reloading-p nil
"TODO")
;;;###autoload
(defun doom/open-private-config ()
"Browse your `doom-private-dir'."
(interactive)
(unless (file-directory-p doom-private-dir)
(make-directory doom-private-dir t))
(doom-project-browse doom-private-dir))
;;;###autoload
(defun doom/find-file-in-private-config ()
"Search for a file in `doom-private-dir'."
(interactive)
(doom-project-find-file doom-private-dir))
;;;###autoload
(defun doom/goto-private-init-file ()
"Open your private init.el file.
And jumps to your `doom!' block."
(interactive)
(find-file (expand-file-name "init.el" doom-private-dir))
(goto-char
(or (save-excursion
(goto-char (point-min))
(search-forward "(doom!" nil t))
(point))))
;;;###autoload
(defun doom/goto-private-config-file ()
"Open your private config.el file."
(interactive)
(find-file (expand-file-name "config.el" doom-private-dir)))
;;;###autoload
(defun doom/goto-private-packages-file ()
"Open your private packages.el file."
(interactive)
(find-file (expand-file-name "packages.el" doom-private-dir)))
;;
;;; Managements
(defmacro doom--if-compile (command on-success &optional on-failure)
(declare (indent 2))
`(let ((default-directory doom-emacs-dir))
(with-current-buffer (compile ,command t)
(let ((w (get-buffer-window (current-buffer))))
(select-window w)
(add-hook
'compilation-finish-functions
(lambda (_buf status)
(if (equal status "finished\n")
(progn
(delete-window w)
,on-success)
,on-failure))
nil 'local)))))
;;;###autoload
(defun doom/reload ()
"Reloads your private config.
This is experimental! It will try to do as `bin/doom sync' does, but from within
this Emacs session. i.e. it reload autoloads files (if necessary), reloads your
package list, and lastly, reloads your private config.el.
Runs `doom-after-reload-hook' afterwards."
(interactive)
(mapc #'require (cdr doom-incremental-packages))
(doom--if-compile (format "%S sync -e" doom-bin)
(let ((doom-reloading-p t))
(doom-run-hooks 'doom-before-reload-hook)
(load "doom-start")
(with-demoted-errors "PRIVATE CONFIG ERROR: %s"
(general-auto-unbind-keys)
(unwind-protect
(doom-initialize-modules 'force)
(general-auto-unbind-keys t)))
(doom-run-hooks 'doom-after-reload-hook)
(message "Config successfully reloaded!"))
(user-error "Failed to reload your config")))
;;;###autoload
(defun doom/reload-autoloads ()
"Reload only `doom-autoloads-file' and `doom-package-autoload-file'.
This is much faster and safer than `doom/reload', but not as comprehensive. This
reloads your package and module visibility, but does not install new packages or
remove orphaned ones. It also doesn't reload your private config.
It is useful to only pull in changes performed by 'doom sync' on the command
line."
(interactive)
(load (file-name-sans-extension doom-autoloads-file) nil 'nomessage))
;;;###autoload
(defun doom/reload-env ()
"Reloads your envvar file.
DOES NOT REGENERATE IT. You must run 'doom env' in your shell OUTSIDE of Emacs.
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)
(let ((default-directory doom-emacs-dir))
(with-temp-buffer
(doom-load-envvars-file doom-env-file)
(message "Reloaded %S" (abbreviate-file-name doom-env-file)))))
;;;###autoload
(defun doom/upgrade ()
"Run 'doom upgrade' then prompt to restart Emacs."
(interactive)
(doom--if-compile (format "%S upgrade --force" doom-bin)
(when (y-or-n-p "You must restart Emacs for the upgrade to take effect.\n\nRestart Emacs?")
(doom/restart-and-restore))))

408
lisp/lib/debug.el Normal file
View file

@ -0,0 +1,408 @@
;;; lisp/lib/debug.el -*- lexical-binding: t; -*-
;;
;;; Doom's debug mode
;;;###autoload
(defvar doom-debug-variables
'(async-debug
debug-on-error
(debugger . doom-debugger)
(doom-print-level . debug)
garbage-collection-messages
gcmh-verbose
init-file-debug
jka-compr-verbose
(message-log-max . 16384)
url-debug
use-package-verbose)
"A list of variable to toggle on `doom-debug-mode'.
Each entry can be a variable symbol or a cons cell whose CAR is the variable
symbol and CDR is the value to set it to when `doom-debug-mode' is activated.")
(defvar doom-debug--undefined-vars nil)
(defun doom-debug--watch-vars-h (&rest _)
(when-let (bound-vars (cl-delete-if-not #'boundp doom-debug--undefined-vars))
(doom-log "New variables available: %s" bound-vars)
(let ((message-log-max nil))
(doom-debug-mode -1)
(doom-debug-mode +1))))
;;;###autoload
(define-minor-mode doom-debug-mode
"Toggle `debug-on-error' and `init-file-debug' for verbose logging."
:init-value init-file-debug
:global t
(let ((enabled doom-debug-mode))
(setq doom-debug--undefined-vars nil)
(dolist (var doom-debug-variables)
(cond ((listp var)
(pcase-let ((`(,var . ,val) var))
(if (boundp var)
(set-default
var (if (not enabled)
(prog1 (get var 'initial-value)
(put 'x 'initial-value nil))
(put var 'initial-value (symbol-value var))
val))
(add-to-list 'doom-debug--undefined-vars var))))
((if (boundp var)
(set-default var enabled)
(add-to-list 'doom-debug--undefined-vars var)))))
(when (called-interactively-p 'any)
(when (fboundp 'explain-pause-mode)
(explain-pause-mode (if enabled +1 -1))))
;; Watch for changes in `doom-debug-variables', or when packages load (and
;; potentially define one of `doom-debug-variables'), in case some of them
;; aren't defined when `doom-debug-mode' is first loaded.
(cond (enabled
(message "Debug mode enabled! (Run 'M-x view-echo-area-messages' to open the log buffer)")
;; Produce more helpful (and visible) error messages from errors
;; emitted from hooks (particularly mode hooks), that usually go
;; unnoticed otherwise.
(advice-add #'run-hooks :override #'doom-run-hooks)
;; Add time stamps to lines in *Messages*
(advice-add #'message :before #'doom--timestamped-message-a)
(add-variable-watcher 'doom-debug-variables #'doom-debug--watch-vars-h)
(add-hook 'after-load-functions #'doom-debug--watch-vars-h))
(t
(advice-remove #'run-hooks #'doom-run-hooks)
(advice-remove #'message #'doom--timestamped-message-a)
(remove-variable-watcher 'doom-debug-variables #'doom-debug--watch-vars-h)
(remove-hook 'after-load-functions #'doom-debug--watch-vars-h)
(message "Debug mode disabled!")))))
;;
;;; Custom debuggers
(autoload 'backtrace-get-frames "backtrace")
(defun doom-backtrace ()
"Return a stack trace as a list of `backtrace-frame' objects."
;; (let* ((n 0)
;; (frame (backtrace-frame n))
;; (frame-list nil)
;; (in-program-stack nil))
;; (while frame
;; (when in-program-stack
;; (push (cdr frame) frame-list))
;; (when (eq (elt frame 1) debugger)
;; (setq in-program-stack t))
;; ;; (when (and (eq (elt frame 1) 'doom-cli-execute)
;; ;; (eq (elt frame 2) :doom))
;; ;; (setq in-program-stack nil))
;; (setq n (1+ n)
;; frame (backtrace-frame n)))
;; (nreverse frame-list))
(cdr (backtrace-get-frames debugger)))
(defun doom-backtrace-write-to-file (backtrace file)
"Write BACKTRACE to FILE with appropriate boilerplate."
(make-directory (file-name-directory file) t)
(let ((doom-print-indent 0))
(with-temp-file file
(insert ";; -*- lisp-interaction -*-\n")
(insert ";; vim: set ft=lisp:\n")
(insert (format ";; command=%S\n" command-line-args))
(insert (format ";; date=%S\n\n" (format-time-string "%Y-%m-%d %H-%M-%S" before-init-time)))
(insert ";;;; ENVIRONMENT\n" (with-output-to-string (doom/version)) "\n")
(let ((standard-output (current-buffer))
(print-quoted t)
(print-escape-newlines t)
(print-escape-control-characters t)
(print-symbols-bare t)
(print-level nil)
(print-circle nil)
(n -1))
(mapc (lambda (frame)
(princ (format ";;;; %d\n" (cl-incf n)))
(pp (list (cons (backtrace-frame-fun frame)
(backtrace-frame-args frame))
(backtrace-frame-locals frame)))
(terpri))
backtrace))
file)))
(defun doom-debugger (&rest args)
"Enter `debugger' in interactive sessions, `doom-cli-debugger' otherwise.
Writes backtraces to file and ensures the backtrace is recorded, so the user can
always access it."
(let ((backtrace (doom-backtrace)))
;; Work around Emacs's heuristic (in eval.c) for detecting errors in the
;; debugger, which would run this handler again on subsequent calls. Taken
;; from `ert--run-test-debugger'.
(cl-incf num-nonmacro-input-events)
;; TODO Write backtraces to file
;; TODO Write backtrace to a buffer in case recursive error interupts the
;; debugger (happens more often than it should).
(apply #'debug args)))
;;
;;; Time-stamped *Message* logs
(defun doom--timestamped-message-a (format-string &rest args)
"Advice to run before `message' that prepends a timestamp to each message.
Activate this advice with:
(advice-add 'message :before 'doom--timestamped-message-a)"
(when (and (stringp format-string)
message-log-max
(not (string-equal format-string "%s%s")))
(with-current-buffer "*Messages*"
(let ((timestamp (format-time-string "[%F %T] " (current-time)))
(deactivate-mark nil))
(with-silent-modifications
(goto-char (point-max))
(if (not (bolp))
(newline))
(insert timestamp))))
(let ((window (get-buffer-window "*Messages*")))
(when (and window (not (equal (selected-window) window)))
(with-current-buffer "*Messages*"
(goto-char (point-max))
(set-window-point window (point-max)))))))
;;
;;; Hooks
;;;###autoload
(defun doom-run-all-startup-hooks-h ()
"Run all startup Emacs hooks. Meant to be executed after starting Emacs with
-q or -Q, for example:
emacs -Q -l init.el -f doom-run-all-startup-hooks-h"
(setq after-init-time (current-time))
(let ((inhibit-startup-hooks nil))
(doom-run-hooks 'after-init-hook
'delayed-warnings-hook
'emacs-startup-hook
'tty-setup-hook
'window-setup-hook)))
;;
;;; Helpers
(defsubst doom--collect-forms-in (file form)
(when (file-readable-p file)
(let (forms)
(with-temp-buffer
(insert-file-contents file)
(with-syntax-table emacs-lisp-mode-syntax-table
(while (re-search-forward (format "(%s " (regexp-quote form)) nil t)
(let ((ppss (syntax-ppss)))
(unless (or (nth 4 ppss)
(nth 3 ppss))
(save-excursion
(goto-char (match-beginning 0))
(push (sexp-at-point) forms))))))
(nreverse forms)))))
;;;###autoload
(defun doom-info ()
"Returns diagnostic information about the current Emacs session in markdown,
ready to be pasted in a bug report on github."
(require 'vc-git)
(require 'doom-packages)
(let ((default-directory doom-emacs-dir))
(letf! ((defun sh (&rest args) (cdr (apply #'doom-call-process args)))
(defun cat (file &optional limit)
(with-temp-buffer
(insert-file-contents file nil 0 limit)
(buffer-string)))
(defun abbrev-path (path)
(replace-regexp-in-string
(regexp-opt (list (user-login-name)) 'words) "$USER"
(abbreviate-file-name path)))
(defun symlink-path (file)
(format "%s%s" (abbrev-path file)
(if (file-symlink-p file) ""
(concat " -> " (abbrev-path (file-truename file)))))))
`((generated . ,(format-time-string "%b %d, %Y %H:%M:%S"))
(system . ,(delq
nil (list (doom-system-distro-version)
(when (executable-find "uname")
(sh "uname" "-msr"))
(window-system))))
(emacs . ,(delq
nil (list emacs-version
(bound-and-true-p emacs-repository-branch)
(and (stringp emacs-repository-version)
(substring emacs-repository-version 0 9))
(symlink-path doom-emacs-dir))))
(doom . ,(list doom-version
(sh "git" "log" "-1" "--format=%D %h %ci")
(symlink-path doom-private-dir)))
(shell . ,(abbrev-path shell-file-name))
(features . ,system-configuration-features)
(traits
. ,(mapcar
#'symbol-name
(delq
nil (list (cond (noninteractive 'batch)
((display-graphic-p) 'gui)
('tty))
(if (daemonp) 'daemon)
(if (and (require 'server)
(server-running-p))
'server-running)
(if (boundp 'chemacs-version)
(intern (format "chemacs-%s" chemacs-version)))
(if (file-exists-p doom-env-file)
'envvar-file)
(if (featurep 'exec-path-from-shell)
'exec-path-from-shell)
(if (file-symlink-p doom-emacs-dir)
'symlinked-emacsdir)
(if (file-symlink-p doom-private-dir)
'symlinked-doomdir)
(if (and (stringp custom-file) (file-exists-p custom-file))
'custom-file)
(if (doom-files-in `(,@doom-modules-dirs
,doom-core-dir
,doom-private-dir)
:type 'files :match "\\.elc$")
'byte-compiled-config)))))
(custom
,@(when (and (stringp custom-file)
(file-exists-p custom-file))
(cl-loop for (type var _) in (get 'user 'theme-settings)
if (eq type 'theme-value)
collect var)))
(modules
,@(or (cl-loop with cat = nil
for key being the hash-keys of doom-modules
if (or (not cat)
(not (eq cat (car key))))
do (setq cat (car key))
and collect cat
collect
(let* ((flags (doom-module-get cat (cdr key) :flags))
(path (doom-module-get cat (cdr key) :path))
(module
(append
(cond ((null path)
(list '&nopath))
((not (file-in-directory-p path doom-modules-dir))
(list '&user)))
(if flags
`(,(cdr key) ,@flags)
(list (cdr key))))))
(if (= (length module) 1)
(car module)
module)))
'("n/a")))
(packages
,@(condition-case e
(mapcar
#'cdr (doom--collect-forms-in
(doom-path doom-private-dir "packages.el")
"package!"))
(error (format "<%S>" e))))
(unpin
,@(condition-case e
(mapcan #'identity
(mapcar
#'cdr (doom--collect-forms-in
(doom-path doom-private-dir "packages.el")
"unpin!")))
(error (list (format "<%S>" e)))))
(elpa
,@(condition-case e
(progn
(unless (bound-and-true-p package--initialized)
(package-initialize))
(cl-loop for (name . _) in package-alist
collect (format "%s" name)))
(error (format "<%S>" e))))))))
;;;###autoload
(defun doom-info-string (&optional width nocolor)
"Return the `doom-info' as a compact string.
FILL-COLUMN determines the column at which lines will be broken."
(with-temp-buffer
(let ((doom-print-backend (unless nocolor doom-print-backend))
(doom-print-indent 0))
(dolist (spec (cl-remove-if-not #'cdr (doom-info)) (buffer-string))
;; FIXME Refactor this horrible cludge, either here or in `format!'
(insert! ((bold "%-10s ") (symbol-name (car spec)))
("%s\n"
(string-trim-left
(indent
(fill
(if (listp (cdr spec))
(mapconcat (doom-partial #'format "%s")
(cdr spec)
" ")
(cdr spec))
(- (or width 80) 11))
11))))))))
;;
;;; Commands
;;;###autoload
(defun doom/version ()
"Display the running version of Doom core, module sources, and Emacs."
(interactive)
(print! "%s\n%s\n%s"
(format "%-13s v%-15s %s"
"GNU Emacs"
emacs-version
emacs-repository-version)
(format "%-13s v%-15s %s"
"Doom core"
doom-version
(or (cdr (doom-call-process
"git" "-C" doom-emacs-dir
"log" "-1" "--format=%D %h %ci"))
"n/a"))
;; NOTE This is a placeholder. Our modules will be moved to its own
;; repo eventually, and Doom core will later be capable of managing
;; them like package sources.
(format "%-13s v%-15s %s"
"Doom modules"
doom-modules-version
(or (cdr (doom-call-process
"git" "-C" doom-modules-dir
"log" "-1" "--format=%D %h %ci"))
"n/a"))))
;;;###autoload
(defun doom/info ()
"Collects some debug information about your Emacs session, formats it and
copies it to your clipboard, ready to be pasted into bug reports!"
(interactive)
(let ((buffer (get-buffer-create "*doom info*")))
(with-current-buffer buffer
(setq buffer-read-only t)
(with-silent-modifications
(erase-buffer)
(insert (doom-info-string 86)))
(pop-to-buffer buffer)
(kill-new (buffer-string))
(when (y-or-n-p "Your doom-info was copied to the clipboard.\n\nOpen pastebin.com?")
(browse-url "https://pastebin.com")))))
;;
;;; Profiling
(defvar doom--profiler nil)
;;;###autoload
(defun doom/toggle-profiler ()
"Toggle the Emacs profiler. Run it again to see the profiling report."
(interactive)
(if (not doom--profiler)
(profiler-start 'cpu+mem)
(profiler-report)
(profiler-stop))
(setq doom--profiler (not doom--profiler)))

350
lisp/lib/files.el Normal file
View file

@ -0,0 +1,350 @@
;;; lisp/lib/files.el -*- lexical-binding: t; -*-
(defun doom--resolve-path-forms (spec &optional directory)
"Converts a simple nested series of or/and forms into a series of
`file-exists-p' checks.
For example
(doom--resolve-path-forms
'(or A (and B C))
\"~\")
Returns (approximately):
'(let* ((_directory \"~\")
(A (expand-file-name A _directory))
(B (expand-file-name B _directory))
(C (expand-file-name C _directory)))
(or (and (file-exists-p A) A)
(and (if (file-exists-p B) B)
(if (file-exists-p C) C))))
This is used by `file-exists-p!' and `project-file-exists-p!'."
(declare (pure t) (side-effect-free t))
(if (and (listp spec)
(memq (car spec) '(or and)))
(cons (car spec)
(mapcar (doom-rpartial #'doom--resolve-path-forms directory)
(cdr spec)))
(let ((filevar (make-symbol "file")))
`(let ((,filevar ,spec))
(and (stringp ,filevar)
,(if directory
`(let ((default-directory ,directory))
(file-exists-p ,filevar))
`(file-exists-p ,filevar))
,filevar)))))
;;;###autoload
(defun doom-path (&rest segments)
"Constructs a file path from SEGMENTS.
Ignores `nil' elements in SEGMENTS."
(let ((segments (remq nil segments))
file-name-handler-alist
dir)
(while segments
(setq segment (pop segments)
dir (expand-file-name
(if (listp segment)
(apply #'doom-path dir segment)
segment)
dir)))
dir))
;;;###autoload
(defun doom-glob (&rest segments)
"Construct a path from SEGMENTS and expand glob patterns.
Returns nil if the path doesn't exist.
Ignores `nil' elements in SEGMENTS."
(let (case-fold-search)
(file-expand-wildcards (apply #'doom-path segments) t)))
;;;###autoload
(defun doom-dir (&rest segments)
"Constructs a path from SEGMENTS.
See `doom-path'.
Ignores `nil' elements in SEGMENTS."
(when-let (path (doom-path segments))
(directory-file-name path)))
;;;###autoload
(cl-defun doom-files-in
(paths &rest rest
&key
filter
map
(full t)
(follow-symlinks t)
(type 'files)
(relative-to (unless full default-directory))
(depth 99999)
(mindepth 0)
(match "/[^._][^/]+"))
"Return a list of files/directories in PATHS (one string or a list of them).
FILTER is a function or symbol that takes one argument (the path). If it returns
non-nil, the entry will be excluded.
MAP is a function or symbol which will be used to transform each entry in the
results.
TYPE determines what kind of path will be included in the results. This can be t
(files and folders), 'files or 'dirs.
By default, this function returns paths relative to PATH-OR-PATHS if it is a
single path. If it a list of paths, this function returns absolute paths.
Otherwise, by setting RELATIVE-TO to a path, the results will be transformed to
be relative to it.
The search recurses up to DEPTH and no further. DEPTH is an integer.
MATCH is a string regexp. Only entries that match it will be included."
(let (result)
(dolist (file (mapcan (doom-rpartial #'doom-glob "*") (doom-enlist paths)))
(cond ((file-directory-p file)
(appendq!
result
(and (memq type '(t dirs))
(string-match-p match file)
(not (and filter (funcall filter file)))
(not (and (file-symlink-p file)
(not follow-symlinks)))
(<= mindepth 0)
(list (if relative-to
(file-relative-name file relative-to)
file)))
(and (>= depth 1)
(apply #'doom-files-in file
(append (list :mindepth (1- mindepth)
:depth (1- depth)
:relative-to relative-to
:map nil)
rest)))))
((and (memq type '(t files))
(string-match-p match file)
(not (and filter (funcall filter file)))
(<= mindepth 0))
(push (if relative-to
(file-relative-name file relative-to)
file)
result))))
(if map
(mapcar map result)
result)))
;;;###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."
(unless (file-exists-p file)
(signal 'file-missing file))
(unless (file-readable-p file)
(error "%S is unreadable" file))
(with-temp-buffer
(insert-file-contents file nil 0 256)
(if (re-search-forward (format "^;;;###%s " (regexp-quote (or cookie "if")))
nil t)
(let ((load-file-name file))
(eval (sexp-at-point) t))
null-value)))
;;;###autoload
(defmacro file-exists-p! (files &optional directory)
"Returns non-nil if the FILES in DIRECTORY all exist.
DIRECTORY is a path; defaults to `default-directory'.
Returns the last file found to meet the rules set by FILES, which can be a
single file or nested compound statement of `and' and `or' statements."
`(let ((p ,(doom--resolve-path-forms files directory)))
(and p (expand-file-name p ,directory))))
;;;###autoload
(defun doom-file-size (file &optional dir)
"Returns the size of FILE (in DIR) in bytes."
(let ((file (expand-file-name file dir)))
(unless (file-exists-p file)
(error "Couldn't find file %S" file))
(unless (file-readable-p file)
(error "File %S is unreadable; can't acquire its filesize"
file))
(nth 7 (file-attributes file))))
(defvar w32-get-true-file-attributes)
;;;###autoload
(defun doom-directory-size (dir)
"Returns the size of FILE (in DIR) in kilobytes."
(unless (file-directory-p dir)
(error "Directory %S does not exist" dir))
(if (executable-find "du")
(/ (string-to-number (cdr (doom-call-process "du" "-sb" dir)))
1024.0)
;; REVIEW This is slow and terribly inaccurate, but it's something
(let ((w32-get-true-file-attributes t)
(file-name-handler-alist dir)
(max-lisp-eval-depth 5000)
(sum 0.0))
(dolist (attrs (directory-files-and-attributes dir nil nil t) sum)
(unless (member (car attrs) '("." ".."))
(cl-incf
sum (if (eq (nth 1 attrs) t) ; is directory
(doom-directory-size (expand-file-name (car attrs) dir))
(/ (nth 8 attrs) 1024.0))))))))
;;
;;; Helpers
(defun doom--update-files (&rest files)
"Ensure FILES are updated in `recentf', `magit' and `save-place'."
(let (toplevels)
(dolist (file files)
(when (featurep 'vc)
(vc-file-clearprops file)
(when-let (buffer (get-file-buffer file))
(with-current-buffer buffer
(vc-refresh-state))))
(when (featurep 'magit)
(when-let (default-directory (magit-toplevel (file-name-directory file)))
(cl-pushnew default-directory toplevels)))
(unless (file-readable-p file)
(when (bound-and-true-p recentf-mode)
(recentf-remove-if-non-kept file))
(when (and (bound-and-true-p projectile-mode)
(doom-project-p)
(projectile-file-cached-p file (doom-project-root)))
(projectile-purge-file-from-cache file))))
(dolist (default-directory toplevels)
(magit-refresh))
(when (bound-and-true-p save-place-mode)
(save-place-forget-unreadable-files))))
;;
;;; Commands
;;;###autoload
(defun doom/delete-this-file (&optional path force-p)
"Delete PATH, kill its buffers and expunge it from vc/magit cache.
If PATH is not specified, default to the current buffer's file.
If FORCE-P, delete without confirmation."
(interactive
(list (buffer-file-name (buffer-base-buffer))
current-prefix-arg))
(let* ((path (or path (buffer-file-name (buffer-base-buffer))))
(short-path (abbreviate-file-name path)))
(unless (and path (file-exists-p path))
(user-error "Buffer is not visiting any file"))
(unless (file-exists-p path)
(error "File doesn't exist: %s" path))
(unless (or force-p (y-or-n-p (format "Really delete %S?" short-path)))
(user-error "Aborted"))
(let ((buf (current-buffer)))
(unwind-protect
(progn (delete-file path t) t)
(if (file-exists-p path)
(error "Failed to delete %S" short-path)
;; Ensures that windows displaying this buffer will be switched to
;; real buffers (`doom-real-buffer-p')
(doom/kill-this-buffer-in-all-windows buf t)
(doom--update-files path)
(message "Deleted %S" short-path))))))
;;;###autoload
(defun doom/copy-this-file (new-path &optional force-p)
"Copy current buffer's file to NEW-PATH.
If FORCE-P, overwrite the destination file if it exists, without confirmation."
(interactive
(list (read-file-name "Copy file to: ")
current-prefix-arg))
(unless (and buffer-file-name (file-exists-p buffer-file-name))
(user-error "Buffer is not visiting any file"))
(let ((old-path (buffer-file-name (buffer-base-buffer)))
(new-path (expand-file-name new-path)))
(make-directory (file-name-directory new-path) 't)
(copy-file old-path new-path (or force-p 1))
(doom--update-files old-path new-path)
(message "File copied to %S" (abbreviate-file-name new-path))))
;;;###autoload
(defun doom/move-this-file (new-path &optional force-p)
"Move current buffer's file to NEW-PATH.
If FORCE-P, overwrite the destination file if it exists, without confirmation."
(interactive
(list (read-file-name "Move file to: ")
current-prefix-arg))
(unless (and buffer-file-name (file-exists-p buffer-file-name))
(user-error "Buffer is not visiting any file"))
(let ((old-path (buffer-file-name (buffer-base-buffer)))
(new-path (expand-file-name new-path)))
(when (directory-name-p new-path)
(setq new-path (concat new-path (file-name-nondirectory old-path))))
(make-directory (file-name-directory new-path) 't)
(rename-file old-path new-path (or force-p 1))
(set-visited-file-name new-path t t)
(doom--update-files old-path new-path)
(message "File moved to %S" (abbreviate-file-name new-path))))
(defun doom--sudo-file-path (file)
(let ((host (or (file-remote-p file 'host) "localhost")))
(concat "/" (when (file-remote-p file)
(concat (file-remote-p file 'method) ":"
(if-let (user (file-remote-p file 'user))
(concat user "@" host)
host)
"|"))
"sudo:root@" host
":" (or (file-remote-p file 'localname)
file))))
;;;###autoload
(defun doom/sudo-find-file (file)
"Open FILE as root."
(interactive "FOpen file as root: ")
(find-file (doom--sudo-file-path file)))
;;;###autoload
(defun doom/sudo-this-file ()
"Open the current file as root."
(interactive)
(find-file
(doom--sudo-file-path
(or buffer-file-name
(when (or (derived-mode-p 'dired-mode)
(derived-mode-p 'wdired-mode))
default-directory)))))
;;;###autoload
(defun doom/sudo-save-buffer ()
"Save this file as root."
(interactive)
(let ((file (doom--sudo-file-path buffer-file-name)))
(if-let (buffer (find-file-noselect file))
(let ((origin (current-buffer)))
(copy-to-buffer buffer (point-min) (point-max))
(unwind-protect
(with-current-buffer buffer
(save-buffer))
(unless (eq origin buffer)
(kill-buffer buffer))
(with-current-buffer origin
(revert-buffer t t))))
(user-error "Unable to open %S" file))))
;;;###autoload
(defun doom/remove-recent-file (file)
"Remove FILE from your recently-opened-files list."
(interactive
(list (completing-read "Remove recent file: " recentf-list
nil t)))
(setq recentf-list (delete file recentf-list))
(recentf-save-list)
(message "Removed %S from `recentf-list'" (abbreviate-file-name file)))

180
lisp/lib/fonts.el Normal file
View file

@ -0,0 +1,180 @@
;;; lisp/lib/fonts.el -*- lexical-binding: t; -*-
;;;###autoload
(defvar doom-font-increment 2
"How many steps to increase the font size each time `doom/increase-font-size'
or `doom/decrease-font-size' are invoked.")
;;;###autoload
(defvar doom-big-font nil
"The font to use for `doom-big-font-mode'.
If nil, `doom-font' will be used, scaled up by `doom-big-font-increment'. See
`doom-font' for details on acceptable values for this variable.")
;;;###autoload
(defvar doom-big-font-increment 4
"How many steps to increase the font size (with `doom-font' as the base) when
`doom-big-font-mode' is enabled and `doom-big-font' is nil.")
;;
;;; Library
;;;###autoload
(defun doom-normalize-font (font)
"Return FONT as a normalized font spec.
The font will be normalized (i.e. :weight, :slant, and :width will set to
'normal if not specified) before it is converted.
FONT can be a `font-spec', a font object, an XFT font string, or an XLFD font
string."
(cl-check-type font (or font string vector))
(when (and (stringp font)
(string-prefix-p "-" font))
(setq font (x-decompose-font-name font)))
(let* ((font
(cond ((stringp font)
(dolist (prop '("weight" "slant" "width") (aref (font-info font) 0))
(unless (string-match-p (format ":%s=" prop) font)
(setq font (concat font ":" prop "=normal")))))
((fontp font)
(dolist (prop '(:weight :slant :width) (font-xlfd-name font))
(unless (font-get font prop)
(font-put font prop 'normal))))
((vectorp font)
(dolist (i '(1 2 3) (x-compose-font-name font))
(unless (aref font i)
(aset font i "normal"))))))
(font (x-resolve-font-name font))
(font (font-spec :name font)))
(unless (font-get font :size)
(font-put font :size
(font-get (font-spec :name (face-font 'default))
:size)))
font))
;;;###autoload
(defun doom-adjust-font-size (increment &optional fixed-size-p font-alist)
"Increase size of font in FRAME by INCREMENT.
If FIXED-SIZE-P is non-nil, treat INCREMENT as a font size, rather than a
scaling factor.
FONT-ALIST is an alist give temporary values to certain Doom font variables,
like `doom-font' or `doom-variable-pitch-font'. e.g.
`((doom-font . ,(font-spec :family \"Sans Serif\" :size 12)))
Doesn't work in terminal Emacs."
(unless (display-multi-font-p)
(user-error "Cannot resize fonts in terminal Emacs"))
(condition-case-unless-debug e
(let (changed)
(dolist (sym '((doom-font . default)
(doom-serif-font . fixed-pitch-serif)
(doom-variable-pitch-font . variable-pitch))
(when changed
(doom-init-fonts-h 'reload)
t))
(cl-destructuring-bind (var . face) sym
(if (null increment)
(when (get var 'initial-value)
(set var (get var 'initial-value))
(put var 'initial-value nil)
(setq changed t))
(let* ((original-font (or (symbol-value var)
(face-font face t)
(with-temp-buffer (face-font face))))
(font (doom-normalize-font original-font))
(dfont
(or (if-let* ((remap-font (alist-get var font-alist))
(remap-xlfd (doom-normalize-font remap-font)))
remap-xlfd
(purecopy font))
(error "Could not decompose %s font" var))))
(let* ((step (if fixed-size-p 0 (* increment doom-font-increment)))
(orig-size (font-get font :size))
(new-size (if fixed-size-p increment (+ orig-size step))))
(cond ((<= new-size 0)
(error "`%s' font is too small to be resized (%d)" var new-size))
((= orig-size new-size)
(user-error "Could not resize `%s' for some reason" var))
((setq changed t)
(unless (get var 'initial-value)
(put var 'initial-value original-font))
(font-put dfont :size new-size)
(set var dfont)))))))))
(error
(ignore-errors (doom-adjust-font-size nil))
(signal (car e) (cdr e)))))
;;;###autoload
(defun doom-font-exists-p (font)
"Return non-nil if FONT exists on this system."
(declare (pure t) (side-effect-free t))
(ignore-errors (find-font (doom-normalize-font font))))
;;
;;; Commands
;;;###autoload
(defun doom/reload-font ()
"Reload your fonts, if they're set.
See `doom-init-fonts-h'."
(interactive)
(doom-init-fonts-h 'reload))
;;;###autoload
(defun doom/increase-font-size (count &optional increment)
"Enlargens the font size across the current and child frames."
(interactive "p")
(doom-adjust-font-size (* count (or increment doom-font-increment))))
;;;###autoload
(defun doom/decrease-font-size (count &optional increment)
"Shrinks the font size across the current and child frames."
(interactive "p")
(doom-adjust-font-size (* (- count) (or increment doom-font-increment))))
;;;###autoload
(defun doom/reset-font-size ()
"Reset font size and `text-scale'.
Assuming it has been adjusted via `doom/increase-font-size' and
`doom/decrease-font-size', or `text-scale-*' commands."
(interactive)
(let (success)
(when (and (boundp 'text-scale-mode-amount)
(/= text-scale-mode-amount 0))
(text-scale-set 0)
(setq success t))
(cond (doom-big-font-mode
(message "Disabling `doom-big-font-mode'")
(doom-big-font-mode -1)
(setq success t))
((doom-adjust-font-size nil)
(setq success t)))
(unless success
(user-error "The font hasn't been resized"))))
;;;###autoload
(define-minor-mode doom-big-font-mode
"Globally resizes your fonts for streams, screen-sharing or presentations.
Uses `doom-big-font' if its set, otherwise uses `doom-font' (falling back to
your system font).
Also resizees `doom-variable-pitch-font' and `doom-serif-font'."
:init-value nil
:lighter " BIG"
:global t
(if doom-big-font
;; Use `doom-big-font' in lieu of `doom-font'
(doom-adjust-font-size
(when doom-big-font-mode
(font-get (doom-normalize-font doom-big-font) :size))
t `((doom-font . ,doom-big-font)))
;; Resize the current font
(doom-adjust-font-size (if doom-big-font-mode doom-big-font-increment))))

752
lisp/lib/help.el Normal file
View file

@ -0,0 +1,752 @@
;;; lisp/lib/help.el -*- lexical-binding: t; -*-
(defvar doom--help-major-mode-module-alist
'((dockerfile-mode :tools docker)
(agda2-mode :lang agda)
(c-mode :lang cc)
(c++-mode :lang cc)
(objc++-mode :lang cc)
(crystal-mode :lang crystal)
(lisp-mode :lang common-lisp)
(csharp-mode :lang csharp)
(clojure-mode :lang clojure)
(clojurescript-mode :lang clojure)
(json-mode :lang json)
(yaml-mode :lang yaml)
(csv-mode :lang data)
(erlang-mode :lang erlang)
(elixir-mode :lang elixir)
(elm-mode :lang elm)
(emacs-lisp-mode :lang emacs-lisp)
(ess-r-mode :lang ess)
(ess-julia-mode :lang ess)
(go-mode :lang go)
(haskell-mode :lang haskell)
(hy-mode :lang hy)
(idris-mode :lang idris)
(java-mode :lang java)
(js2-mode :lang javascript)
(rjsx-mode :lang javascript)
(typescript-mode :lang javascript)
(typescript-tsx-mode :lang javascript)
(coffee-mode :lang javascript)
(julia-mode :lang julia)
(kotlin-mode :lang kotlin)
(latex-mode :lang latex)
(LaTeX-mode :lang latex)
(ledger-mode :lang ledger)
(lua-mode :lang lua)
(moonscript-mode :lang lua)
(markdown-mode :lang markdown)
(gfm-mode :lang markdown)
(nim-mode :lang nim)
(nix-mode :lang nix)
(tuareg-mode :lang ocaml)
(org-mode :lang org)
(raku-mode :lang raku)
(php-mode :lang php)
(hack-mode :lang php)
(plantuml-mode :lang plantuml)
(purescript-mode :lang purescript)
(python-mode :lang python)
(restclient-mode :lang rest)
(ruby-mode :lang ruby)
(rust-mode :lang rust)
(rustic-mode :lang rust)
(scala-mode :lang scala)
(scheme-mode :lang scheme)
(sh-mode :lang sh)
(swift-mode :lang swift)
(web-mode :lang web)
(css-mode :lang web)
(scss-mode :lang web)
(sass-mode :lang web)
(less-css-mode :lang web)
(stylus-mode :lang web)
(terra-mode :lang terra))
"An alist mapping major modes to Doom modules.
This is used by `doom/help-modules' to auto-select the module corresponding to
the current major-modea.")
;;
;;; Helpers
;;;###autoload
(defun doom-active-minor-modes ()
"Return a list of active minor-mode symbols."
(cl-loop for mode in minor-mode-list
if (and (boundp mode) (symbol-value mode))
collect mode))
;;
;;; Custom describe commands
;;;###autoload (defalias 'doom/describe-autodefs #'doom/help-autodefs)
;;;###autoload (defalias 'doom/describe-module #'doom/help-modules)
;;;###autoload (defalias 'doom/describe-package #'doom/help-packages)
;;;###autoload
(defun doom/describe-active-minor-mode (mode)
"Get information on an active minor mode. Use `describe-minor-mode' for a
selection of all minor-modes, active or not."
(interactive
(list (completing-read "Describe active mode: " (doom-active-minor-modes))))
(let ((symbol
(cond ((stringp mode) (intern mode))
((symbolp mode) mode)
((error "Expected a symbol/string, got a %s" (type-of mode))))))
(if (fboundp symbol)
(helpful-function symbol)
(helpful-variable symbol))))
;;
;;; Documentation commands
(defvar org-agenda-files)
(cl-defun doom--org-headings (files &key depth mindepth include-files &allow-other-keys)
"TODO"
(require 'org)
(let* ((default-directory doom-docs-dir)
(org-agenda-files (mapcar #'expand-file-name (doom-enlist files)))
(depth (if (integerp depth) depth))
(mindepth (if (integerp mindepth) mindepth))
(org-inhibit-startup t))
(message "Loading search results...")
(unwind-protect
(delq
nil
(org-map-entries
(lambda ()
(cl-destructuring-bind (level _reduced-level _todo _priority text tags)
(org-heading-components)
(when (and (or (null depth)
(<= level depth))
(or (null mindepth)
(>= level mindepth))
(or (null tags)
(not (string-match-p ":TOC" tags))))
(let ((path (org-get-outline-path))
(title (org-collect-keywords '("TITLE") '("TITLE"))))
(list (string-join
(list (string-join
(append (when include-files
(list (or (cdr (assoc "TITLE" title))
(file-relative-name (buffer-file-name)))))
path
(when text
(list (replace-regexp-in-string org-link-any-re "\\4" text))))
" > ")
tags)
" ")
(buffer-file-name)
(point))))))
t 'agenda))
(mapc #'kill-buffer org-agenda-new-buffers)
(setq org-agenda-new-buffers nil))))
(defvar ivy-sort-functions-alist)
;;;###autoload
(cl-defun doom-completing-read-org-headings
(prompt files &rest plist &key depth mindepth include-files initial-input extra-candidates action)
"TODO"
(let ((alist
(append (apply #'doom--org-headings files plist)
extra-candidates))
ivy-sort-functions-alist)
(if-let (result (completing-read prompt alist nil nil initial-input))
(cl-destructuring-bind (file &optional location)
(cdr (assoc result alist))
(if action
(funcall action file location)
(find-file file)
(cond ((functionp location)
(funcall location))
(location
(goto-char location)))
(ignore-errors
(when (outline-invisible-p)
(save-excursion
(outline-previous-visible-heading 1)
(org-show-subtree))))))
(user-error "Aborted"))))
;;;###autoload
(defun doom/homepage ()
"Open the doom emacs homepage in the browser."
(interactive)
(browse-url "https://doomemacs.org"))
;;;###autoload
(defun doom/issue-tracker ()
"Open Doom Emacs' issue tracker on Discourse."
(interactive)
(browse-url "https://github.com/hlissner/doom-emacs/issues"))
;;;###autoload
(defun doom/report-bug ()
"Open the browser on our Discourse.
If called when a backtrace buffer is present, it and the output of `doom-info'
will be automatically appended to the result."
(interactive)
;; TODO Upload doom/info to pastebin and append to querystring
(browse-url "https://github.com/hlissner/doom-emacs/issues/new?labels=1.+bug%2C2.+status%3Aunread&template=bug_report.yml"))
;;;###autoload
(defun doom/discourse ()
"Open Doom Emacs' issue tracker on Discourse."
(interactive)
(browse-url "https://discourse.doomemacs.org"))
;;;###autoload
(defun doom/help ()
"Open Doom's user manual."
(interactive)
(find-file (expand-file-name "index.org" doom-docs-dir)))
;;;###autoload
(defun doom/help-search-headings (&optional initial-input)
"Search Doom's documentation and jump to a headline."
(interactive)
(doom-completing-read-org-headings
"Find in Doom help: "
(list "getting_started.org"
"contributing.org"
"troubleshooting.org"
"tutorials.org"
"faq.org")
:depth 3
:include-files t
:initial-input initial-input
:extra-candidates
(mapcar (lambda (x)
(setcar x (concat "Doom Modules > " (car x)))
x)
(doom--help-modules-list))))
;;;###autoload
(defun doom/help-search (&optional initial-input)
"Perform a text search on all of Doom's documentation."
(interactive)
(funcall (cond ((fboundp '+ivy-file-search)
#'+ivy-file-search)
((fboundp '+helm-file-search)
#'+helm-file-search)
((fboundp '+vertico-file-search)
#'+vertico-file-search)
((rgrep
(read-regexp
"Search for" (or initial-input 'grep-tag-default)
'grep-regexp-history)
"*.org" doom-emacs-dir)
#'ignore))
:query initial-input
:args '("-t" "org")
:in doom-emacs-dir
:prompt "Search documentation for: "))
;;;###autoload
(defun doom/help-search-news (&optional initial-input)
"Search headlines in Doom's newsletters."
(interactive)
(doom-completing-read-org-headings
"Find in News: "
(nreverse (doom-files-in (expand-file-name "news" doom-docs-dir)
:match "/[0-9]"
:relative-to doom-docs-dir))
:include-files t
:initial-input initial-input))
;;;###autoload
(defun doom/help-faq (&optional initial-input)
"Search Doom's FAQ and jump to a question."
(interactive)
(doom-completing-read-org-headings
"Find in FAQ: " (list "faq.org")
:depth 2
:initial-input initial-input))
;;;###autoload
(defun doom/help-news ()
"Open a Doom newsletter.
The latest newsletter will be selected by default."
(interactive)
(let* ((default-directory (expand-file-name "news/" doom-docs-dir))
(news-files (doom-files-in default-directory)))
(find-file
(read-file-name (format "Open Doom newsletter (current: v%s): "
doom-version)
default-directory
(if (member doom-version news-files)
doom-version
(concat (mapconcat #'number-to-string
(nbutlast (version-to-list doom-version) 1)
".")
".x"))
t doom-version))))
;;;###autoload
(defun doom/help-autodefs (autodef)
"Open documentation for an autodef.
An autodef is a Doom concept. It is a function or macro that is always defined,
whether or not its containing module is disabled (in which case it will safely
no-op without evaluating its arguments). This syntactic sugar lets you use them
without needing to check if they are available."
(interactive
(let* ((settings
(cl-loop with case-fold-search = nil
for sym being the symbols of obarray
for sym-name = (symbol-name sym)
if (and (or (functionp sym)
(macrop sym))
(string-match-p "[a-z]!$" sym-name))
collect sym))
(sym (symbol-at-point))
(autodef
(completing-read
"Describe setter: "
;; TODO Could be cleaner (refactor me!)
(cl-loop with maxwidth = (apply #'max (mapcar #'length (mapcar #'symbol-name settings)))
for def in (sort settings #'string-lessp)
if (get def 'doom-module)
collect
(format (format "%%-%ds%%s" (+ maxwidth 4))
def (propertize (format "%s %s" (car it) (cdr it))
'face 'font-lock-comment-face))
else if (and (string-match-p "^set-.+!$" (symbol-name def))
(symbol-file def)
(file-in-directory-p (symbol-file def) doom-core-dir))
collect
(format (format "%%-%ds%%s" (+ maxwidth 4))
def (propertize (format "lisp/%s.el" (file-name-sans-extension (file-relative-name (symbol-file def) doom-core-dir)))
'face 'font-lock-comment-face)))
nil t
(when (and (symbolp sym)
(string-match-p "!$" (symbol-name sym)))
(symbol-name sym)))))
(list (and autodef (car (split-string autodef " "))))))
(or (stringp autodef)
(functionp autodef)
(signal 'wrong-type-argument (list '(stringp functionp) autodef)))
(let ((fn (if (functionp autodef)
autodef
(intern-soft autodef))))
(or (fboundp fn)
(error "'%s' is not a valid DOOM autodef" autodef))
(if (fboundp 'helpful-callable)
(helpful-callable fn)
(describe-function fn))))
(defun doom--help-modules-list ()
(cl-loop for path in (cdr (doom-module-load-path 'all))
for (cat . mod) = (doom-module-from-path path)
for readme-path = (or (doom-module-locate-path cat mod "README.org")
(doom-module-locate-path cat mod))
for format = (format "%s %s" cat mod)
if (doom-module-p cat mod)
collect (list format readme-path)
else if (and cat mod)
collect (list (propertize format 'face 'font-lock-comment-face)
readme-path)))
(defun doom--help-current-module-str ()
(cond ((save-excursion
(require 'smartparens)
(ignore-errors
(sp-beginning-of-sexp)
(unless (eq (char-after) ?\()
(backward-char))
(let ((sexp (sexp-at-point)))
(when (memq (car-safe sexp) '(featurep! require!))
(format "%s %s" (nth 1 sexp) (nth 2 sexp)))))))
((when buffer-file-name
(when-let (mod (doom-module-from-path buffer-file-name))
(unless (memq (car mod) '(:core :private))
(format "%s %s" (car mod) (cdr mod))))))
((when-let (mod (cdr (assq major-mode doom--help-major-mode-module-alist)))
(format "%s %s"
(symbol-name (car mod))
(symbol-name (cadr mod)))))))
;;;###autoload
(defun doom/help-modules (category module &optional visit-dir)
"Open the documentation for a Doom module.
CATEGORY is a keyword and MODULE is a symbol. e.g. :editor and 'evil.
If VISIT-DIR is non-nil, visit the module's directory rather than its
documentation.
Automatically selects a) the module at point (in private init files), b) the
module derived from a `featurep!' or `require!' call, c) the module that the
current file is in, or d) the module associated with the current major mode (see
`doom--help-major-mode-module-alist')."
(interactive
(nconc
(mapcar #'intern
(split-string
(completing-read "Describe module: "
(doom--help-modules-list)
nil t nil nil
(doom--help-current-module-str))
" " t))
(list current-prefix-arg)))
(cl-check-type category symbol)
(cl-check-type module symbol)
(cl-destructuring-bind (module-string path)
(or (assoc (format "%s %s" category module) (doom--help-modules-list))
(user-error "'%s %s' is not a valid module" category module))
(setq module-string (substring-no-properties module-string))
(unless (file-readable-p path)
(error "Can't find or read %S module at %S" module-string path))
(cond ((not (file-directory-p path))
(if visit-dir
(doom-project-browse (file-name-directory path))
(find-file path)))
(visit-dir
(doom-project-browse path))
((y-or-n-p (format "The %S module has no README file. Explore its directory?"
module-string))
(doom-project-browse (file-name-directory path)))
((user-error "Aborted module lookup")))))
;;;###autoload
(defun doom/help-custom-variable (var)
"Look up documentation for a custom variable.
Unlike `helpful-variable', which casts a wider net that includes internal
variables, this only lists variables that exist to be customized (defined with
`defcustom')."
(interactive
(list (helpful--read-symbol
"Custom variable: "
(helpful--variable-at-point)
(lambda (sym)
(and (helpful--variable-p sym)
(custom-variable-p sym)
;; Exclude minor mode state variables, which aren't meant to be
;; modified directly, but through their associated function.
(not (or (and (string-suffix-p "-mode" (symbol-name sym))
(fboundp sym))
(eq (get sym 'custom-set) 'custom-set-minor-mode))))))))
(helpful-variable var))
;;
;;; `doom/help-packages'
(defun doom--help-insert-button (label &optional uri line)
"Helper function to insert a button at point.
The button will have the text LABEL. If URI is given, the button will open it,
otherwise the LABEL will be used. If the uri to open is a url it will be opened
in a browser. If LINE is given (and the uri to open is not a url), then the file
will open with point on that line."
(let ((uri (or uri label)))
(insert-text-button
label
'face 'link
'follow-link t
'action
(if (string-match-p "^https?://" uri)
(lambda (_) (browse-url uri))
(unless (file-exists-p uri)
(error "Path does not exist: %S" uri))
(lambda (_)
(when (window-dedicated-p)
(other-window 1))
(find-file uri)
(when line
(goto-char (point-min))
(forward-line (1- line))
(recenter)))))))
(defun doom--help-package-configs (package)
(let ((default-directory doom-emacs-dir))
;; TODO Use ripgrep instead
(split-string
(cdr (doom-call-process
"git" "grep" "--no-break" "--no-heading" "--line-number"
(format "%s %s\\($\\| \\)"
"\\(^;;;###package\\|(after!\\|(use-package!\\)"
package)
":(exclude)*.org"))
"\n" t)))
(defvar doom--help-packages-list nil)
;;;###autoload
(defun doom/help-packages (package)
"Like `describe-package', but for packages installed by Doom modules.
Only shows installed packages. Includes information about where packages are
defined and configured.
If prefix arg is present, refresh the cache."
(interactive
(let ((guess (or (function-called-at-point)
(symbol-at-point))))
(require 'finder-inf nil t)
(require 'package)
(require 'straight)
(let ((packages
(if (and doom--help-packages-list (null current-prefix-arg))
doom--help-packages-list
(message "Generating packages list for the first time...")
(redisplay)
(setq doom--help-packages-list
(delete-dups
(append (mapcar #'car package-alist)
(mapcar #'car package--builtins)
(mapcar #'intern
(hash-table-keys straight--build-cache))
(mapcar #'car (doom-package-list 'all))
nil))))))
(unless (memq guess packages)
(setq guess nil))
(list
(intern
(completing-read (format "Describe Doom package (%s): "
(concat (when guess
(format "default '%s', " guess))
(format "total %d" (length packages))))
packages nil t nil nil
(when guess (symbol-name guess))))))))
;; TODO Refactor me.
(require 'doom-packages)
(doom-initialize-packages)
(help-setup-xref (list #'doom/help-packages package)
(called-interactively-p 'interactive))
(with-help-window (help-buffer)
(with-current-buffer standard-output
(when (or (package-desc-p package)
(and (symbolp package)
(or (assq package package-alist)
(assq package package--builtins))))
(describe-package-1 package))
(let ((indent (make-string 13 ? )))
(goto-char (point-min))
(if (re-search-forward " Status: .*$" nil t)
(insert "\n")
(search-forward "\n\n" nil t))
(package--print-help-section "Package")
(insert (symbol-name package) "\n")
(package--print-help-section "Source")
(pcase (doom-package-backend package)
(`straight
(insert "Straight\n")
(package--print-help-section "Pinned")
(insert (if-let (pin (plist-get (cdr (assq package doom-packages)) :pin))
pin
"unpinned")
"\n")
(package--print-help-section "Build")
(let ((default-directory (straight--repos-dir (symbol-name package))))
(if (file-exists-p default-directory)
(insert (cdr (doom-call-process "git" "log" "-1" "--format=%D %h %ci")))
(insert "n/a")))
(insert "\n" indent)
(package--print-help-section "Build location")
(let ((build-dir (straight--build-dir (symbol-name package))))
(if (file-exists-p build-dir)
(doom--help-insert-button (abbreviate-file-name build-dir))
(insert "n/a")))
(insert "\n" indent)
(package--print-help-section "Repo location")
(let* ((local-repo (doom-package-recipe-repo package))
(repo-dir (straight--repos-dir local-repo)))
(if (file-exists-p repo-dir)
(doom--help-insert-button (abbreviate-file-name repo-dir))
(insert "n/a"))
(insert "\n"))
(let ((recipe (doom-package-build-recipe package)))
(package--print-help-section "Recipe")
(insert
(replace-regexp-in-string "\n" (concat "\n" indent)
(pp-to-string recipe))))
(package--print-help-section "Homepage")
(doom--help-insert-button (doom-package-homepage package)))
(`elpa (insert "[M]ELPA ")
(doom--help-insert-button (doom-package-homepage package))
(package--print-help-section "Location")
(doom--help-insert-button
(abbreviate-file-name
(file-name-directory
(locate-library (symbol-name package))))))
(`builtin (insert "Built-in\n")
(package--print-help-section "Location")
(doom--help-insert-button
(abbreviate-file-name
(file-name-directory
(locate-library (symbol-name package))))))
(`other (doom--help-insert-button
(abbreviate-file-name
(or (symbol-file package)
(locate-library (symbol-name package))))))
(_ (insert "Not installed")))
(insert "\n")
(when-let
(modules
(if (gethash (symbol-name package) straight--build-cache)
(doom-package-get package :modules)
(plist-get (cdr (assq package (doom-package-list 'all)))
:modules)))
(package--print-help-section "Modules")
(insert "Declared by the following Doom modules:\n")
(dolist (m modules)
(let* ((module-path (pcase (car m)
(:core doom-core-dir)
(:private doom-private-dir)
(category
(doom-module-locate-path category
(cdr m)))))
(readme-path (expand-file-name "README.org" module-path)))
(insert indent)
(doom--help-insert-button
(format "%s %s" (car m) (or (cdr m) ""))
module-path)
(insert " (")
(if (file-exists-p readme-path)
(doom--help-insert-button "readme" readme-path)
(insert "no readme"))
(insert ")\n"))))
(package--print-help-section "Configs")
(if-let ((configs (doom--help-package-configs package)))
(progn
(insert "This package is configured in the following locations:")
(dolist (location configs)
(insert "\n" indent)
(cl-destructuring-bind (file line _match &rest)
(split-string location ":")
(doom--help-insert-button location
(expand-file-name file doom-emacs-dir)
(string-to-number line)))))
(insert "This package is not configured anywhere"))
(goto-char (point-min))))))
(defvar doom--package-cache nil)
(defun doom--package-list (&optional prompt)
(let* ((guess (or (function-called-at-point)
(symbol-at-point))))
(require 'finder-inf nil t)
(unless package--initialized
(package-initialize t))
(let ((packages (or doom--package-cache
(progn
(message "Reading packages...")
(cl-delete-duplicates
(append (mapcar 'car package-alist)
(mapcar 'car package--builtins)
(mapcar 'car package-archive-contents)))))))
(setq doom--package-cache packages)
(unless (memq guess packages)
(setq guess nil))
(intern (completing-read (or prompt
(if guess
(format "Select package to search for (default %s): "
guess)
"Describe package: "))
packages nil t nil nil
(if guess (symbol-name guess)))))))
;;;###autoload
(defun doom/help-package-config (package)
"Jump to any `use-package!', `after!' or ;;;###package block for PACKAGE.
This only searches `doom-emacs-dir' (typically ~/.emacs.d) and does not include
config blocks in your private config."
(interactive (list (doom--package-list "Find package config: ")))
(cl-destructuring-bind (file line _match)
(split-string
(completing-read
"Jump to config: "
(or (doom--help-package-configs package)
(user-error "This package isn't configured by you or Doom")))
":")
(find-file (expand-file-name file doom-emacs-dir))
(goto-char (point-min))
(forward-line (1- line))
(recenter)))
;;;###autoload
(defalias 'doom/help-package-homepage #'straight-visit-package-website)
(defun doom--help-search-prompt (prompt)
(let ((query (doom-thing-at-point-or-region)))
(if (featurep 'counsel)
query
(read-string prompt query 'git-grep query))))
(defvar counsel-rg-base-command)
(defun doom--help-search (dirs query prompt)
;; REVIEW Replace with deadgrep
(unless (executable-find "rg")
(user-error "Can't find ripgrep on your system"))
(cond ((fboundp 'consult--grep)
(consult--grep
prompt
(lambda (input)
(pcase-let* ((cmd (split-string-and-unquote consult-ripgrep-args))
(type (consult--ripgrep-regexp-type (car cmd)))
(`(,arg . ,opts) (consult--command-split input))
(`(,re . ,hl) (funcall consult--regexp-compiler arg type)))
(when re
(list :command
(append cmd
(and (eq type 'pcre) '("-P"))
(list "-e" (consult--join-regexps re type))
opts
dirs)
:highlight hl))))
data-directory query))
((fboundp 'counsel-rg)
(let ((counsel-rg-base-command
(if (stringp counsel-rg-base-command)
(format counsel-rg-base-command
(concat "%s " (mapconcat #'shell-quote-argument dirs " ")))
(append counsel-rg-base-command dirs))))
(counsel-rg query nil "-Lz" (concat prompt ": "))))
;; () TODO Helm support?
((grep-find
(string-join
(append (list "rg" "-L" "--search-zip" "--no-heading" "--color=never"
(shell-quote-argument query))
(mapcar #'shell-quote-argument dirs))
" ")))))
;;;###autoload
(defun doom/help-search-load-path (query)
"Perform a text search on your `load-path'.
Uses the symbol at point or the current selection, if available."
(interactive
(list (doom--help-search-prompt "Search load-path: ")))
(doom--help-search (cl-remove-if-not #'file-directory-p load-path)
query "Search load-path: "))
;;;###autoload
(defun doom/help-search-loaded-files (query)
"Perform a text search on your `load-path'.
Uses the symbol at point or the current selection, if available."
(interactive
(list (doom--help-search-prompt "Search loaded files: ")))
(doom--help-search
(cl-loop for (file . _) in (cl-remove-if-not #'stringp load-history :key #'car)
for filebase = (file-name-sans-extension file)
if (file-exists-p! (or (format "%s.el.gz" filebase)
(format "%s.el" filebase)))
collect it)
query "Search loaded files: "))

307
lisp/lib/packages.el Normal file
View file

@ -0,0 +1,307 @@
;;; lisp/lib/packages.el -*- lexical-binding: t; -*-
;;;###autoload
(defun doom/reload-packages ()
"Reload `doom-packages', `package' and `quelpa'."
(interactive)
;; HACK straight.el must be loaded for this to work
(message "Reloading packages")
(doom-initialize-packages t)
(message "Reloading packages...DONE"))
;;
;;; Bump commands
(defun doom--package-merge-recipes (package plist)
(require 'straight)
(doom-plist-merge
(plist-get plist :recipe)
(if-let (recipe (straight-recipes-retrieve package))
(cdr (if (memq (car recipe) '(quote \`))
(eval recipe t)
recipe))
(let ((recipe (plist-get (cdr (assq package doom-packages))
:recipe)))
(if (keywordp (car recipe))
recipe
(cdr recipe))))))
(defun doom--package-to-bump-string (package plist)
"Return a PACKAGE and its PLIST in 'username/repo@commit' format."
(format "%s@%s"
(plist-get (doom--package-merge-recipes package plist) :repo)
(substring-no-properties (plist-get plist :pin) 0 12)))
(defun doom--package-at-point (&optional point)
"Return the package and plist from the (package! PACKAGE PLIST...) at point."
(save-match-data
(save-excursion
(and point (goto-char point))
(while (and (or (atom (sexp-at-point))
(doom-point-in-string-or-comment-p))
(search-backward "(" nil t)))
(when (eq (car-safe (sexp-at-point)) 'package!)
(cl-destructuring-bind (beg . end)
(bounds-of-thing-at-point 'sexp)
(let* ((doom-packages nil)
(buffer-file-name
(or buffer-file-name
(bound-and-true-p org-src-source-file-name)))
(package (eval (sexp-at-point) t)))
(list :beg beg
:end end
:package (car package)
:plist (cdr package))))))))
;;;###autoload
(defun doom/bumpify-package-at-point ()
"Convert `package!' call at point to a bump string."
(interactive)
(cl-destructuring-bind (&key package plist beg end)
(doom--package-at-point)
(when-let (str (doom--package-to-bump-string package plist))
(goto-char beg)
(delete-region beg end)
(insert str))))
;;;###autoload
(defun doom/bumpify-packages-in-buffer ()
"Convert all `package!' calls in buffer into bump strings."
(interactive)
(save-excursion
(goto-char (point-min))
(while (search-forward "(package!" nil t)
(unless (doom-point-in-string-or-comment-p)
(doom/bumpify-package-at-point)))))
;;;###autoload
(defun doom/bump-package-at-point (&optional select)
"Inserts or updates a `:pin' for the `package!' statement at point.
Grabs the latest commit id of the package using 'git'."
(interactive "P")
(doom-initialize-packages)
(cl-destructuring-bind (&key package plist beg end)
(or (doom--package-at-point)
(user-error "Not on a `package!' call"))
(let* ((recipe (doom--package-merge-recipes package plist))
(branch (plist-get recipe :branch))
(oldid (or (plist-get plist :pin)
(doom-package-get package :pin)))
(url (straight-vc-git--destructure recipe (upstream-repo upstream-host)
(straight-vc-git--encode-url upstream-repo upstream-host)))
(id (or (when url
(cdr (doom-call-process
"git" "ls-remote" url
(unless select branch))))
(user-error "Couldn't find a recipe for %s" package)))
(id (car (split-string
(if select
(completing-read "Commit: " (split-string id "\n" t))
id)))))
(when (and oldid
(plist-member plist :pin)
(equal oldid id))
(user-error "%s: no update necessary" package))
(save-excursion
(if (re-search-forward ":pin +\"\\([^\"]+\\)\"" end t)
(replace-match id t t nil 1)
(goto-char (1- end))
(insert " :pin " (prin1-to-string id))))
(cond ((not oldid)
(message "%s: → %s" package (substring id 0 10)))
((< (length oldid) (length id))
(message "%s: extended to %s..." package id))
((message "%s: %s → %s"
package
(substring oldid 0 10)
(substring id 0 10)))))))
;;;###autoload
(defun doom/bump-packages-in-buffer (&optional select)
"Inserts or updates a `:pin' to all `package!' statements in current buffer.
If SELECT (prefix arg) is non-nil, prompt you to choose a specific commit for
each package."
(interactive "P")
(save-excursion
(goto-char (point-min))
(doom-initialize-packages)
(let (packages)
(while (search-forward "(package! " nil t)
(unless (let ((ppss (syntax-ppss)))
(or (nth 4 ppss)
(nth 3 ppss)
(save-excursion
(and (goto-char (match-beginning 0))
(not (plist-member (sexp-at-point) :pin))))))
(condition-case e
(push (doom/bump-package-at-point select) packages)
(user-error (message "%s" (error-message-string e))))))
(if packages
(message "Updated %d packages\n- %s" (length packages) (string-join packages "\n- "))
(message "No packages to update")))))
;;;###autoload
(defun doom/bump-module (category &optional module select)
"Bump packages in CATEGORY MODULE.
If SELECT (prefix arg) is non-nil, prompt you to choose a specific commit for
each package."
(interactive
(let* ((module (completing-read
"Bump module: "
(let ((modules (doom-module-list 'all)))
(mapcar (lambda (m)
(if (listp m)
(format "%s %s" (car m) (cdr m))
(format "%s" m)))
(append '(:private :core)
(delete-dups (mapcar #'car modules))
modules)))
nil t nil nil))
(module (split-string module " " t)))
(list (intern (car module))
(ignore-errors (intern (cadr module)))
current-prefix-arg)))
(mapc (lambda! ((cat . mod))
(if-let (packages-file
(pcase cat
(:private (car (doom-glob doom-private-dir "packages.el")))
(:core (car (doom-glob doom-core-dir "packages.el")))
(_ (doom-module-locate-path cat mod "packages.el"))))
(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))))
(if module
(list (cons category module))
(cl-remove-if-not (lambda (m) (eq (car m) category))
(append '((:core) (:private))
(doom-module-list 'all))))))
;;;###autoload
(defun doom/bump-package (package)
"Bump PACKAGE in all modules that install it."
(interactive
(list (intern (completing-read "Bump package: "
(mapcar #'car (doom-package-list 'all))))))
(let* ((packages (doom-package-list 'all))
(modules (plist-get (alist-get package packages) :modules)))
(unless modules
(user-error "This package isn't installed by any Doom module"))
(dolist (module modules)
(when-let (packages-file (doom-module-locate-path (car module) (cdr module)))
(doom/bump-module (car module) (cdr module))))))
;;
;;; Bump commits
;;;###autoload
(defun doom/bumpify-diff (&optional interactive)
"Copy user/repo@hash -> user/repo@hash's of changed packages to clipboard.
Must be run from a magit diff buffer."
(interactive (list 'interactive))
(save-window-excursion
(magit-diff-staged)
(unless (eq major-mode 'magit-diff-mode)
(user-error "Not in a magit diff buffer"))
(goto-char (point-min))
(let (targets lines)
(save-excursion
(while (re-search-forward "^modified +\\(.+\\)$" nil t)
(cl-pushnew (doom-module-from-path (match-string 1)) targets
:test #'equal)))
(while (re-search-forward "^-" nil t)
(let ((file (magit-file-at-point))
before after)
(and (save-window-excursion
(call-interactively #'magit-diff-visit-file)
(when (or (looking-at-p "(package!")
(re-search-forward "(package! " (line-end-position) t)
(re-search-backward "(package! " nil t))
(let ((buffer-file-name file))
(cl-destructuring-bind (&key package plist _beg _end)
(doom--package-at-point)
(setq before (doom--package-to-bump-string package plist))))))
(re-search-forward "^+" nil t)
(save-window-excursion
(call-interactively #'magit-diff-visit-file)
(or (looking-at-p "(package!")
(re-search-forward "(package! " (line-end-position) t)
(re-search-backward "(package! "))
(let ((buffer-file-name file))
(cl-destructuring-bind (&key package plist _beg _end)
(doom--package-at-point)
(setq after (doom--package-to-bump-string package plist)))))
(cl-pushnew (format "%s -> %s" before after) lines))))
(if (null lines)
(user-error "No bumps to bumpify")
(prog1 (funcall (if interactive #'kill-new #'identity)
(format "bump: %s\n\n%s"
(mapconcat (lambda (x)
(mapconcat #'symbol-name x " "))
(cl-loop with alist = ()
for (category . module) in (reverse targets)
do (setf (alist-get category alist)
(append (alist-get category alist) (list module)))
finally return alist)
" ")
(string-join (sort (reverse lines) #'string-lessp)
"\n")))
(when interactive
(message "Copied to clipboard")))))))
;;;###autoload
(defun doom/commit-bumps ()
"Create a pre-filled magit commit for currently bumped packages."
(interactive)
(magit-commit-create
(list "-e" "-m" (doom/bumpify-diff))))
;;
;;; Package metadata
;;;###autoload
(defun doom-package-homepage (package)
"Return the url to PACKAGE's homepage (usually a repo)."
(doom-initialize-packages)
(or (get package 'homepage)
(put package 'homepage
(cond ((when-let (location (locate-library (symbol-name package)))
(with-temp-buffer
(if (string-match-p "\\.gz$" location)
(jka-compr-insert-file-contents location)
(insert-file-contents (concat (file-name-sans-extension location) ".el")
nil 0 4096))
(let ((case-fold-search t))
(when (re-search-forward " \\(?:URL\\|homepage\\|Website\\): \\(http[^\n]+\\)\n" nil t)
(match-string-no-properties 1))))))
((when-let ((recipe (straight-recipes-retrieve package)))
(straight--with-plist (straight--convert-recipe recipe)
(host repo)
(pcase host
(`github (format "https://github.com/%s" repo))
(`gitlab (format "https://gitlab.com/%s" repo))
(`bitbucket (format "https://bitbucket.com/%s" (plist-get plist :repo)))
(`git repo)
(_ nil)))))
((or package-archive-contents
(progn (package-refresh-contents)
package-archive-contents))
(pcase (ignore-errors (package-desc-archive (cadr (assq package package-archive-contents))))
(`nil nil)
("org" "https://orgmode.org")
((or "melpa" "melpa-mirror")
(format "https://melpa.org/#/%s" package))
("gnu"
(format "https://elpa.gnu.org/packages/%s.html" package))
(archive
(if-let (src (cdr (assoc package package-archives)))
(format "%s" src)
(user-error "%S isn't installed through any known source (%s)"
package archive)))))
((user-error "Can't get homepage for %S package" package))))))

61
lisp/lib/plist.el Normal file
View file

@ -0,0 +1,61 @@
;;; lisp/lib/plist.el -*- lexical-binding: t; -*-
;;
;;; Macros
;;; DEPRECATED In favor of `cl-callf'
;;;###autoload
(defmacro plist-put! (plist &rest rest)
"Set each PROP VALUE pair in REST to PLIST in-place."
`(cl-loop for (prop value)
on (list ,@rest) by #'cddr
do ,(if (symbolp plist)
`(setq ,plist (plist-put ,plist prop value))
`(plist-put ,plist prop value))))
;;
;;; Library
;;;###autoload
(defun doom-plist-get (plist prop &optional nil-value)
"Return PROP in PLIST, if it exists. Otherwise NIL-VALUE."
(if-let (val (plist-member plist prop))
(cadr val)
nil-value))
;;;###autoload
(defun doom-plist-merge (from-plist to-plist)
"Non-destructively merge FROM-PLIST onto TO-PLIST"
(let ((plist (copy-sequence from-plist)))
(while plist
(cl-callf plist-put to-plist (pop plist) (pop plist)))
to-plist))
;;;###autoload
(defun doom-plist-delete-nil (plist)
"Delete `nil' properties from a copy of PLIST."
(let (p)
(while plist
(if (car plist)
(cl-callf plist-put p (car plist) (nth 1 plist)))
(setq plist (cddr plist)))
p))
;;;###autoload
(defun doom-plist-keys (plist)
"Return the keys in PLIST."
(let (keys)
(while plist
(push (car plist) keys)
(setq plist (cddr plist)))
keys))
;;;###autoload
(defun doom-plist-values (plist)
"Return the values in PLIST."
(let (keys)
(while plist
(push (cadr plist) keys)
(setq plist (cddr plist)))
keys))

472
lisp/lib/print.el Normal file
View file

@ -0,0 +1,472 @@
;;; lisp/lib/print.el -*- lexical-binding: t; -*-
;;; Commentary
;;;
;;; This is Doom's output library, for controlling what does and doesn't get
;;; logged, and provides a simple DSL for formatting output. It's mainly to
;;; serve the noninteractive use-case, as `message' is more than good enough in
;;; interactive sessions, but `print!' and `doom-log' are safe to use as a
;;; drop-in replacement.
;;;
;;; Code:
(require 'ansi-color)
(defvar doom-print-ansi-alist
'(;; fx
(bold 1 :weight bold)
(dark 2)
(italic 3 :slant italic)
(underscore 4 :underline t)
(blink 5)
(rapid 6)
(contrary 7)
(concealed 8)
(strike 9 :strike-through t)
;; fg
(black 30 term-color-black)
(red 31 term-color-red)
(green 32 term-color-green)
(yellow 33 term-color-yellow)
(blue 34 term-color-blue)
(magenta 35 term-color-magenta)
(cyan 36 term-color-cyan)
(white 37 term-color-white)
;; bg
(on-black 40 term-color-black)
(on-red 41 term-color-red)
(on-green 42 term-color-green)
(on-yellow 43 term-color-yellow)
(on-blue 44 term-color-blue)
(on-magenta 45 term-color-magenta)
(on-cyan 46 term-color-cyan)
(on-white 47 term-color-white))
"An alist of fg/bg/fx names mapped to ansi codes and term-color-* variables.
This serves as the cipher for converting (COLOR ...) function calls in `print!'
and `format!' into colored output, where COLOR is any car of this list.")
(defvar doom-print-class-alist
`((buffer . doom-print--buffer)
(color . doom-print--style)
(class . doom-print--class)
(indent . doom-print--indent)
(fill . doom-print--fill)
(join . doom-print--join)
(org . doom-print--org)
(markup . doom-print--cli-markup)
(trim . string-trim)
(rtrim . string-trim-right)
(ltrim . string-trim-left)
(p . doom-print--paragraph)
(buffer . (lambda (buffer)
(with-current-buffer buffer
(buffer-string))))
(truncate . doom-print--truncate)
(success . (lambda (str &rest args)
(apply #'doom-print--style 'green
(doom-print--indent str "")
args)))
(warn . (lambda (str &rest args)
(apply #'doom-print--style 'yellow
(doom-print--indent str "! ")
args)))
(error . (lambda (str &rest args)
(apply #'doom-print--style 'red
(doom-print--indent str "x ")
args)))
(item . (lambda (str &rest args)
(doom-print--indent
(if args (apply #'format str args) str)
"- ")))
(start . (lambda (str &rest args)
(doom-print--indent
(if args (apply #'format str args) str)
"> ")))
(path . abbreviate-file-name)
(symbol . symbol-name)
(relpath . (lambda (str &optional dir)
(if (or (not str)
(not (stringp str))
(string-blank-p str))
str
(let ((dir (or dir (file-truename default-directory)))
(str (file-truename str)))
(if (file-in-directory-p str dir)
(file-relative-name str dir)
(abbreviate-file-name str))))))
(filename . file-name-nondirectory)
(dirname . (lambda (path)
(unless (file-directory-p path)
(setq path (file-name-directory path)))
(directory-file-name path))))
"An alist of text classes that map to transformation functions.
Any of these classes can be called like functions from within `format!' and
`print!' calls, which will transform their input.")
(defvar doom-print-indent 0
"Level to rigidly indent text returned by `format!' and `print!'.")
(defvar doom-print-indent-increment 2
"Steps in which to increment `doom-print-indent' for consecutive levels.")
(defvar doom-print-backend (if noninteractive 'ansi 'text-properties)
"Whether to print colors/styles with ANSI codes or with text properties.
Accepts `ansi' and `text-properties'. `nil' means don't render styles at all.")
(defvar doom-print-level (if init-file-debug 'debug 'info)
"The default level of messages to print.")
(defvar doom-print-logging-level 'debug
"The default logging level used by `doom-log'/`doom-print'.")
(defvar doom-print-message-level (if noninteractive 'debug 'info)
"The default logging level used by `message'.")
(defvar doom-print--levels
'(debug ; the system is thinking out loud
info ; a FYI; to keep you posted
warning ; a dismissable issue that may have reprecussions later
error)) ; functionality has been disabled by misbehavior
(dotimes (i (length doom-print--levels))
(put (nth i doom-print--levels) 'level i))
;;
;;; Library
;;;###autoload
(cl-defun doom-print
(output &key
(format t)
(newline t)
(stream standard-output)
(level doom-print-level))
"Print OUTPUT to stdout.
Unlike `message', this:
- Respects `standard-output'.
- Respects `doom-print-indent' (if FORMAT)
- Prints to stdout instead of stderr in batch mode.
- Respects more ANSI codes (only in batch mode).
- No-ops if OUTPUT is nil or an empty/blank string.
Returns OUTPUT."
(cl-check-type output (or null string))
(when (and (stringp output)
(not (string-blank-p output))
(or (eq level t)
(>= (get level 'level)
(get doom-print-level 'level))))
(let ((output (if format
(doom-print--format "%s" output)
output)))
(princ output stream)
(if newline (terpri stream))
output)))
;;;###autoload
(progn
;; Autoload whole definition, so its buried uses don't pull in this whole file
;; with them at expansion time.
(defmacro doom-log (output &rest args)
"Log a message in *Messages*.
Does not emit the message in the echo area. This is a macro instead of a
function to prevent the potentially expensive execution of its arguments when
debug mode is off."
`(when (or init-file-debug noninteractive)
(let ((inhibit-message t))
(message
"%s" (propertize
(doom-print--format
(format
"* [%s] %s"
,(let ((time `(format "%.06f" (float-time (time-subtract (current-time) before-init-time)))))
(cond (noninteractive time)
((bound-and-true-p doom--current-module)
(format "[:%s %s] "
(doom-keyword-name (car doom--current-module))
(cdr doom--current-module)))
((when-let (file (ignore-errors (file!)))
(format "[%s] "
(file-relative-name
file (doom-path (file-name-directory file) "../")))))
(time)))
,output)
,@args)
'face 'font-lock-doc-face))))))
;;;###autoload
(defmacro format! (message &rest args)
"An alternative to `format' that understands (color ...) and converts them
into faces or ANSI codes depending on the type of sesssion we're in."
`(doom-print--format ,@(doom-print--apply `(,message ,@args))))
;;;###autoload
(defmacro print-group! (&rest body)
"Indents any `print!' or `format!' output within BODY."
`(print-group-if! t ,@body))
;;;###autoload
(defmacro print-group-if! (condition &rest body)
"Indents any `print!' or `format!' output within BODY."
(declare (indent 1))
`(let ((doom-print-indent
(+ (if ,condition doom-print-indent-increment 0)
doom-print-indent)))
,@body))
;;;###autoload
(defmacro print! (message &rest args)
"Prints MESSAGE, formatted with ARGS, to stdout.
Returns non-nil if the message is a non-empty string.
Can be colored using (color ...) blocks:
(print! \"Hello %s\" (bold (blue \"How are you?\")))
(print! \"Hello %s\" (red \"World\"))
(print! (green \"Great %s!\") \"success\")
Uses faces in interactive sessions and ANSI codes otherwise."
`(doom-print (format! ,message ,@args) :format nil))
;;;###autoload
(defmacro insert! (&rest args)
"Like `insert', but with the power of `format!'.
Each argument in ARGS can be a list, as if they were arguments to `format!':
\(MESSAGE [ARGS...]).
\(fn &rest (MESSAGE . ARGS)...)"
`(insert ,@(cl-loop for arg in args
if (listp arg)
collect `(format! ,@arg)
else collect arg)))
;;
;;; Helpers
;;;###autoload
(defun doom-print--format (message &rest args)
(if (or (null message) (string-blank-p message))
""
(concat (make-string doom-print-indent 32)
(replace-regexp-in-string
"\n" (concat "\n" (make-string doom-print-indent 32))
(if args (apply #'format message args) message)
t t))))
;;;###autoload
(defun doom-print--indent (text &optional prefix)
"Indent TEXT by WIDTH spaces. If ARGS, format TEXT with them."
(with-temp-buffer
(let ((width
(cond ((null prefix)
doom-print-indent-increment)
((integerp prefix)
prefix)
((length (ansi-color-filter-apply (format "%s" prefix)))))))
(insert (format "%s" (or text "")))
(indent-rigidly (point-min) (point-max) width)
(when (stringp prefix)
(goto-char (point-min))
(delete-char width)
(insert prefix))
(buffer-string))))
;;;###autoload
(defun doom-print--fill (message &optional column indent)
"Ensure MSG is split into lines no longer than `fill-column'."
(with-temp-buffer
(let* ((fill-column (or column fill-column))
(col 0)
(indent (or indent 0))
(fill-prefix (make-string indent ?\s)))
(save-excursion
(insert (format "%s" (or message ""))))
;; HACK This monkey patches `fill-region' to not count ANSI codes as
;; legitimate characters, when calculating per-line `fill-column'.
(letf! (defun current-fill-column ()
(let ((target (funcall current-fill-column)))
(save-excursion
(goto-char (line-beginning-position))
(let ((n 0)
(c 0))
(while (and (not (eolp)) (<= n target))
(save-match-data
(if (looking-at ansi-color-control-seq-regexp)
(let ((len (length (match-string 0))))
(cl-incf c len)
(forward-char len))
(cl-incf n 1)
(forward-char 1))))
(+ target c (length fill-prefix))))))
(fill-region (point-min) (point-max) nil t))
(buffer-string))))
;;;###autoload
(defun doom-print--paragraph (&rest lines)
"TODO"
(doom-print--fill (apply #'concat lines)))
;;;###autoload
(defun doom-print--join (sequence &optional separator)
"Ensure SEQUENCE is joined with SEPARATOR.
`nil' and empty strings in SEQUENCE are omitted."
(mapconcat (doom-partial #'format "%s")
(seq-remove (fn! (or (null %)
(and (stringp %)
(string-empty-p %))))
sequence)
(or separator " ")))
;;;###autoload
(defun doom-print--truncate (text &optional col ellipsis)
"Replaces basic org markup with ansi/text-properties."
(truncate-string-to-width (or text "") (or col (- fill-column doom-print-indent))
nil nil (or ellipsis "...")))
;;;###autoload
(defun doom-print--buffer (buffer &optional beg end)
"Replaces basic org markup with ansi/text-properties."
(if (and (bufferp buffer) (buffer-live-p buffer))
(with-current-buffer buffer
(if (or beg end)
(buffer-substring (or beg (point-min))
(or end (point-max)))
(buffer-string)))
""))
;;;###autoload
(defun doom-print--cli-markup (text)
"Replace `...', `...`, and ```...``` quotes in TEXT with CLI formatting.
- `$ENVVAR' = bolded
- `--switch' = bolded
- `ARG' = underlined
- `symbol' = highlighted in blue
- `arbitrary code` = highlighted in blue
- ```
Arbitrary multiline code gets highlighted in blue too.
```"
(if (not text) ""
(let ((case-fold-search nil))
;; TODO Syntax highlighting?
(replace-regexp-in-string
" *```\n\\(.+?\\)\n *```" (doom-print--style 'blue "%s" "\\1")
(replace-regexp-in-string
"`\\$ \\([^`\n]+?\\)`" (format "`%s`" (doom-print--style 'blue "%s" "\\1"))
(replace-regexp-in-string
"`\\([^ \n]+?\\)'"
(let ((styles '(("^\\$" . envvar)
("^--?" . option)
("^[A-Z][A-Z0-9-_]*$" . arg)
("." . symbol))))
(lambda (match)
(let ((text (match-string 1 match)))
(pcase (assoc-default text styles #'string-match-p)
(`arg (doom-print--style 'underscore "%s" text))
(`envvar (doom-print--style 'bold "%s" text))
(`option (doom-print--style 'bold "%s" text))
(_ (format "`%s'" (doom-print--style 'blue "%s" text)))))))
text t)
t)
t))))
;;;###autoload
(defun doom-print--org (text)
"Replaces basic Org markup with ansi/text-properties.
All emphasis markers need to be preceded by a backslash."
(let* ((inhibit-modification-hooks t)
(styles '((?* . bold)
(?_ . underscore)
(?/ . italic)
(?= . magenta)
(?+ . strike)
(?~ . blue)))
(fences (regexp-quote (mapconcat #'char-to-string (mapcar #'car styles) ""))))
(with-temp-buffer
(save-excursion (insert text))
(while (re-search-forward (format "\\([%s]\\)" fences) nil t)
(unless (= (char-before (match-beginning 0)) ?\\)
(let* ((beg (match-beginning 0))
(ibeg (point))
(fence (match-string 1))
(fence-re (regexp-quote fence)))
(when (re-search-forward (format "[^\\]%s" fence-re) (line-end-position 2) t)
(let ((end (point))
(iend (1- (point))))
(let ((text (buffer-substring ibeg iend)))
(when-let (style (cdr (assq (string-to-char fence) styles)))
(goto-char beg)
(delete-region beg end)
(insert (doom-print--style style "%s" text)))))
(goto-char beg)))))
(buffer-string))))
;;;###autoload
(defun doom-print--style (style format &rest args)
"Apply STYLE to formatted MESSAGE with ARGS.
STYLE is a symbol that correlates to `doom-print-ansi-alist'.
In a noninteractive session, this wraps the result in ansi color codes.
Otherwise, it maps colors to a term-color-* face."
(let* ((code (cadr (assq style doom-print-ansi-alist)))
(format (format "%s" (or format "")))
(message (if args (apply #'format format args) format)))
(unless code
(error "Invalid print style: %s" style))
(pcase doom-print-backend
(`ansi
(format "\e[0%dm%s\e[%dm" code message 0))
(`text-properties
(require 'term) ; piggyback on term's color faces
(propertize
message
'face
(append (get-text-property 0 'face format)
(cond ((>= code 40)
`(:background ,(caddr (assq style doom-print-ansi-alist))))
((>= code 30)
`(:foreground ,(face-foreground (caddr (assq style doom-print-ansi-alist)))))
((cddr (assq style doom-print-ansi-alist)))))))
(_ message))))
;;;###autoload
(defun doom-print--class (class format &rest args)
"Apply CLASS to formatted format with ARGS.
CLASS is derived from `doom-print-class-alist', and can contain any arbitrary,
transformative logic."
(let (fn)
(cond ((setq fn (cdr (assq class doom-print-class-alist)))
(if (functionp fn)
(apply fn format args)
(error "%s does not have a function" class)))
(args (apply #'format format args))
(format))))
(defun doom-print--apply (forms &optional sub)
"Replace color-name functions with calls to `doom-print--style'."
(cond ((null forms) nil)
((listp forms)
(append (cond ((not (symbolp (car forms)))
(list (doom-print--apply (car forms))))
(sub
(list (car forms)))
((assq (car forms) doom-print-ansi-alist)
`(doom-print--style ',(car forms)))
((assq (car forms) doom-print-class-alist)
`(doom-print--class ',(car forms)))
((list (car forms))))
(doom-print--apply (cdr forms) t)
nil))
(forms)))

42
lisp/lib/process.el Normal file
View file

@ -0,0 +1,42 @@
;;; lisp/lib/process.el -*- lexical-binding: t; -*-
;;;###autoload
(defun doom-call-process (command &rest args)
"Execute COMMAND with ARGS synchronously.
Returns (STATUS . OUTPUT) when it is done, where STATUS is the returned error
code of the process and OUTPUT is its stdout output."
(with-temp-buffer
(cons (or (apply #'call-process command nil t nil (remq nil args))
-1)
(string-trim (buffer-string)))))
;;;###autoload
(defun doom-exec-process (command &rest args)
"Execute COMMAND with ARGS synchronously.
Unlike `doom-call-process', this pipes output to `standard-output' on the fly to
simulate 'exec' in the shell, so batch scripts could run external programs
synchronously without sacrificing their output.
Warning: freezes indefinitely on any stdin prompt."
;; FIXME Is there any way to handle prompts?
(with-temp-buffer
(cons (let ((process
(make-process :name "doom-sh"
:buffer (current-buffer)
:command (cons command (remq nil args))
:connection-type 'pipe))
done-p)
(set-process-filter
process (lambda (_process output)
(princ output (current-buffer))
(princ (doom--format output))))
(set-process-sentinel
process (lambda (process _event)
(when (memq (process-status process) '(exit stop))
(setq done-p t))))
(while (not done-p)
(sit-for 0.1))
(process-exit-status process))
(string-trim (buffer-string)))))

180
lisp/lib/projects.el Normal file
View file

@ -0,0 +1,180 @@
;;; lisp/lib/projects.el -*- lexical-binding: t; -*-
;; HACK We forward declare these variables because they are let-bound in a
;; number of places with no guarantee that they've been defined yet (i.e.
;; that `projectile' is loaded). If a variable is defined with `defvar'
;; while it is lexically bound, you get "Defining as dynamic an already
;; lexical var" errors in Emacs 28+).
;;;###autoload (defvar projectile-project-root nil)
;;;###autoload (defvar projectile-enable-caching (not noninteractive))
;;;###autoload (defvar projectile-require-project-root 'prompt)
;;;###autodef
(cl-defun set-project-type! (name &key predicate compile run test configure dir)
"Add a project type to `projectile-project-type'."
(declare (indent 1))
(after! projectile
(add-to-list 'projectile-project-types
(list name
'marker-files predicate
'compilation-dir dir
'configure-command configure
'compile-command compile
'test-command test
'run-command run))))
;;
;;; Macros
;;;###autoload
(defmacro project-file-exists-p! (files)
"Checks if the project has the specified FILES.
Paths are relative to the project root, unless they start with ./ or ../ (in
which case they're relative to `default-directory'). If they start with a slash,
they are absolute."
`(file-exists-p! ,files (doom-project-root)))
;;
;;; Commands
;;;###autoload
(defun doom/find-file-in-other-project (project-root)
"Performs `projectile-find-file' in a known project of your choosing."
(interactive
(list
(completing-read "Find file in project: " (projectile-relevant-known-projects))))
(unless (file-directory-p project-root)
(error "Project directory '%s' doesn't exist" project-root))
(doom-project-find-file project-root))
;;;###autoload
(defun doom/browse-in-other-project (project-root)
"Performs `find-file' in a known project of your choosing."
(interactive
(list
(completing-read "Browse in project: " (projectile-relevant-known-projects))))
(unless (file-directory-p project-root)
(error "Project directory '%s' doesn't exist" project-root))
(doom-project-browse project-root))
;;;###autoload
(defun doom/browse-in-emacsd ()
"Browse files from `doom-emacs-dir'."
(interactive) (doom-project-browse doom-emacs-dir))
;;;###autoload
(defun doom/find-file-in-emacsd ()
"Find a file under `doom-emacs-dir', recursively."
(interactive) (doom-project-find-file doom-emacs-dir))
;;;###autoload
(defun doom/add-directory-as-project (dir)
"Register an arbitrary directory as a project.
Unlike `projectile-add-known-project', if DIR isn't a valid project, a .project
file will be created within it so that it will always be treated as one. This
command will throw an error if a parent of DIR is a valid project (which would
mask DIR)."
(interactive "D")
(let ((short-dir (abbreviate-file-name dir)))
(unless (file-equal-p (doom-project-root dir) dir)
(with-temp-file (doom-path dir ".project")))
(let ((proj-dir (doom-project-root dir)))
(unless (file-equal-p proj-dir dir)
(user-error "Can't add %S as a project, because %S is already a project"
short-dir (abbreviate-file-name proj-dir)))
(message "%S was not a project; adding .project file to it"
short-dir (abbreviate-file-name proj-dir))
(projectile-add-known-project dir))))
;;
;;; Library
;;;###autoload
(defun doom-project-p (&optional dir)
"Return t if DIR (defaults to `default-directory') is a valid project."
(and (doom-project-root dir)
t))
;;;###autoload
(defun doom-project-root (&optional dir)
"Return the project root of DIR (defaults to `default-directory').
Returns nil if not in a project."
(let ((projectile-project-root
(unless dir (bound-and-true-p projectile-project-root)))
projectile-require-project-root)
(projectile-project-root dir)))
;;;###autoload
(defun doom-project-name (&optional dir)
"Return the name of the current project.
Returns '-' if not in a valid project."
(if-let (project-root (or (doom-project-root dir)
(if dir (expand-file-name dir))))
(funcall projectile-project-name-function project-root)
"-"))
;;;###autoload
(defun doom-project-expand (name &optional dir)
"Expand NAME to project root."
(expand-file-name name (doom-project-root dir)))
;;;###autoload
(defun doom-project-find-file (dir)
"Jump to a file in DIR (searched recursively).
If DIR is not a project, it will be indexed (but not cached)."
(unless (file-directory-p dir)
(error "Directory %S does not exist" dir))
(unless (file-readable-p dir)
(error "Directory %S isn't readable" dir))
(let* ((default-directory (file-truename dir))
(projectile-project-root (doom-project-root dir))
(projectile-enable-caching projectile-enable-caching))
(cond ((and projectile-project-root (file-equal-p projectile-project-root default-directory))
(unless (doom-project-p default-directory)
;; Disable caching if this is not a real project; caching
;; non-projects easily has the potential to inflate the projectile
;; cache beyond reason.
(setq projectile-enable-caching nil))
(call-interactively
;; 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)
#'counsel-projectile-find-file
#'projectile-find-file)))
((and (bound-and-true-p vertico-mode)
(fboundp '+vertico/find-file-in))
(+vertico/find-file-in default-directory))
((and (bound-and-true-p ivy-mode)
(fboundp 'counsel-file-jump))
(call-interactively #'counsel-file-jump))
((project-current nil dir)
(project-find-file-in nil nil dir))
((and (bound-and-true-p helm-mode)
(fboundp 'helm-find-files))
(call-interactively #'helm-find-files))
((call-interactively #'find-file)))))
;;;###autoload
(defun doom-project-browse (dir)
"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)
#'counsel-find-file)
((doom-module-p :completion 'helm)
#'helm-find-files)
(#'find-file)))))
;;;###autoload
(defun doom-project-ignored-p (project-root)
"Return non-nil if temporary file or a straight package."
(unless (file-remote-p project-root)
(or (file-in-directory-p project-root temporary-file-directory)
(file-in-directory-p project-root doom-local-dir))))

183
lisp/lib/sandbox.el Normal file
View file

@ -0,0 +1,183 @@
;;; lisp/lib/sandbox.el -*- lexical-binding: t; -*-
(defvar doom-sandbox-buffer-name "*doom:sandbox*"
"Name of the Doom sandbox buffer.")
(defvar doom-sandbox-dir
(expand-file-name "doom-sandbox" (temporary-file-directory))
"TODO")
(defvar doom-sandbox-preamble
";; Welcome to the sandbox!
;;
;; This is a test bed for running Emacs Lisp in another instance of Emacs that
;; has varying amounts of Doom loaded:
;;
;; - vanilla Emacs (nothing loaded) \\[doom--run-vanilla-emacs]
;; - vanilla Doom (only Doom core) \\[doom--run-vanilla-doom]
;; - Doom + modules - your private config \\[doom--run-vanilla-doom+]
;; - Doom + modules + your private config \\[doom--run-full-doom]
;;
;; This is done without sacrificing access to installed packages. Use the sandbox
;; to reproduce bugs and determine if Doom is to blame.\n\n"
"TODO")
(defun doom--sandbox-launch (args forms)
(require 'package)
(require 'restart-emacs)
(let* ((sandbox-file (expand-file-name "init.el" doom-sandbox-dir))
(args (append args (list "-l" sandbox-file))))
(delete-directory doom-sandbox-dir 'recursive)
(make-directory doom-sandbox-dir 'parents)
(with-temp-file sandbox-file
(prin1 forms (current-buffer)))
(condition-case-unless-debug e
(cond ((display-graphic-p)
(if (memq system-type '(windows-nt ms-dos))
(restart-emacs--start-gui-on-windows args)
(restart-emacs--start-gui-using-sh args)))
((memq system-type '(windows-nt ms-dos))
(user-error "Cannot start another Emacs from Windows shell."))
((suspend-emacs
(format "%s %s -nw; fg"
(shell-quote-argument (restart-emacs--get-emacs-binary))
(mapconcat #'shell-quote-argument args " ")))))
(error
(delete-directory doom-sandbox-dir 'recursive)
(signal (car e) (cdr e))))))
(defun doom--sandbox-run (&optional mode)
"TODO"
(doom--sandbox-launch
(unless (eq mode 'doom) '("-Q"))
(let ((forms
(read (format "(progn\n%s\n)"
(buffer-substring-no-properties
(point-min)
(point-max))))))
(if (eq mode 'doom)
forms
`(progn
;; doom variables
(setq init-file-debug t
doom-emacs-dir ,doom-emacs-dir
doom-cache-dir ,(expand-file-name "cache/" doom-sandbox-dir)
doom-etc-dir ,(expand-file-name "etc/" doom-sandbox-dir))
(defun doom--write-to-etc-dir-a (fn &rest args)
(let ((user-emacs-directory doom-etc-dir))
(apply fn args)))
(advice-add #'locate-user-emacs-file :around #'doom--write-to-etc-dir-a)
;; emacs essential variables
(setq before-init-time (current-time)
after-init-time nil
init-file-debug init-file-debug
noninteractive nil
process-environment (get 'process-environment 'initial-value)
exec-path (get 'exec-path 'initial-value)
load-path ',load-path
user-init-file load-file-name)
;; package.el
(setq package--init-file-ensured t
package-user-dir ,package-user-dir
package-archives ',package-archives)
;; (add-hook 'kill-emacs-hook
;; (lambda ()
;; (delete-file user-init-file)
;; (when (file-equal-p user-emacs-directory ,doom-sandbox-dir)
;; (delete-directory user-emacs-directory 'recursive))))
(with-eval-after-load 'undo-tree
;; HACK `undo-tree' sometimes throws errors because
;; `buffer-undo-tree' isn't correctly initialized.
(setq-default buffer-undo-tree (make-undo-tree)))
;; Then launch as much about Emacs as we can
(defun --run-- () ,forms)
,(pcase mode
(`doom
'(--run--))
(`vanilla-doom+ ; Doom core + modules - private config
`(progn
(load-file ,(expand-file-name "doom.el" doom-core-dir))
(setq doom-modules-dirs (list doom-modules-dir))
(let ((doom-init-modules-p t))
(doom-initialize)
(doom-initialize-core-modules))
(setq doom-modules ',doom-modules)
(maphash (lambda (key plist)
(doom-module-put
(car key) (cdr key)
:path (doom-module-locate-path (car key) (cdr key))))
doom-modules)
(--run--)
(maphash (doom-module-loader doom-module-init-file) doom-modules)
(maphash (doom-module-loader doom-module-config-file) doom-modules)
(doom-run-hooks 'doom-init-modules-hook)))
(`vanilla-doom ; only Doom core
`(progn
(load-file ,(expand-file-name "doom.el" doom-core-dir))
(let ((doom-init-modules-p t))
(doom-initialize)
(doom-initialize-core-modules))
(--run--)))
(`vanilla ; nothing loaded
`(progn
(if (boundp 'comp-deferred-compilation)
;; REVIEW Remove me after a month
(setq comp-deferred-compilation nil
comp-deferred-compilation-deny-list ',(bound-and-true-p native-comp-async-env-modifier-form)
comp-async-env-modifier-form ',(bound-and-true-p native-comp-async-env-modifier-form)
comp-eln-load-path ',(bound-and-true-p native-comp-eln-load-path))
(setq native-comp-deferred-compilation nil
native-comp-deferred-compilation-deny-list ',(bound-and-true-p native-comp-async-env-modifier-form)
native-comp-async-env-modifier-form ',(bound-and-true-p native-comp-async-env-modifier-form)
native-comp-eln-load-path ',(bound-and-true-p native-comp-eln-load-path)))
(package-initialize t)
(--run--))))
;; Then rerun Emacs' startup hooks to simulate a fresh Emacs session,
;; because they've already fired.
(fset 'doom-run-hook #',(symbol-function #'doom-run-hook))
(fset 'doom-run-hooks #',(symbol-function #'doom-run-hooks))
(fset 'doom-run-all-startup-hooks-h #',(symbol-function #'doom-run-all-startup-hooks-h))
(doom-run-all-startup-hooks-h))))))
(fset 'doom--run-vanilla-emacs (cmd! (doom--sandbox-run 'vanilla)))
(fset 'doom--run-vanilla-doom (cmd! (doom--sandbox-run 'vanilla-doom)))
(fset 'doom--run-vanilla-doom+ (cmd! (doom--sandbox-run 'vanilla-doom+)))
(fset 'doom--run-full-doom (cmd! (doom--sandbox-run 'doom)))
(defvar doom-sandbox-emacs-lisp-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'doom--run-vanilla-emacs)
(define-key map (kbd "C-c C-d") #'doom--run-vanilla-doom)
(define-key map (kbd "C-c C-p") #'doom--run-vanilla-doom+)
(define-key map (kbd "C-c C-f") #'doom--run-full-doom)
(define-key map (kbd "C-c C-k") #'kill-current-buffer)
map))
(define-derived-mode doom-sandbox-emacs-lisp-mode emacs-lisp-mode "Sandbox Elisp"
"TODO")
;;;###autoload
(defun doom/sandbox ()
"Open the Emacs Lisp sandbox.
This is a test bed for running Emacs Lisp in another instance of Emacs with
varying amounts of Doom loaded, including:
a) vanilla Emacs (nothing loaded),
b) vanilla Doom (only Doom core),
c) Doom + modules - your private config or
c) Doom + modules + your private config (a complete Doom session)
This is done without sacrificing access to installed packages. Use the sandbox
to reproduce bugs and determine if Doom is to blame."
(interactive)
(pop-to-buffer
(with-current-buffer (get-buffer-create doom-sandbox-buffer-name)
(doom-sandbox-emacs-lisp-mode)
(setq-local default-directory doom-emacs-dir)
(and (buffer-live-p (get-buffer doom-sandbox-buffer-name))
(= (buffer-size) 0)
(insert (substitute-command-keys doom-sandbox-preamble)))
(goto-char (point-max))
(current-buffer))))

199
lisp/lib/scratch.el Normal file
View file

@ -0,0 +1,199 @@
;;; lisp/lib/scratch.el -*- lexical-binding: t; -*-
(defvar doom-scratch-default-file "__default"
"The default file name for a project-less scratch buffer.
Will be saved in `doom-scratch-dir'.")
(defvar doom-scratch-dir (concat doom-etc-dir "scratch")
"Where to save persistent scratch buffers.")
(defvar doom-scratch-initial-major-mode nil
"What major mode to start fresh scratch buffers in.
Scratch buffers preserve their last major mode, however, so this only affects
the first, fresh scratch buffer you create. This accepts:
t Inherits the major mode of the last buffer you had selected.
nil Uses `fundamental-mode'
MAJOR-MODE Any major mode symbol")
(defvar doom-scratch-buffers nil
"A list of active scratch buffers.")
(defvar doom-scratch-current-project nil
"The name of the project associated with the current scratch buffer.")
(put 'doom-scratch-current-project 'permanent-local t)
(defvar doom-scratch-buffer-hook ()
"The hooks to run after a scratch buffer is created.")
(defun doom--load-persistent-scratch-buffer (project-name)
(setq-local doom-scratch-current-project
(or project-name
doom-scratch-default-file))
(let ((smart-scratch-file
(expand-file-name (concat doom-scratch-current-project ".el")
doom-scratch-dir)))
(make-directory doom-scratch-dir t)
(when (file-readable-p smart-scratch-file)
(message "Reading %s" smart-scratch-file)
(cl-destructuring-bind (content point mode)
(with-temp-buffer
(save-excursion (insert-file-contents smart-scratch-file))
(read (current-buffer)))
(erase-buffer)
(funcall mode)
(insert content)
(goto-char point)
t))))
;;;###autoload
(defun doom-scratch-buffer (&optional dont-restore-p mode directory project-name)
"Return a scratchpad buffer in major MODE."
(let* ((buffer-name (if project-name
(format "*doom:scratch (%s)*" project-name)
"*doom:scratch*"))
(buffer (get-buffer buffer-name)))
(with-current-buffer
(or buffer (get-buffer-create buffer-name))
(setq default-directory directory)
(setq-local so-long--inhibited t)
(if dont-restore-p
(erase-buffer)
(unless buffer
(doom--load-persistent-scratch-buffer project-name)
(when (and (eq major-mode 'fundamental-mode)
(functionp mode))
(funcall mode))))
(cl-pushnew (current-buffer) doom-scratch-buffers)
(add-transient-hook! 'doom-switch-buffer-hook (doom-persist-scratch-buffers-h))
(add-transient-hook! 'doom-switch-window-hook (doom-persist-scratch-buffers-h))
(add-hook 'kill-buffer-hook #'doom-persist-scratch-buffer-h nil 'local)
(run-hooks 'doom-scratch-buffer-created-hook)
(current-buffer))))
;;
;;; Persistent scratch buffer
;;;###autoload
(defun doom-persist-scratch-buffer-h ()
"Save the current buffer to `doom-scratch-dir'."
(let ((content (buffer-substring-no-properties (point-min) (point-max)))
(point (point))
(mode major-mode))
(with-temp-file
(expand-file-name (concat (or doom-scratch-current-project
doom-scratch-default-file)
".el")
doom-scratch-dir)
(prin1 (list content
point
mode)
(current-buffer)))))
;;;###autoload
(defun doom-persist-scratch-buffers-h ()
"Save all scratch buffers to `doom-scratch-dir'."
(setq doom-scratch-buffers
(cl-delete-if-not #'buffer-live-p doom-scratch-buffers))
(dolist (buffer doom-scratch-buffers)
(with-current-buffer buffer
(doom-persist-scratch-buffer-h))))
;;;###autoload
(defun doom-persist-scratch-buffers-after-switch-h ()
"Kill scratch buffers when they are no longer visible, saving them to disk."
(unless (cl-some #'get-buffer-window doom-scratch-buffers)
(mapc #'kill-buffer doom-scratch-buffers)
(remove-hook 'doom-switch-buffer-hook #'doom-persist-scratch-buffers-after-switch-h)))
;;;###autoload
(unless noninteractive
(add-hook 'kill-emacs-hook #'doom-persist-scratch-buffers-h))
;;
;;; Commands
(defvar projectile-enable-caching)
;;;###autoload
(defun doom/open-scratch-buffer (&optional arg project-p same-window-p)
"Pop up a persistent scratch buffer.
If passed the prefix ARG, do not restore the last scratch buffer.
If PROJECT-P is non-nil, open a persistent scratch buffer associated with the
current project."
(interactive "P")
(let (projectile-enable-caching)
(funcall
(if same-window-p
#'switch-to-buffer
#'pop-to-buffer)
(doom-scratch-buffer
arg
(cond ((eq doom-scratch-initial-major-mode t)
(unless (or buffer-read-only
(derived-mode-p 'special-mode)
(string-match-p "^ ?\\*" (buffer-name)))
major-mode))
((null doom-scratch-initial-major-mode)
nil)
((symbolp doom-scratch-initial-major-mode)
doom-scratch-initial-major-mode))
default-directory
(when project-p
(doom-project-name))))))
;;;###autoload
(defun doom/switch-to-scratch-buffer (&optional arg project-p)
"Like `doom/open-scratch-buffer', but switches to it in the current window.
If passed the prefix ARG, do not restore the last scratch buffer."
(interactive "P")
(doom/open-scratch-buffer arg project-p 'same-window))
;;;###autoload
(defun doom/open-project-scratch-buffer (&optional arg same-window-p)
"Opens the (persistent) project scratch buffer in a popup.
If passed the prefix ARG, do not restore the last scratch buffer."
(interactive "P")
(doom/open-scratch-buffer arg 'project same-window-p))
;;;###autoload
(defun doom/switch-to-project-scratch-buffer (&optional arg)
"Like `doom/open-project-scratch-buffer', but switches to it in the current
window.
If passed the prefix ARG, do not restore the last scratch buffer."
(interactive "P")
(doom/open-project-scratch-buffer arg 'same-window))
;;;###autoload
(defun doom/revert-scratch-buffer ()
"Revert scratch buffer to last persistent state."
(interactive)
(unless (string-match-p "^\\*doom:scratch" (buffer-name))
(user-error "Not in a scratch buffer"))
(when (doom--load-persistent-scratch-buffer doom-scratch-current-project)
(message "Reloaded scratch buffer")))
;;;###autoload
(defun doom/delete-persistent-scratch-file (&optional arg)
"Deletes a scratch buffer file in `doom-scratch-dir'.
If prefix ARG, delete all persistent scratches."
(interactive)
(if arg
(progn
(delete-directory doom-scratch-dir t)
(message "Cleared %S" (abbreviate-file-name doom-scratch-dir)))
(make-directory doom-scratch-dir t)
(let ((file (read-file-name "Delete scratch file > " doom-scratch-dir "scratch")))
(if (not (file-exists-p file))
(message "%S does not exist" (abbreviate-file-name file))
(delete-file file)
(message "Successfully deleted %S" (abbreviate-file-name file))))))

152
lisp/lib/sessions.el Normal file
View file

@ -0,0 +1,152 @@
;;; lisp/lib/sessions.el -*- lexical-binding: t; -*-
(defvar desktop-base-file-name)
(defvar desktop-dirname)
(defvar desktop-restore-eager)
(defvar desktop-file-modtime)
;;
;;; Helpers
;;;###autoload
(defun doom-session-file (&optional name)
"TODO"
(cond ((require 'persp-mode nil t)
(expand-file-name (or name persp-auto-save-fname) persp-save-dir))
((require 'desktop nil t)
(if name
(expand-file-name name (file-name-directory (desktop-full-file-name)))
(desktop-full-file-name)))
((error "No session backend available"))))
;;;###autoload
(defun doom-save-session (&optional file)
"TODO"
(setq file (expand-file-name (or file (doom-session-file))))
(cond ((require 'persp-mode nil t)
(unless persp-mode (persp-mode +1))
(setq persp-auto-save-opt 0)
(persp-save-state-to-file file))
((and (require 'frameset nil t)
(require 'restart-emacs nil t))
(let ((frameset-filter-alist (append '((client . restart-emacs--record-tty-file))
frameset-filter-alist))
(desktop-base-file-name (file-name-nondirectory file))
(desktop-dirname (file-name-directory file))
(desktop-restore-eager t)
desktop-file-modtime)
(make-directory desktop-dirname t)
;; Prevents confirmation prompts
(let ((desktop-file-modtime (nth 5 (file-attributes (desktop-full-file-name)))))
(desktop-save desktop-dirname t))))
((error "No session backend to save session with"))))
;;;###autoload
(defun doom-load-session (&optional file)
"TODO"
(setq file (expand-file-name (or file (doom-session-file))))
(message "Attempting to load %s" file)
(cond ((not (file-readable-p file))
(message "No session file at %S to read from" file))
((require 'persp-mode nil t)
(unless persp-mode
(persp-mode +1))
(let ((allowed (persp-list-persp-names-in-file file)))
(cl-loop for name being the hash-keys of *persp-hash*
unless (member name allowed)
do (persp-kill name))
(persp-load-state-from-file file)))
((and (require 'frameset nil t)
(require 'restart-emacs nil t))
(restart-emacs--restore-frames-using-desktop file))
((error "No session backend to load session with"))))
;;
;;; Commands
;;;###autoload
(defun doom/quickload-session ()
"TODO"
(interactive)
(message "Restoring session...")
(doom-load-session)
(message "Session restored. Welcome back."))
;;;###autoload
(defun doom/quicksave-session ()
"TODO"
(interactive)
(message "Saving session")
(doom-save-session)
(message "Saving session...DONE"))
;;;###autoload
(defun doom/load-session (file)
"TODO"
(interactive
(let ((session-file (doom-session-file)))
(list (or (read-file-name "Session to restore: "
(file-name-directory session-file)
(file-name-nondirectory session-file)
t)
(user-error "No session selected. Aborting")))))
(unless file
(error "No session file selected"))
(message "Loading '%s' session" file)
(doom-load-session file)
(message "Session restored. Welcome back."))
;;;###autoload
(defun doom/save-session (file)
"TODO"
(interactive
(let ((session-file (doom-session-file)))
(list (or (read-file-name "Save session to: "
(file-name-directory session-file)
(file-name-nondirectory session-file))
(user-error "No session selected. Aborting")))))
(unless file
(error "No session file selected"))
(message "Saving '%s' session" file)
(doom-save-session file))
;;;###autoload
(defun doom/restart ()
"Restart Emacs (and the daemon, if active).
Unlike `doom/restart-and-restore', does not restart the current session."
(interactive)
(require 'restart-emacs)
(restart-emacs))
;;;###autoload
(defun doom/restart-and-restore (&optional debug)
"Restart Emacs (and the daemon, if active).
If DEBUG (the prefix arg) is given, start the new instance with the --debug
switch."
(interactive "P")
(require 'restart-emacs)
(doom/quicksave-session)
(save-some-buffers nil t)
(letf! ((#'save-buffers-kill-emacs #'kill-emacs)
(confirm-kill-emacs)
(tmpfile (make-temp-file "post-load")))
;; HACK `restart-emacs' does not properly escape arguments on Windows (in
;; `restart-emacs--daemon-on-windows' and
;; `restart-emacs--start-gui-on-windows'), so don't give it complex
;; arguments at all. Should be fixed upstream, but restart-emacs seems to
;; be unmaintained.
(with-temp-file tmpfile
(print `(progn
(when (boundp 'doom-version)
(add-hook 'window-setup-hook #'doom-load-session 100))
(delete-file ,tmpfile))
(current-buffer)))
(restart-emacs
(append (if debug (list "--debug-init"))
(when (boundp 'chemacs-current-emacs-profile)
(list "--with-profile" chemacs-current-emacs-profile))
(list "-l" tmpfile)))))

155
lisp/lib/store.el Normal file
View file

@ -0,0 +1,155 @@
;;; lisp/lib/cache.el -*- lexical-binding: t; -*-
;; This little library abstracts the process of writing arbitrary elisp values
;; to a 2-tiered file store (in `doom-store-dir'/`doom-store-location').
(defvar doom-store-dir (concat doom-etc-dir "store/")
"Directory to look for and store data accessed through this API.")
(defvar doom-store-persist-alist ()
"An alist of alists, containing lists of variables for the doom cache library
to persist across Emacs sessions.")
(defvar doom-store-location "default"
"The default location for cache files. This symbol is translated into a file
name under `pcache-directory' (by default a subdirectory under
`doom-store-dir'). One file may contain multiple cache entries.")
(defvar doom--store-table (make-hash-table :test 'equal))
(defun doom-save-persistent-store-h ()
"Hook to persist `doom-store's storage when Emacs is killed."
(let (locations)
;; Persist `doom-store-persist-alist'
(dolist (alist (butlast doom-store-persist-alist 1))
(cl-loop with location = (car alist)
for var in (cdr alist)
do (doom-store-put var (symbol-value var) nil location 'noflush)
and do (cl-pushnew location locations :test #'equal)))
;; Clean up expired entries,
(dolist (location (doom-files-in doom-store-dir :relative-to doom-store-dir))
(maphash (lambda (key val)
(when (doom--store-expired-p key val)
(cl-pushnew location locations :test #'equal)
(doom--store-rem key location 'noflush)))
(doom--store-init location)))
(mapc #'doom--store-flush locations)))
(add-hook 'kill-emacs-hook #'doom-save-persistent-store-h)
;;
;;; Library
;;;###autoload
(defun doom-store-persist (location variables)
"Persist VARIABLES (list of symbols) in LOCATION (symbol).
This populates these variables with cached values, if one exists, and saves them
to file when Emacs quits. This cannot persist buffer-local variables."
(cl-check-type location string)
(dolist (var variables)
(when (doom-store-member-p var location)
(set var (doom-store-get var location))))
(setf (alist-get location doom-store-persist-alist)
(append variables (alist-get location doom-store-persist-alist))))
;;;###autoload
(defun doom-store-desist (location &optional variables)
"Unregisters VARIABLES (list of symbols) in LOCATION (symbol).
Variables to persist are recorded in `doom-store-persist-alist'. Does not affect
the actual variables themselves or their values."
(cl-check-type location string)
(if variables
(setf (alist-get location doom-store-persist-alist)
(cl-set-difference (cdr (assq location doom-store-persist-alist))
variables))
(delq! location doom-store-persist-alist 'assoc)))
(defun doom--store-init (&optional location)
(cl-check-type location (or null string))
(let ((location (or location doom-store-location)))
(or (gethash location doom--store-table)
(let* ((file-name-handler-alist nil)
(location-path (expand-file-name location doom-store-dir)))
(if (file-exists-p location-path)
(puthash location
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(insert-file-contents-literally location-path)
(read (current-buffer)))
doom--store-table)
(puthash location (make-hash-table :test 'equal)
doom--store-table))))))
(defun doom--store-expired-p (key data)
(let ((ttl (car data)))
(cond ((functionp ttl)
(not (funcall ttl key data)))
((consp ttl)
(time-less-p ttl (current-time))))))
(defun doom--store-flush (location)
"Write `doom--store-table' to `doom-store-dir'."
(let ((file-name-handler-alist nil)
(coding-system-for-write 'binary)
(write-region-annotate-functions nil)
(write-region-post-annotation-function nil))
(let* ((location (or location doom-store-location))
(data (doom--store-init location)))
(make-directory doom-store-dir 'parents)
(with-temp-file (expand-file-name location doom-store-dir)
(prin1 data (current-buffer)))
data)))
;;;###autoload
(defun doom-store-get (key &optional location default-value noflush)
"Retrieve KEY from LOCATION (defaults to `doom-store-location').
If it doesn't exist or has expired, DEFAULT_VALUE is returned."
(let ((data (gethash key (doom--store-init location) default-value)))
(if (not (or (eq data default-value)
(doom--store-expired-p key data)))
(cdr data)
(doom-store-rem key location noflush)
default-value)))
;;;###autoload
(defun doom-store-put (key value &optional ttl location noflush)
"Set KEY to VALUE in the store at LOCATION.
KEY can be any lisp object that is comparable with `equal'. TTL is the duration
(in seconds) after which this cache entry expires; if nil, no cache expiration.
LOCATION is the super-key to store this cache item under. It defaults to
`doom-store-location'."
(cl-check-type ttl (or null integer function))
(puthash key (cons (if (integerp ttl)
(time-add (current-time) ttl)
ttl)
value)
(doom--store-init location))
(unless noflush
(doom--store-flush location)))
;;;###autoload
(defun doom-store-rem (key &optional location noflush)
"Clear a cache LOCATION (defaults to `doom-store-location')."
(remhash key (doom--store-init location))
(unless noflush
(doom--store-flush location)))
;;;###autoload
(defun doom-store-member-p (key &optional location)
"Return t if KEY in LOCATION exists.
LOCATION defaults to `doom-store-location'."
(let ((nil-value (format "--nilvalue%s--" (current-time))))
(not (equal (doom-store-get key location nil-value)
nil-value))))
;;;###autoload
(defun doom-store-clear (&optional location)
"Clear the store at LOCATION (defaults to `doom-store-location')."
(let* ((location (or location doom-store-location))
(path (expand-file-name location doom-store-dir)))
(remhash location doom--store-table)
(when (file-exists-p path)
(delete-file path)
t)))

108
lisp/lib/system.el Normal file
View file

@ -0,0 +1,108 @@
;;; lisp/lib/system.el -*- lexical-binding: t; -*-
;;;###autoload
(defun doom-system-distro ()
"Return a symbol representing the installed distro."
(cond (IS-WINDOWS 'windows)
(IS-MAC 'macos)
((and (file-exists-p "/etc/os-release")
(with-temp-buffer
(insert-file-contents "/etc/os-release")
(when (re-search-forward "^ID=\"?\\([^\"\n]+\\)\"?" nil t)
(intern (downcase (match-string 1)))))))
;; A few redundancies in case os-release fails us
((file-exists-p "/etc/debian_version")
'debian)
((executable-find "nixos-version")
'nixos)
((and (or (file-exists-p "/etc/config.scm")
(file-directory-p "/run/current-system"))
(executable-find "guix"))
'guix)
('linux)))
;;;###autoload
(defun doom-system-distro-version ()
"Return a distro name and version string."
(letf! (defun sh (&rest args) (cdr (apply #'doom-call-process args)))
(let ((distro (doom-system-distro)))
(cond
((eq distro 'windows)
(format "Windows %s" "Unknown")) ; TODO
((eq distro 'macos)
(format "MacOS %s" (sh "sw_vers" "-productVersion")))
((executable-find "lsb_release")
(sh "lsb_release" "-s" "-d"))
((executable-find "nixos-version")
(format "NixOS %s" (sh "nixos-version")))
((and (file-exists-p "/etc/os-release")
(with-temp-buffer
(insert-file-contents "/etc/os-release")
(when (re-search-forward "^PRETTY_NAME=\"?\\([^\"\n]+\\)\"?" nil t)
(match-string 1)))))
((when-let (files (doom-glob "/etc/*-release"))
(truncate-string-to-width
(replace-regexp-in-string "\n" " " (cat (car files) 73) nil t)
64 nil nil "...")))
((concat "Unknown " (sh "uname" "-v")))))))
;;;###autoload
(defun doom-system-distro-icon ()
"Display icon for the installed distro."
(propertize
(pcase (doom-system-distro)
(`windows (all-the-icons-faicon "windows"))
(`macos (all-the-icons-faicon "apple"))
(`arch "\uF303")
(`debian "\uF306")
(`raspbian "\uF315")
(`ubuntu "\uF31b")
(`elementary "\uF309")
(`fedora "\uF30a")
(`coreos "\uF305")
(`gentoo "\uF30d")
(`mageia "\uF310")
(`centos "\uF304")
((or `opensuse `tumbleweed) "\uF314")
(`sabayon "\uF317")
(`slackware "\uF319")
(`linuxmint "\uF30e")
(`alpine "\uF300")
(`aosc "\uF301")
(`nixos "\uF313")
(`devuan "\uF307")
(`manjaro "\uF312")
((or `void `artix) "\uF17c")
(_ (all-the-icons-faicon "linux")))
'face '(:height 1)
'display '(raise 0)))
;;;###autoload
(defun doom-system-cpus ()
"Return the max number of processing units on this system.
Tries to be portable. Returns 1 if cannot be determined."
(or (get 'doom-system-cpus 'cached-value)
(put 'doom-system-cpus 'cached-value
(let ((cpus
(cond ((fboundp 'w32-get-nproc)
(w32-get-nproc))
((getenv "NUMBER_OF_PROCESSORS"))
((executable-find "nproc")
(doom-call-process "nproc"))
((executable-find "sysctl")
(doom-call-process "sysctl" "-n" "hw.ncpu")))))
(max
1 (or (cl-typecase cpus
(integer cpus)
(string
(condition-case _
(string-to-number cpus)
(wrong-type-argument
(user-error "NUMBER_OF_PROCESSORS contains an invalid value: %S"
cpus))))
(cons
(if (zerop (car cpus))
(string-to-number (cdr cpus))
(user-error "Failed to look up number of processors, because:\n\n%s"
(cdr cpus)))))
1))))))

350
lisp/lib/text.el Normal file
View file

@ -0,0 +1,350 @@
;;; lisp/lib/text.el -*- lexical-binding: t; -*-
;;;###autoload
(defvar doom-point-in-comment-functions ()
"List of functions to run to determine if point is in a comment.
Each function takes one argument: the position of the point. Stops on the first
function to return non-nil. Used by `doom-point-in-comment-p'.")
;;;###autoload
(defvar doom-point-in-string-functions ()
"List of functions to run to determine if point is in a string.
Each function takes one argument: the position of the point. Stops on the first
function to return non-nil. Used by `doom-point-in-string-p'.")
;;;###autoload
(defun doom-surrounded-p (pair &optional inline balanced)
"Returns t if point is surrounded by a brace delimiter: {[(
If INLINE is non-nil, only returns t if braces are on the same line, and
whitespace is balanced on either side of the cursor.
If INLINE is nil, returns t if the opening and closing braces are on adjacent
lines, above and below, with only whitespace in between."
(when pair
(let ((beg (plist-get pair :beg))
(end (plist-get pair :end))
(pt (point)))
(when (and (> pt beg) (< pt end))
(when-let* ((cl (plist-get pair :cl))
(op (plist-get pair :op)))
(and (not (string= op ""))
(not (string= cl ""))
(let ((nbeg (+ (length op) beg))
(nend (- end (length cl))))
(let ((content (buffer-substring-no-properties nbeg nend)))
(and (string-match-p (format "[ %s]*" (if inline "" "\n")) content)
(or (not balanced)
(= (- pt nbeg) (- nend pt))))))))))))
;;;###autoload
(defun doom-point-in-comment-p (&optional pos)
"Return non-nil if POS is in a comment.
POS defaults to the current position."
(let ((pos (or pos (point))))
(if doom-point-in-comment-functions
(run-hook-with-args-until-success 'doom-point-in-comment-functions pos)
(nth 4 (syntax-ppss pos)))))
;;;###autoload
(defun doom-point-in-string-p (&optional pos)
"Return non-nil if POS is in a string."
;; REVIEW Should we cache `syntax-ppss'?
(let ((pos (or pos (point))))
(if doom-point-in-string-functions
(run-hook-with-args-until-success 'doom-point-in-string-functions pos)
(nth 3 (syntax-ppss pos)))))
;;;###autoload
(defun doom-point-in-string-or-comment-p (&optional pos)
"Return non-nil if POS is in a string or comment."
(or (doom-point-in-string-p pos)
(doom-point-in-comment-p pos)))
;;;###autoload
(defun doom-region-active-p ()
"Return non-nil if selection is active.
Detects evil visual mode as well."
(declare (side-effect-free t))
(or (use-region-p)
(and (bound-and-true-p evil-local-mode)
(evil-visual-state-p))))
;;;###autoload
(defun doom-region-beginning ()
"Return beginning position of selection.
Uses `evil-visual-beginning' if available."
(declare (side-effect-free t))
(or (and (bound-and-true-p evil-local-mode)
(markerp evil-visual-beginning)
(marker-position evil-visual-beginning))
(region-beginning)))
;;;###autoload
(defun doom-region-end ()
"Return end position of selection.
Uses `evil-visual-end' if available."
(declare (side-effect-free t))
(if (bound-and-true-p evil-local-mode)
evil-visual-end
(region-end)))
;;;###autoload
(defun doom-thing-at-point-or-region (&optional thing prompt)
"Grab the current selection, THING at point, or xref identifier at point.
Returns THING if it is a string. Otherwise, if nothing is found at point and
PROMPT is non-nil, prompt for a string (if PROMPT is a string it'll be used as
the prompting string). Returns nil if all else fails.
NOTE: Don't use THING for grabbing symbol-at-point. The xref fallback is smarter
in some cases."
(declare (side-effect-free t))
(cond ((stringp thing)
thing)
((doom-region-active-p)
(buffer-substring-no-properties
(doom-region-beginning)
(doom-region-end)))
(thing
(thing-at-point thing t))
((require 'xref nil t)
;; Eglot, nox (a fork of eglot), and elpy implementations for
;; `xref-backend-identifier-at-point' betray the documented purpose of
;; the interface. Eglot/nox return a hardcoded string and elpy prepends
;; the line number to the symbol.
(if (memq (xref-find-backend) '(eglot elpy nox))
(thing-at-point 'symbol t)
;; A little smarter than using `symbol-at-point', though in most
;; cases, xref ends up using `symbol-at-point' anyway.
(xref-backend-identifier-at-point (xref-find-backend))))
(prompt
(read-string (if (stringp prompt) prompt "")))))
;;
;;; Commands
(defun doom--bol-bot-eot-eol (&optional pos)
(save-mark-and-excursion
(when pos
(goto-char pos))
(let* ((bol (if visual-line-mode
(save-excursion
(beginning-of-visual-line)
(point))
(line-beginning-position)))
(bot (save-excursion
(goto-char bol)
(skip-chars-forward " \t\r")
(point)))
(eol (if visual-line-mode
(save-excursion (end-of-visual-line) (point))
(line-end-position)))
(eot (or (save-excursion
(if (not comment-use-syntax)
(progn
(goto-char bol)
(when (re-search-forward comment-start-skip eol t)
(or (match-end 1) (match-beginning 0))))
(goto-char eol)
(while (and (doom-point-in-comment-p)
(> (point) bol))
(backward-char))
(skip-chars-backward " " bol)
(or (eq (char-after) 32)
(eolp)
(bolp)
(forward-char))
(point)))
eol)))
(list bol bot eot eol))))
(defvar doom--last-backward-pt nil)
;;;###autoload
(defun doom/backward-to-bol-or-indent (&optional point)
"Jump between the indentation column (first non-whitespace character) and the
beginning of the line. The opposite of
`doom/forward-to-last-non-comment-or-eol'."
(interactive "^d")
(let ((pt (or point (point))))
(cl-destructuring-bind (bol bot _eot _eol)
(doom--bol-bot-eot-eol pt)
(cond ((> pt bot)
(goto-char bot))
((= pt bol)
(or (and doom--last-backward-pt
(= (line-number-at-pos doom--last-backward-pt)
(line-number-at-pos pt)))
(setq doom--last-backward-pt nil))
(goto-char (or doom--last-backward-pt bot))
(setq doom--last-backward-pt nil))
((<= pt bot)
(setq doom--last-backward-pt pt)
(goto-char bol))))))
(defvar doom--last-forward-pt nil)
;;;###autoload
(defun doom/forward-to-last-non-comment-or-eol (&optional point)
"Jumps between the last non-blank, non-comment character in the line and the
true end of the line. The opposite of `doom/backward-to-bol-or-indent'."
(interactive "^d")
(let ((pt (or point (point))))
(cl-destructuring-bind (_bol _bot eot eol)
(doom--bol-bot-eot-eol pt)
(cond ((< pt eot)
(goto-char eot))
((= pt eol)
(goto-char (or doom--last-forward-pt eot))
(setq doom--last-forward-pt nil))
((>= pt eot)
(setq doom--last-backward-pt pt)
(goto-char eol))))))
;;;###autoload
(defun doom/backward-kill-to-bol-and-indent ()
"Kill line to the first non-blank character. If invoked again afterwards, kill
line to beginning of line. Same as `evil-delete-back-to-indentation'."
(interactive)
(let ((empty-line-p (save-excursion (beginning-of-line)
(looking-at-p "[ \t]*$"))))
(funcall (if (fboundp 'evil-delete)
#'evil-delete
#'delete-region)
(point-at-bol) (point))
(unless empty-line-p
(indent-according-to-mode))))
;;;###autoload
(defun doom/delete-backward-word (arg)
"Like `backward-kill-word', but doesn't affect the kill-ring."
(interactive "p")
(let (kill-ring)
(ignore-errors (backward-kill-word arg))))
;;;###autoload
(defun doom/dumb-indent ()
"Inserts a tab character (or spaces x tab-width)."
(interactive)
(if indent-tabs-mode
(insert "\t")
(let* ((movement (% (current-column) tab-width))
(spaces (if (= 0 movement) tab-width (- tab-width movement))))
(insert (make-string spaces ? )))))
;;;###autoload
(defun doom/dumb-dedent ()
"Dedents the current line."
(interactive)
(if indent-tabs-mode
(call-interactively #'backward-delete-char)
(unless (bolp)
(save-excursion
(when (> (current-column) (current-indentation))
(back-to-indentation))
(let ((movement (% (current-column) tab-width)))
(delete-char
(- (if (= 0 movement)
tab-width
(- tab-width movement)))))))))
;;;###autoload
(defun doom/retab (arg &optional beg end)
"Converts tabs-to-spaces or spaces-to-tabs within BEG and END (defaults to
buffer start and end, to make indentation consistent. Which it does depends on
the value of `indent-tab-mode'.
If ARG (universal argument) is non-nil, retab the current buffer using the
opposite indentation style."
(interactive "P\nr")
(unless (and beg end)
(setq beg (point-min)
end (point-max)))
(let ((indent-tabs-mode (if arg (not indent-tabs-mode) indent-tabs-mode)))
(if indent-tabs-mode
(tabify beg end)
(untabify beg end))))
;;;###autoload
(defun doom/delete-trailing-newlines ()
"Trim trailing newlines.
Respects `require-final-newline'."
(interactive)
(save-excursion
(goto-char (point-max))
(delete-blank-lines)))
;;;###autoload
(defun doom/dos2unix ()
"Convert the current buffer to a Unix file encoding."
(interactive)
(set-buffer-file-coding-system 'undecided-unix nil))
;;;###autoload
(defun doom/unix2dos ()
"Convert the current buffer to a DOS file encoding."
(interactive)
(set-buffer-file-coding-system 'undecided-dos nil))
;;;###autoload
(defun doom/toggle-indent-style ()
"Switch between tabs and spaces indentation style in the current buffer."
(interactive)
(setq indent-tabs-mode (not indent-tabs-mode))
(message "Indent style changed to %s" (if indent-tabs-mode "tabs" "spaces")))
(defvar editorconfig-lisp-use-default-indent)
;;;###autoload
(defun doom/set-indent-width (width)
"Change the indentation size to WIDTH of the current buffer.
The effectiveness of this command is significantly improved if you have
editorconfig or dtrt-indent installed."
(interactive
(list (if (integerp current-prefix-arg)
current-prefix-arg
(read-number "New indent size: "))))
(setq tab-width width)
(setq-local standard-indent width)
(when (boundp 'evil-shift-width)
(setq evil-shift-width width))
(cond ((require 'editorconfig nil t)
(let (editorconfig-lisp-use-default-indent)
(editorconfig-set-indentation nil width)))
((require 'dtrt-indent nil t)
(when-let (vars (nth 2 (assq major-mode dtrt-indent-hook-mapping-list)))
(dolist (var (doom-enlist vars))
(doom-log "Updated %s = %d" var width)
(set var width)))))
(message "Changed indentation to %d" width))
;;
;;; Hooks
;;;###autoload
(defun doom-enable-delete-trailing-whitespace-h ()
"Enables the automatic deletion of trailing whitespaces upon file save.
i.e. enables `ws-butler-mode' in the current buffer."
(ws-butler-mode +1))
;;;###autoload
(defun doom-disable-delete-trailing-whitespace-h ()
"Disables the automatic deletion of trailing whitespaces upon file save.
i.e. disables `ws-butler-mode' in the current buffer."
(ws-butler-mode -1))
;;;###autoload
(defun doom-enable-show-trailing-whitespace-h ()
"Enable `show-trailing-whitespace' in the current buffer."
(setq-local show-trailing-whitespace t))
;;;###autoload
(defun doom-disable-show-trailing-whitespace-h ()
"Disable `show-trailing-whitespace' in the current buffer."
(setq-local show-trailing-whitespace nil))

109
lisp/lib/themes.el Normal file
View file

@ -0,0 +1,109 @@
;;; lisp/lib/themes.el -*- lexical-binding: t; -*-
;;;###autoload
(defconst doom-customize-theme-hook nil)
(add-hook! 'doom-load-theme-hook
(defun doom-apply-customized-faces-h ()
"Run `doom-customize-theme-hook'."
(run-hooks 'doom-customize-theme-hook)))
(defun doom--custom-theme-set-face (spec)
(cond ((listp (car spec))
(cl-loop for face in (car spec)
collect
(car (doom--custom-theme-set-face (cons face (cdr spec))))))
((keywordp (cadr spec))
`((,(car spec) ((t ,(cdr spec))))))
(`((,(car spec) ,(cdr spec))))))
;;;###autoload
(defmacro custom-theme-set-faces! (theme &rest specs)
"Apply a list of face SPECS as user customizations for THEME.
THEME can be a single symbol or list thereof. If nil, apply these settings to
all themes. It will apply to all themes once they are loaded."
(declare (indent defun))
(let ((fn (gensym "doom--customize-themes-h-")))
`(progn
(defun ,fn ()
(let (custom--inhibit-theme-enable)
(dolist (theme (doom-enlist (or ,theme 'user)))
(when (or (eq theme 'user)
(custom-theme-enabled-p theme))
(apply #'custom-theme-set-faces theme
(mapcan #'doom--custom-theme-set-face
(list ,@specs)))))))
;; Apply the changes immediately if the user is using the default theme
;; or the theme has already loaded. This allows you to evaluate these
;; macros on the fly and customize your faces iteratively.
(when (or (get 'doom-theme 'previous-themes)
(null doom-theme))
(funcall #',fn))
;; FIXME Prevent clobbering this on-the-fly
(add-hook 'doom-customize-theme-hook #',fn 100))))
;;;###autoload
(defmacro custom-set-faces! (&rest specs)
"Apply a list of face SPECS as user customizations.
This is a convenience macro alternative to `custom-set-face' which allows for a
simplified face format, and takes care of load order issues, so you can use
doom-themes' API without worry."
(declare (indent defun))
`(custom-theme-set-faces! 'user ,@specs))
;;;###autoload
(defun doom/reload-theme ()
"Reload the current Emacs theme."
(interactive)
(unless doom-theme
(user-error "No theme is active"))
(let ((themes (copy-sequence custom-enabled-themes)))
(mapc #'disable-theme custom-enabled-themes)
(let (doom-load-theme-hook)
(mapc #'enable-theme (reverse themes)))
(doom-run-hooks 'doom-load-theme-hook)
(doom/reload-font)
(message "%s %s"
(propertize
(format "Reloaded %d theme%s:"
(length themes)
(if (cdr themes) "s" ""))
'face 'bold)
(mapconcat #'prin1-to-string themes ", "))))
;;
;;; Helpers
;;;###autoload
(defun doom-theme-face-attribute (theme face attribute &optional recursive)
"Read a FACE's ATTRIBUTE for a loaded THEME.
This is different from `face-attribute', which reads the attribute of an active
face for the current theme, but an active theme can change (or fail to load) in
non-interactive or frame-less sessions."
(let* ((spec
(cl-loop for (type f _ spec) in (get theme 'theme-settings)
if (and (eq type 'theme-face) (eq face f))
return spec))
(spec
(letf! ((defun window-system (_frame) 'x)
(defun display-color-cells (_frame) 257)
(defun frame-parameter (frame parameter)
(pcase parameter
(`display-type 'color)
(`background-mode 'dark)
(_ (funcall frame-parameter frame parameter))))
(#'display-supports-face-attributes-p #'always))
(face-spec-choose spec)))
(inherit (if recursive (plist-get spec :inherit)))
(value (if (plist-member spec attribute)
(plist-get spec attribute)
'unspecified)))
(when (and inherit (not (eq inherit 'unspecified)))
(letf! (defun face-attribute (face attribute &optional _frame inherit)
(doom-theme-face-attribute theme face attribute inherit))
(setq value (face-attribute-merged-with attribute value inherit))))
value))

246
lisp/lib/ui.el Normal file
View file

@ -0,0 +1,246 @@
;;; lisp/lib/ui.el -*- lexical-binding: t; -*-
;;
;;; Public library
;;;###autoload
(defun doom-resize-window (window new-size &optional horizontal force-p)
"Resize a window to NEW-SIZE. If HORIZONTAL, do it width-wise.
If FORCE-P is omitted when `window-size-fixed' is non-nil, resizing will fail."
(with-selected-window (or window (selected-window))
(let ((window-size-fixed (unless force-p window-size-fixed)))
(enlarge-window (- new-size (if horizontal (window-width) (window-height)))
horizontal))))
;;;###autoload
(defun doom-quit-p (&optional prompt)
"Prompt the user for confirmation when killing Emacs.
Returns t if it is safe to kill this session. Does not prompt if no real buffers
are open."
(or (not (ignore-errors (doom-real-buffer-list)))
(yes-or-no-p (format "%s" (or prompt "Really quit Emacs?")))
(ignore (message "Aborted"))))
;;
;;; Advice
;;;###autoload
(defun doom-recenter-a (&rest _)
"Generic advice for recentering window (typically :after other functions)."
(recenter))
;;;###autoload
(defun doom-preserve-window-position-a (fn &rest args)
"Generic advice for preserving cursor position on screen after scrolling."
(let ((row (cdr (posn-col-row (posn-at-point)))))
(prog1 (apply fn args)
(save-excursion
(let ((target-row (- (line-number-at-pos) row)))
(unless (< target-row 0)
(evil-scroll-line-to-top target-row)))))))
;;;###autoload
(defun doom-shut-up-a (fn &rest args)
"Generic advisor for silencing noisy functions.
In interactive Emacs, this just inhibits messages from appearing in the
minibuffer. They are still logged to *Messages*.
In tty Emacs, messages are suppressed completely."
(quiet! (apply fn args)))
;;
;;; Hooks
;;;###autoload
(defun doom-apply-ansi-color-to-compilation-buffer-h ()
"Applies ansi codes to the compilation buffers. Meant for
`compilation-filter-hook'."
(with-silent-modifications
(ansi-color-apply-on-region compilation-filter-start (point))))
;;;###autoload
(defun doom-disable-show-paren-mode-h ()
"Turn off `show-paren-mode' buffer-locally."
(setq-local show-paren-mode nil))
;;;###autoload
(defun doom-enable-line-numbers-h ()
(display-line-numbers-mode +1))
;;;###autoload
(defun doom-disable-line-numbers-h ()
(display-line-numbers-mode -1))
;;
;;; Commands
;;;###autoload
(defun doom/toggle-line-numbers ()
"Toggle line numbers.
Cycles through regular, relative and no line numbers. The order depends on what
`display-line-numbers-type' is set to. If you're using Emacs 26+, and
visual-line-mode is on, this skips relative and uses visual instead.
See `display-line-numbers' for what these values mean."
(interactive)
(defvar doom--line-number-style display-line-numbers-type)
(let* ((styles `(t ,(if visual-line-mode 'visual 'relative) nil))
(order (cons display-line-numbers-type (remq display-line-numbers-type styles)))
(queue (memq doom--line-number-style order))
(next (if (= (length queue) 1)
(car order)
(car (cdr queue)))))
(setq doom--line-number-style next)
(setq display-line-numbers next)
(message "Switched to %s line numbers"
(pcase next
(`t "normal")
(`nil "disabled")
(_ (symbol-name next))))))
;;;###autoload
(defun doom/delete-frame-with-prompt ()
"Delete the current frame, but ask for confirmation if it isn't empty."
(interactive)
(if (cdr (frame-list))
(when (doom-quit-p "Close frame?")
(delete-frame))
(save-buffers-kill-emacs)))
(defun doom--enlargened-forget-last-wconf-h ()
(set-frame-parameter nil 'doom--maximize-last-wconf nil)
(set-frame-parameter nil 'doom--enlargen-last-wconf nil)
(remove-hook 'doom-switch-window-hook #'doom--enlargened-forget-last-wconf-h))
;;;###autoload
(defun doom/window-maximize-buffer (&optional arg)
"Close other windows to focus on this one.
Use `winner-undo' to undo this. Alternatively, use `doom/window-enlargen'."
(interactive "P")
(when (and (bound-and-true-p +popup-mode)
(+popup-window-p))
(+popup/raise (selected-window)))
(delete-other-windows))
;;;###autoload
(defun doom/window-enlargen (&optional arg)
"Enlargen the current window (i.e. shrinks others) so you can focus on it.
Use `winner-undo' to undo this. Alternatively, use
`doom/window-maximize-buffer'."
(interactive "P")
(let* ((window (selected-window))
(dedicated-p (window-dedicated-p window))
(preserved-p (window-parameter window 'window-preserved-size))
(ignore-window-parameters t)
(window-resize-pixelwise nil)
(frame-resize-pixelwise nil))
(unwind-protect
(progn
(when dedicated-p
(set-window-dedicated-p window nil))
(when preserved-p
(set-window-parameter window 'window-preserved-size nil))
(maximize-window window))
(set-window-dedicated-p window dedicated-p)
(when preserved-p
(set-window-parameter window 'window-preserved-size preserved-p)))))
;;;###autoload
(defun doom/window-maximize-horizontally ()
"Delete all windows to the left and right of the current window."
(interactive)
(require 'windmove)
(save-excursion
(while (ignore-errors (windmove-left)) (delete-window))
(while (ignore-errors (windmove-right)) (delete-window))))
;;;###autoload
(defun doom/window-maximize-vertically ()
"Delete all windows above and below the current window."
(interactive)
(require 'windmove)
(save-excursion
(while (ignore-errors (windmove-up)) (delete-window))
(while (ignore-errors (windmove-down)) (delete-window))))
;;;###autoload
(defun doom/set-frame-opacity (opacity)
"Interactively change the current frame's opacity.
OPACITY is an integer between 0 to 100, inclusive."
(interactive
(list (read-number "Opacity (0-100): "
(or (frame-parameter nil 'alpha)
100))))
(set-frame-parameter nil 'alpha opacity))
(defvar doom--narrowed-base-buffer nil)
;;;###autoload
(defun doom/narrow-buffer-indirectly (beg end)
"Restrict editing in this buffer to the current region, indirectly.
This recursively creates indirect clones of the current buffer so that the
narrowing doesn't affect other windows displaying the same buffer. Call
`doom/widen-indirectly-narrowed-buffer' to undo it (incrementally).
Inspired from http://demonastery.org/2013/04/emacs-evil-narrow-region/"
(interactive
(list (or (bound-and-true-p evil-visual-beginning) (region-beginning))
(or (bound-and-true-p evil-visual-end) (region-end))))
(unless (region-active-p)
(setq beg (line-beginning-position)
end (line-end-position)))
(deactivate-mark)
(let ((orig-buffer (current-buffer)))
(with-current-buffer (switch-to-buffer (clone-indirect-buffer nil nil))
(narrow-to-region beg end)
(setq-local doom--narrowed-base-buffer orig-buffer))))
;;;###autoload
(defun doom/widen-indirectly-narrowed-buffer (&optional arg)
"Widens narrowed buffers.
This command will incrementally kill indirect buffers (under the assumption they
were created by `doom/narrow-buffer-indirectly') and switch to their base
buffer.
If ARG, then kill all indirect buffers, return the base buffer and widen it.
If the current buffer is not an indirect buffer, it is `widen'ed."
(interactive "P")
(unless (buffer-narrowed-p)
(user-error "Buffer isn't narrowed"))
(let ((orig-buffer (current-buffer))
(base-buffer doom--narrowed-base-buffer))
(cond ((or (not base-buffer)
(not (buffer-live-p base-buffer)))
(widen))
(arg
(let ((buffer orig-buffer)
(buffers-to-kill (list orig-buffer)))
(while (setq buffer (buffer-local-value 'doom--narrowed-base-buffer buffer))
(push buffer buffers-to-kill))
(switch-to-buffer (buffer-base-buffer))
(mapc #'kill-buffer (remove (current-buffer) buffers-to-kill))))
((switch-to-buffer base-buffer)
(kill-buffer orig-buffer)))))
;;;###autoload
(defun doom/toggle-narrow-buffer (beg end)
"Narrow the buffer to BEG END. If narrowed, widen it."
(interactive
(list (or (bound-and-true-p evil-visual-beginning) (region-beginning))
(or (bound-and-true-p evil-visual-end) (region-end))))
(if (buffer-narrowed-p)
(widen)
(unless (region-active-p)
(setq beg (line-beginning-position)
end (line-end-position)))
(narrow-to-region beg end)))