doomemacs/modules/lang/emacs-lisp/config.el
Henrik Lissner 22d5e3059f
feat(emacs-lisp): extend config to lisp-{data,interaction}-mode
So these modes (and particularly .doom* dotfiles and .dir-locals.el) can
benefit from smarter syntax highlighting and integration with other
modules.
2024-09-17 05:56:44 -04:00

342 lines
14 KiB
EmacsLisp

;;; lang/emacs-lisp/config.el -*- lexical-binding: t; -*-
(defvar +emacs-lisp-enable-extra-fontification t
"If non-nil, highlight special forms, and defined functions and variables.")
(defvar +emacs-lisp-outline-regexp "[ \t]*;;;\\(;*\\**\\) [^ \t\n]"
"Regexp to use for `outline-regexp' in `emacs-lisp-mode'.
This marks a foldable marker for `outline-minor-mode' in elisp buffers.")
(defvar +emacs-lisp-linter-warnings
'(not free-vars ; don't complain about unknown variables
noruntime ; don't complain about unknown function calls
unresolved) ; don't complain about undefined functions
"The value for `byte-compile-warnings' in non-packages.
This reduces the verbosity of flycheck in Emacs configs and scripts, which are
so stateful that the deluge of false positives (from the byte-compiler,
package-lint, and checkdoc) can be more overwhelming than helpful.
See `+emacs-lisp-non-package-mode' for details.")
;; `elisp-mode' is loaded at startup. In order to lazy load its config we need
;; to pretend it isn't loaded
(defer-feature! elisp-mode emacs-lisp-mode)
;;
;;; Config
(use-package! elisp-mode
:mode ("\\.Cask\\'" . emacs-lisp-mode)
:interpreter ("doomscript" . emacs-lisp-mode)
:config
(let ((modes '(emacs-lisp-mode lisp-interaction-mode lisp-data-mode)))
(set-repl-handler! modes #'+emacs-lisp/open-repl)
(set-eval-handler! modes #'+emacs-lisp-eval)
(set-lookup-handlers! `(,@modes helpful-mode)
:definition #'+emacs-lisp-lookup-definition
:documentation #'+emacs-lisp-lookup-documentation)
(set-docsets! modes "Emacs Lisp")
(set-ligatures! modes :lambda "lambda")
(set-formatter! 'lisp-indent #'apheleia-indent-lisp-buffer :modes modes)
(set-rotate-patterns! modes
:symbols '(("t" "nil")
("let" "let*")
("when" "unless")
("advice-add" "advice-remove")
("defadvice!" "undefadvice!")
("add-hook" "remove-hook")
("add-hook!" "remove-hook!")
("it" "xit")
("describe" "xdescribe"))))
(setq-hook! 'emacs-lisp-mode-hook
;; Emacs' built-in elisp files use a hybrid tab->space indentation scheme
;; with a tab width of 8. Any smaller and the indentation will be
;; unreadable. Since Emacs' lisp indenter doesn't respect this variable it's
;; safe to ignore this setting otherwise.
tab-width 8
;; Don't treat autoloads or sexp openers as outline headers, we have
;; hideshow for that.
outline-regexp +emacs-lisp-outline-regexp
outline-level #'+emacs-lisp-outline-level)
;; DEPRECATED: Remove when 27.x support is dropped.
(when (< emacs-major-version 28)
;; As of Emacs 28+, `emacs-lisp-mode' uses a shorter label in the mode-line
;; ("ELisp/X", where X = l or d, depending on `lexical-binding'). In <=27,
;; it uses "Emacs-Lisp". The former is more useful, so I backport it:
(setq-hook! 'emacs-lisp-mode-hook
mode-name `("ELisp"
(lexical-binding (:propertize "/l"
help-echo "Using lexical-binding mode")
(:propertize "/d"
help-echo "Using old dynamic scoping mode"
face warning
mouse-face mode-line-highlight)))))
;; Introduces logic to improve plist indentation in emacs-lisp-mode.
(advice-add #'calculate-lisp-indent :override #'+emacs-lisp--calculate-lisp-indent-a)
;; Variable-width indentation is superior in elisp. Otherwise, `dtrt-indent'
;; and `editorconfig' would force fixed indentation on elisp.
(add-to-list 'doom-detect-indentation-excluded-modes 'emacs-lisp-mode)
(add-hook! '(emacs-lisp-mode-hook lisp-data-mode-local-vars-hook)
;; Allow folding of outlines in comments
#'outline-minor-mode
;; Make parenthesis depth easier to distinguish at a glance
#'rainbow-delimiters-mode
;; Make quoted symbols easier to distinguish from free variables
#'highlight-quoted-mode
;; Extend imenu support to Doom constructs
#'+emacs-lisp-extend-imenu-h
;; Ensure straight sees modifications to installed packages
#'+emacs-lisp-init-straight-maybe-h)
;; UX: Both Flycheck's and Flymake's two emacs-lisp checkers produce a *lot*
;; of false positives in non-packages (like Emacs configs or elisp scripts),
;; so I disable `checkdoc' (`emacs-lisp-checkdoc', `elisp-flymake-checkdoc')
;; and set `byte-compile-warnings' to a subset that makes more sense (see
;; `+emacs-lisp-linter-warnings')
(add-hook! '(flycheck-mode-hook flymake-mode-hook) #'+emacs-lisp-non-package-mode)
(defadvice! +syntax--fix-elisp-flymake-load-path (orig-fn &rest args)
"Set load path for elisp byte compilation Flymake backend"
:around #'elisp-flymake-byte-compile
(let ((elisp-flymake-byte-compile-load-path
(append elisp-flymake-byte-compile-load-path load-path)))
(apply orig-fn args)))
;; Enhance elisp syntax highlighting, by highlighting Doom-specific
;; constructs, defined symbols, and truncating :pin's in `package!' calls.
(font-lock-add-keywords
'emacs-lisp-mode
(append `(;; custom Doom cookies
("^;;;###\\(autodef\\|if\\|package\\)[ \n]" (1 font-lock-warning-face t)))
;; highlight defined, special variables & functions
(when +emacs-lisp-enable-extra-fontification
`((+emacs-lisp-highlight-vars-and-faces . +emacs-lisp--face)))))
(defadvice! +emacs-lisp-append-value-to-eldoc-a (fn sym)
"Display variable value next to documentation in eldoc."
:around #'elisp-get-var-docstring
(when-let (ret (funcall fn sym))
(if (boundp sym)
(concat ret " "
(let* ((truncated " [...]")
(print-escape-newlines t)
(str (symbol-value sym))
(str (prin1-to-string str))
(limit (- (frame-width) (length ret) (length truncated) 1)))
(format (format "%%0.%ds%%s" (max limit 0))
(propertize str 'face 'warning)
(if (< (length str) limit) "" truncated))))
ret)))
(map! :localleader
:map (emacs-lisp-mode-map lisp-interaction-mode-map)
:desc "Expand macro" "m" #'macrostep-expand
(:prefix ("d" . "debug")
"f" #'+emacs-lisp/edebug-instrument-defun-on
"F" #'+emacs-lisp/edebug-instrument-defun-off)
(:prefix ("e" . "eval")
"b" #'eval-buffer
"d" #'eval-defun
"e" #'eval-last-sexp
"r" #'eval-region
"l" #'load-library)
(:prefix ("g" . "goto")
"f" #'find-function
"v" #'find-variable
"l" #'find-library)))
(use-package! ielm
:defer t
:config
(set-lookup-handlers! 'inferior-emacs-lisp-mode
:definition #'+emacs-lisp-lookup-definition
:documentation #'+emacs-lisp-lookup-documentation)
;; Adapted from http://www.modernemacs.com/post/comint-highlighting/ to add
;; syntax highlighting to ielm REPLs.
(setq ielm-font-lock-keywords
(append '(("\\(^\\*\\*\\*[^*]+\\*\\*\\*\\)\\(.*$\\)"
(1 font-lock-comment-face)
(2 font-lock-constant-face)))
(when (require 'highlight-numbers nil t)
(highlight-numbers--get-regexp-for-mode 'emacs-lisp-mode))
(cl-loop for (matcher . match-highlights)
in (append lisp-el-font-lock-keywords-2
lisp-cl-font-lock-keywords-2)
collect
`((lambda (limit)
(when ,(if (symbolp matcher)
`(,matcher limit)
`(re-search-forward ,matcher limit t))
;; Only highlight matches after the prompt
(> (match-beginning 0) (car comint-last-prompt))
;; Make sure we're not in a comment or string
(let ((state (syntax-ppss)))
(not (or (nth 3 state)
(nth 4 state))))))
,@match-highlights)))))
;;
;;; Packages
;;;###package overseer
(autoload 'overseer-test "overseer" nil t)
;; Properly lazy load overseer by not loading it so early:
(remove-hook 'emacs-lisp-mode-hook #'overseer-enable-mode)
(use-package! flycheck-cask
:when (and (modulep! :checkers syntax)
(not (modulep! :checkers syntax +flymake)))
:defer t
:init
(add-hook! 'emacs-lisp-mode-hook
(add-hook 'flycheck-mode-hook #'flycheck-cask-setup nil t)))
(use-package! flycheck-package
:when (and (modulep! :checkers syntax)
(not (modulep! :checkers syntax +flymake)))
:after flycheck
:config (flycheck-package-setup))
(use-package! elisp-demos
:defer t
:init
(advice-add #'describe-function-1 :after #'elisp-demos-advice-describe-function-1)
(advice-add #'helpful-update :after #'elisp-demos-advice-helpful-update)
:config
;; Add Doom's core and module demo files, so additional demos can be specified
;; by end-users (in $DOOMDIR/demos.org), by modules (modules/X/Y/demos.org),
;; or Doom's core (lisp/demos.org).
(dolist (file (doom-module-locate-paths (doom-module-list) "demos.org"))
(add-to-list 'elisp-demos-user-files file))
;; HACK: These functions open Org files non-interactively without any
;; performance optimizations. Given how prone org-mode is to being tied to
;; expensive functionality, this will often introduce unexpected freezes
;; without this advice.
;; TODO: PR upstream?
(defvar org-inhibit-startup)
(defvar org-mode-hook)
(defadvice! +emacs-lisp--optimize-org-init-a (fn &rest args)
"Disable unrelated functionality to optimize calls to `org-mode'."
:around #'elisp-demos--export-json-file
:around #'elisp-demos--symbols
:around #'elisp-demos--syntax-highlight
(let ((org-inhibit-startup t)
(doom-inhibit-local-var-hooks t)
enable-dir-local-variables
org-mode-hook)
(apply fn args))))
(use-package! buttercup
:defer t
:minor ("/test[/-].+\\.el$" . buttercup-minor-mode)
:preface
;; buttercup.el doesn't define a keymap for `buttercup-minor-mode', as we have
;; to fool its internal `define-minor-mode' call into thinking one exists, so
;; it will associate it with the mode.
(defvar buttercup-minor-mode-map (make-sparse-keymap))
:config
(set-popup-rule! "^\\*Buttercup\\*$" :size 0.45 :select nil :ttl 0)
(set-yas-minor-mode! 'buttercup-minor-mode)
(when (featurep 'evil)
(add-hook 'buttercup-minor-mode-hook #'evil-normalize-keymaps))
(map! :localleader
:map buttercup-minor-mode-map
:prefix "t"
"t" #'+emacs-lisp/buttercup-run-file
"a" #'+emacs-lisp/buttercup-run-project
"s" #'buttercup-run-at-point))
(use-package! helpful
;; a better *help* buffer
:commands helpful--read-symbol
:hook (helpful-mode . visual-line-mode)
:init
;; Make `apropos' et co search more extensively. They're more useful this way.
(setq apropos-do-all t)
(global-set-key [remap describe-function] #'helpful-callable)
(global-set-key [remap describe-command] #'helpful-command)
(global-set-key [remap describe-variable] #'helpful-variable)
(global-set-key [remap describe-key] #'helpful-key)
;; (global-set-key [remap describe-symbol] #'helpful-symbol)
(defun doom-use-helpful-a (fn &rest args)
"Force FN to use helpful instead of the old describe-* commands."
(letf! ((#'describe-function #'helpful-function)
(#'describe-variable #'helpful-variable))
(apply fn args)))
(after! apropos
;; patch apropos buttons to call helpful instead of help
(dolist (fun-bt '(apropos-function apropos-macro apropos-command))
(button-type-put
fun-bt 'action
(lambda (button)
(helpful-callable (button-get button 'apropos-symbol)))))
(dolist (var-bt '(apropos-variable apropos-user-option))
(button-type-put
var-bt 'action
(lambda (button)
(helpful-variable (button-get button 'apropos-symbol))))))
;; DEPRECATED: Remove when support for 29 is dropped.
(when (= emacs-major-version 29)
(defadvice! doom--find-function-search-for-symbol-save-excursion-a (fn &rest args)
"Suppress cursor movement by `find-function-search-for-symbol'.
Addresses an unwanted side-effect in `find-function-search-for-symbol' on Emacs
29 where the cursor is moved to a variable's definition if it's defined in the
current buffer."
:around #'find-function-search-for-symbol
(let (buf pos)
(letf! (defun find-library-name (library)
(let ((filename (funcall find-library-name library)))
(with-current-buffer (find-file-noselect filename)
(setq buf (current-buffer)
pos (point)))
filename))
(prog1 (apply fn args)
(when (buffer-live-p buf)
(with-current-buffer buf (goto-char pos))))))))
:config
(setq helpful-set-variable-function #'setq!)
(cond ((modulep! :completion ivy)
(setq counsel-describe-function-function #'helpful-callable
counsel-describe-variable-function #'helpful-variable
counsel-descbinds-function #'helpful-callable))
((modulep! :completion helm)
(dolist (fn '(helm-describe-variable helm-describe-function))
(advice-add fn :around #'doom-use-helpful-a))))
;; Open help:* links with helpful-* instead of describe-*
(advice-add #'org-link--open-help :around #'doom-use-helpful-a)
(map! :map helpful-mode-map
:ng "o" #'link-hint-open-link
:n "gr" #'helpful-update))
;;
;;; Project modes
(def-project-mode! +emacs-lisp-ert-mode
:modes '(emacs-lisp-mode)
:match "/test[/-].+\\.el$"
:add-hooks '(overseer-enable-mode))