tvl-depot/users/aspen/emacs/config.org
Aspen Smith f566f15dd9 chore(aspen/emacs): Scrub all result blocks
These are noisy, but more importantly I'm worried I might accidentally
commit a secret at some point

Change-Id: If6f2c358f2803af25ea27ef34d39c7f2108d4186
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11299
Reviewed-by: aspen <root@gws.fyi>
Autosubmit: aspen <root@gws.fyi>
Tested-by: BuildkiteCI
2024-03-31 17:56:51 +00:00

42 KiB

Emacs Config

;; -*- lexical-binding: t; -*-

Utils

(use-package! dash)

Elisp extras

(defmacro comment (&rest _body)
  "Comment out one or more s-expressions"
  nil)

(defun inc (x) "Returns x + 1" (+ 1 x))
(defun dec (x) "Returns x - 1" (- x 1))

(defun average (ns)
  "Arithmetic mean of xs"
  (if (null ns) nil
    (/ (apply #'+ ns)
       (length ns))))

(defun alist-set (alist-symbol key value)
  "Set VALUE of a KEY in ALIST-SYMBOL."
  (set alist-symbol (cons (list key value) (assq-delete-all key (eval alist-symbol)))))

(defun rx-words (&rest words)
  (rx-to-string
   `(and symbol-start (group (or ,@words)) symbol-end)))
(average (list 1 2 3 4))

Text editing utils

Reading strings

(defun get-char (&optional point)
  "Get the character at the given `point' (defaulting to the current point),
without properties"
  (let ((point (or point (point))))
    (buffer-substring-no-properties point (+ 1 point))))

(defun get-line (&optional lineno)
  "Read the line number `lineno', or the current line if `lineno' is nil, and
return it as a string stripped of all text properties"
  (let ((current-line (line-number-at-pos)))
    (if (or (not lineno)
            (= current-line lineno))
        (thing-at-point 'line t)
      (save-mark-and-excursion
       (line-move (- lineno (line-number-at-pos)))
       (thing-at-point 'line t)))))

(defun get-line-point ()
  "Get the position in the current line of the point"
  (- (point) (line-beginning-position)))

;; Moving in the file

(defun goto-line-char (pt)
  "Moves the point to the given position expressed as an offset from the start
of the line"
  (goto-char (+ (line-beginning-position) pt)))

(defun goto-eol ()
  "Moves to the end of the current line"
  (goto-char (line-end-position)))

(defun goto-regex-on-line (regex)
  "Moves the point to the first occurrence of `regex' on the current line.
Returns nil if the regex did not match, non-nil otherwise"
  (when-let ((current-line (get-line))
             (line-char (string-match regex current-line)))
    (goto-line-char line-char)))

(defun goto-regex-on-line-r (regex)
  "Moves the point to the *last* occurrence of `regex' on the current line.
Returns nil if the regex did not match, non-nil otherwise"
  (when-let ((current-line (get-line))
             (modified-regex (concat ".*\\(" regex "\\)"))
             (_ (string-match modified-regex current-line))
             (match-start (match-beginning 1)))
    (goto-line-char match-start)))
(progn
  (string-match (rx (and (zero-or-more anything)
                         (group "foo" "foo")))
                "foofoofoo")
  (match-beginning 1))

Changing file contents

(defmacro saving-excursion (&rest body)
  `(λ! () (save-excursion ,@body)))

(defun delete-line ()
  "Remove the line at the current point"
  (delete-region (line-beginning-position)
                 (inc (line-end-position))))

(defmacro modify-then-indent (&rest body)
  "Modify text in the buffer according to body, then re-indent from where the
  cursor started to where the cursor ended up, then return the cursor to where
  it started."
  `(let ((beg (line-beginning-position))
         (orig-line-char (- (point) (line-beginning-position))))
     (atomic-change-group
       (save-mark-and-excursion
        ,@body
        (evil-indent beg (+ (line-end-position) 1))))
     (goto-line-char orig-line-char)))

(pcase-defmacro s-starts-with (prefix)
  `(pred (s-starts-with-p ,prefix)))

(pcase-defmacro s-contains (needle &optional ignore-case)
  `(pred (s-contains-p ,needle
                       ,@(when ignore-case (list ignore-case)))))
(pcase "foo"
  ((s-contains "bar") 1)
  ((s-contains "o") 2))

Evil utils

(defmacro define-move-and-insert
    (name &rest body)
  `(defun ,name (count &optional vcount skip-empty-lines)
     ;; Following interactive form taken from the source for `evil-insert'
     (interactive
      (list (prefix-numeric-value current-prefix-arg)
            (and (evil-visual-state-p)
                 (memq (evil-visual-type) '(line block))
                 (save-excursion
                   (let ((m (mark)))
                     ;; go to upper-left corner temporarily so
                     ;; `count-lines' yields accurate results
                     (evil-visual-rotate 'upper-left)
                     (prog1 (count-lines evil-visual-beginning evil-visual-end)
                       (set-mark m)))))
            (evil-visual-state-p)))
     (atomic-change-group
       ,@body
       (evil-insert count vcount skip-empty-lines))))

Name and email

(setq user-full-name "Aspen Smith"
      user-mail-address "root@gws.fyi")

Visual style

(let ((font-family (pcase system-type
                     ('darwin "MesloLGSDZ NF")
                     ('gnu/linux "Meslo LGSDZ Nerd Font"))))
  (setq doom-font (font-spec :family font-family :height 113)
        doom-big-font (font-spec :family font-family :size 24)
        doom-big-font-increment 5
        doom-variable-pitch-font (font-spec :family font-family)
        doom-theme 'doom-solarized-light))

(setq display-line-numbers-type t)

(setq doom-modeline-buffer-file-name-style 'relative-to-project
      doom-modeline-modal-icon nil
      doom-modeline-github t
      doom-modeline-height 12)
(setq whitespace-style '(face lines-tail))
(global-whitespace-mode t)
(add-hook 'org-mode-hook (lambda () (whitespace-mode -1)) t)

Theme

LAB colour gradient picker is a good tool for trying to find "halfway points" between two colours

Variables

(rainbow-mode)
(setq +solarized-s-base03    "#002b36"
      +solarized-s-base02    "#073642"
      ;; emphasized content
      +solarized-s-base01    "#586e75"
      ;; primary content
      +solarized-s-base00    "#657b83"
      +solarized-s-base0     "#839496"
      ;; comments
      +solarized-s-base1     "#93a1a1"
      ;; background highlight light
      +solarized-s-base2     "#eee8d5"
      ;; background light
      +solarized-s-base3     "#fdf6e3"

      +solarized-halfway-highlight "#f5efdc"

      ;; Solarized accented colors
      +solarized-yellow    "#b58900"
      +solarized-orange    "#cb4b16"
      +solarized-red       "#dc322f"
      +solarized-magenta   "#d33682"
      +solarized-violet    "#6c71c4"
      +solarized-blue      "#268bd2"
      +solarized-cyan      "#2aa198"
      +solarized-green     "#859900"

      ;; Darker and lighter accented colors
      ;; Only use these in exceptional circumstances!
      +solarized-yellow-d  "#7B6000"
      +solarized-yellow-l  "#DEB542"
      +solarized-orange-d  "#8B2C02"
      +solarized-orange-l  "#F2804F"
      +solarized-red-d     "#990A1B"
      +solarized-red-l     "#FF6E64"
      +solarized-magenta-d "#93115C"
      +solarized-magenta-l "#F771AC"
      +solarized-violet-d  "#3F4D91"
      +solarized-violet-l  "#9EA0E5"
      +solarized-blue-d    "#00629D"
      +solarized-blue-l    "#69B7F0"
      +solarized-cyan-d    "#00736F"
      +solarized-cyan-l    "#69CABF"
      +solarized-green-d   "#546E00"
      +solarized-green-l   "#B4C342")

Overrides

(custom-set-faces!
  `(cursor :background ,+solarized-s-base00)
  `(font-lock-doc-face :foreground ,+solarized-s-base1)
  `(font-lock-preprocessor-face :foreground ,+solarized-red :bold nil)
  `(font-lock-keyword-face :foreground ,+solarized-green :bold nil)
  `(font-lock-builtin-face :foreground ,+solarized-s-base01 :bold t)
  `(font-lock-function-name-face :foreground ,+solarized-blue)
  `(font-lock-constant-face :foreground ,+solarized-blue)
  `(font-lock-type-face :italic nil)
  `(highlight-numbers-number :bold nil)
  `(highlight :background ,+solarized-s-base2)
  `(solaire-hl-line-face :background ,+solarized-halfway-highlight)
  `(hl-line :background ,+solarized-s-base2)

  `(linum :background ,+solarized-s-base2 :foreground ,+solarized-s-base1)
  `(line-number :background ,+solarized-s-base2 :foreground ,+solarized-s-base1)
  `(line-number-current-line :background ,+solarized-s-base2 :foreground ,+solarized-s-base1)
  `(fringe :background ,+solarized-s-base2)

  `(whitespace-line :foreground ,+solarized-red :underline t)

  `(haskell-operator-face :foreground ,+solarized-green)
  `(haskell-keyword-face :foreground ,+solarized-cyan)

  `(magit-branch-local :foreground ,+solarized-blue :bold t)
  `(magit-branch-remote :foreground ,+solarized-green :bold t)
  `(magit-branch-remote-head :foreground ,+solarized-green :bold t :box t)
  `(magit-branch-current :box t :bold t)
  `(magit-header-line :background nil :foreground ,+solarized-yellow :bold t :box nil)
  `(diff-refine-added :foreground "#dbdb9c" :background "#5b6e35" :bold nil)
  `(magit-diff-added-highlight :foreground "#657827" :background "#efeac7" :bold nil)
  `(diff-refine-removed :background "#8e433d" :foreground "#ffb9a1" :bold nil)
  `(magit-diff-removed-highlight :foreground "#a33c35" :background "#ffdec8" :bold nil)
  `(magit-diff-hunk-heading :background "#f8e8c6" :foreground "#876d26" :bold nil)
  `(magit-diff-hunk-heading-highlight :background "#f1d49b" :foreground "#766634" :bold nil)
  `(magit-section-heading :foreground "#b58900")
  `(magit-filename :foreground ,+solarized-s-base00)
  `(magit-diff-context-highlight :background ,+solarized-halfway-highlight)

  `(transient-delimiter :foreground ,+solarized-s-base1)
  `(transient-inapt-suffix :foreground ,+solarized-s-base1)
  `(transient-inactive-value :foreground ,+solarized-s-base1)
  `(transient-inactive-argument :foreground ,+solarized-s-base1)
  `(transient-key-exit :foreground ,+solarized-green :bold t)
  `(transient-key-stay :foreground ,+solarized-blue :bold t)
  )

Keybindings and navigation

Get the hell out of here, snipe!

(remove-hook 'doom-first-input-hook #'evil-snipe-mode)
(map!
 (:leader
  "b" #'consult-buffer
  "r" #'consult-recent-file))

Flycheck

(evil-set-command-property 'flycheck-next-error :repeat nil)
(evil-set-command-property 'flycheck-prev-error :repeat nil)
(evil-set-command-property 'flycheck-previous-error :repeat nil)

(map!
 (:map flycheck-mode-map
  :m  "]e" #'flycheck-next-error
  :m  "[e" #'flycheck-previous-error))

Smerge

(evil-set-command-property 'smerge-next :repeat nil)
(evil-set-command-property 'smerge-prev :repeat nil)

(map!
 :n "] n" #'smerge-next
 :n "[ n" #'smerge-prev
 (:leader
  (:desc "smerge" :prefix "g m"
   :desc "Keep Current" :n "SPC" #'smerge-keep-current
   :desc "Keep All"     :n "a" #'smerge-keep-all
   :desc "Keep Upper"   :n "u" #'smerge-keep-upper
   :desc "Keep Lower"   :n "l" #'smerge-keep-lower)))
t

Vinegar-style dired

(defun dired-mode-p () (eq 'dired-mode major-mode))

(defun aspen/dired-minus ()
  (interactive)
  (if (dired-mode-p)
      (dired-up-directory)
    (when buffer-file-name
      (-> (buffer-file-name)
          (f-dirname)
          (dired)))))

(map!
 :n "-" #'aspen/dired-minus
 (:map dired-mode-map
       "-" #'aspen/dired-minus))

Lisp mappings

Use paxedit

(use-package! paxedit
  :hook ((emacs-lisp-mode . paxedit-mode)
         (clojure-mode . paxedit-mode)
         (common-lisp-mode . paxedit-mode)))

Paxedit functions

(define-move-and-insert aspen/insert-at-sexp-end
  (when (not (equal (get-char) "("))
    (backward-up-list))
  (forward-sexp)
  (backward-char))

(define-move-and-insert aspen/insert-at-sexp-start
  (backward-up-list)
  (forward-char))

(define-move-and-insert aspen/insert-at-form-start
  (backward-sexp)
  (backward-char)
  (insert " "))

(define-move-and-insert aspen/insert-at-form-end
  (forward-sexp)
  (insert " "))

(defun aspen/paxedit-kill (&optional n)
  (interactive "p")
  (or (paxedit-comment-kill)
      (when (paxedit-symbol-cursor-within?)
        (paxedit-symbol-kill))
      (paxedit-implicit-sexp-kill n)
      (paxedit-sexp-kill n)
      (message paxedit-message-kill)))

Paxedit mappings

(map!
 (:after paxedit
         (:map paxedit-mode-map
          :i ";"                          #'paxedit-insert-semicolon
          :i "("                          #'paxedit-open-round
          :i "["                          #'paxedit-open-bracket
          :i "{"                          #'paxedit-open-curly
          :n [remap evil-yank-line]       #'paxedit-copy
          :n [remap evil-delete-line]     #'aspen/paxedit-kill
          :n "g o"                        #'paxedit-sexp-raise
          :n [remap evil-join-whitespace] #'paxedit-compress
          :n "g S"                        #'paxedit-format-1
          :n "g k"                        #'paxedit-backward-up
          :n "g j"                        #'paxedit-backward-end)))

(require 'general)
(general-evil-setup t)

(nmap
  ">" (general-key-dispatch 'evil-shift-right
        "e" 'paxedit-transpose-forward
        ")" 'sp-forward-slurp-sexp
        "(" 'sp-backward-barf-sexp
        "I" 'aspen/insert-at-sexp-end
        ;; "a" 'grfn/insert-at-form-end
        ))

(nmap
  "<" (general-key-dispatch 'evil-shift-left
        "e" 'paxedit-transpose-backward
        ")" 'sp-forward-barf-sexp
        "(" 'sp-backward-slurp-sexp
        "I" 'aspen/insert-at-sexp-start
        ;; "a" 'grfn/insert-at-form-start
        ))

Eval functions

(use-package! predd)

(predd-defmulti eval-sexp (lambda (form) major-mode))

(predd-defmethod eval-sexp 'clojure-mode (form)
  (cider-interactive-eval form))

(predd-defmethod eval-sexp 'emacs-lisp-mode (form)
  (pp-eval-expression form))

(predd-defmulti eval-sexp-region (lambda (_beg _end) major-mode))

(predd-defmethod eval-sexp-region 'clojure-mode (beg end)
  (cider-interactive-eval nil nil (list beg end)))

(predd-defmethod eval-sexp-region 'emacs-lisp-mode (beg end)
  (pp-eval-expression (read (buffer-substring beg end))))

(predd-defmulti eval-sexp-region-context (lambda (_beg _end _context) major-mode))

(predd-defmethod eval-sexp-region-context 'clojure-mode (beg end context)
  (cider--eval-in-context (buffer-substring beg end)))

(defun pp-eval-context-region (beg end context)
  (interactive "r\nxContext: ")
  (let* ((inner-expr (read (buffer-substring beg end)))
         (full-expr (list 'let* context inner-expr)))
    (pp-eval-expression full-expr)))

(predd-defmethod eval-sexp-region-context 'emacs-lisp-mode (beg end context)
  (pp-eval-context-region beg end context))

(predd-defmulti preceding-sexp (lambda () major-mode))

(predd-defmethod preceding-sexp 'clojure-mode ()
  (cider-last-sexp))

(predd-defmethod preceding-sexp 'emacs-lisp-mode ()
  (elisp--preceding-sexp))

(defun eval-sexp-at-point ()
  (interactive)
  (let ((bounds (bounds-of-thing-at-point 'sexp)))
    (eval-sexp-region (car bounds)
                      (cdr bounds))))

(defun eval-last-sexp (_)
  (interactive)
  (eval-sexp (preceding-sexp)))

;;;

(defun cider-insert-current-sexp-in-repl (&optional arg)
  "Insert the expression at point in the REPL buffer.
If invoked with a prefix ARG eval the expression after inserting it"
  (interactive "P")
  (cider-insert-in-repl (cider-sexp-at-point) arg))

(evil-define-operator fireplace-send (beg end)
  (cider-insert-current-sexp-in-repl nil nil (list beg end)))

(defun +clojure-pprint-expr (form)
  (format "(with-out-str (clojure.pprint/pprint %s))"
          form))

(defun cider-eval-read-and-print-handler (&optional buffer)
  "Make a handler for evaluating and reading then printing result in BUFFER."
  (nrepl-make-response-handler
   (or buffer (current-buffer))
   (lambda (buffer value)
     (let ((value* (read value)))
       (with-current-buffer buffer
         (insert
          (if (derived-mode-p 'cider-clojure-interaction-mode)
              (format "\n%s\n" value*)
            value*)))))
   (lambda (_buffer out) (cider-emit-interactive-eval-output out))
   (lambda (_buffer err) (cider-emit-interactive-eval-err-output err))
   '()))

(defun cider-eval-and-replace (beg end)
  "Evaluate the expression in region and replace it with its result"
  (interactive "r")
  (let ((form (buffer-substring beg end)))
    (cider-nrepl-sync-request:eval form)
    (kill-region beg end)
    (cider-interactive-eval
     (+clojure-pprint-expr form)
     (cider-eval-read-and-print-handler))))

(defun cider-eval-current-sexp-and-replace ()
  "Evaluate the expression at point and replace it with its result"
  (interactive)
  (apply #'cider-eval-and-replace (cider-sexp-at-point 'bounds)))

;;;

Eval bindings

fireplace-esque eval binding

(evil-define-operator fireplace-eval (beg end)
  (eval-sexp-region beg end))

(evil-define-operator fireplace-replace (beg end)
  (cider-eval-and-replace beg end))

(evil-define-operator fireplace-eval-context (beg end)
  (eval-sexp-region-context beg end))

(nmap :keymaps 'cider-mode-map
  "c" (general-key-dispatch 'evil-change
        "p" (general-key-dispatch 'fireplace-eval
              "p" 'cider-eval-sexp-at-point
              "c" 'cider-eval-last-sexp
              "d" 'cider-eval-defun-at-point
              "r" 'cider-test-run-test)
        "q" (general-key-dispatch 'fireplace-send
              "q" 'cider-insert-current-sexp-in-repl
              "c" 'cider-insert-last-sexp-in-repl)
        "x" (general-key-dispatch 'fireplace-eval-context
              "x" 'cider-eval-sexp-at-point-in-context
              "c" 'cider-eval-last-sexp-in-context)
        "!" (general-key-dispatch 'fireplace-replace
              "!" 'cider-eval-current-sexp-and-replace
              "c" 'cider-eval-last-sexp-and-replace)
        "y" 'cider-copy-last-result))

;;;

(nmap :keymaps 'emacs-lisp-mode-map
  "c" (general-key-dispatch 'evil-change
        "p" (general-key-dispatch 'fireplace-eval
              "p" 'eval-sexp-at-point
              "c" 'eval-last-sexp
              "d" 'eval-defun
              "r" 'cider-test-run-test)
        "x" (general-key-dispatch 'fireplace-eval-context
              "x" 'cider-eval-sexp-at-point-in-context
              "c" 'cider-eval-last-sexp-in-context)
        "!" (general-key-dispatch 'fireplace-replace
              "!" 'cider-eval-current-sexp-and-replace
              "c" 'cider-eval-last-sexp-and-replace)
        "y" 'cider-copy-last-result))

(nmap :keymaps 'sly-mode-map
  "c" (general-key-dispatch 'evil-change
        "p" (general-key-dispatch 'sly-eval
              ;; "p" 'eval-sexp-at-point
              "c" 'sly-eval-last-expression
              "d" 'sly-eval-defun
              ;; "r" 'cider-test-run-test
              )
        ;; "x" (general-key-dispatch 'fireplace-eval-context
        ;;       "x" 'cider-eval-sexp-at-point-in-context
        ;;       "c" 'cider-eval-last-sexp-in-context
        ;;       )
        ;; "!" (general-key-dispatch 'fireplace-replace
        ;;       "!" 'cider-eval-current-sexp-and-replace
        ;;       "c" 'cider-eval-last-sexp-and-replace)
        ;; "y" 'cider-copy-last-result
        ))

Coerce

(use-package! string-inflection
  :config
  (nmap "c" (general-key-dispatch 'evil-change
              "r c" (saving-excursion (string-inflection-lower-camelcase))
              "r C" (saving-excursion (string-inflection-camelcase))
              "r m" (saving-excursion (string-inflection-camelcase))
              "r s" (saving-excursion (string-inflection-underscore))
              "r u" (saving-excursion (string-inflection-upcase))
              "r -" (saving-excursion (string-inflection-kebab-case))
              "r k" (saving-excursion (string-inflection-kebab-case))
              ;; "r ." (saving-excursion (string-inflection-dot-case))
              ;; "r ." (saving-excursion (string-inflection-space-case))
              ;; "r ." (saving-excursion (string-inflection-title-case))
              )))

Mode-specific config

org-mode

(after! org
  (load! "org-config")
  (load! "org-query"))

Theme overrides

(custom-set-faces!
  `(org-drawer :foreground ,+solarized-s-base1 :bold t)
  `(org-block :foreground ,+solarized-s-base00)
  `(org-meta-line :foreground ,+solarized-s-base1 :italic t)
  `(org-document-title :foreground ,+solarized-s-base01 :height 1.3)
  `(org-done :foreground ,+solarized-green)
  `(org-headline-done :foreground ,+solarized-green)
  `(org-special-keyword :foreground ,+solarized-s-base1 :bold t)
  `(org-date :foreground ,+solarized-blue :underline t)
  `(org-table
    :foreground ,+solarized-s-base0  ; used to be green, I think I like this better?
    :italic t)
  `(org-link :foreground ,+solarized-yellow)
  `(org-todo :foreground ,+solarized-cyan)
  `(org-code :foreground ,+solarized-s-base1)
  `(org-block-begin-line :foreground ,+solarized-s-base1 :italic t)
  `(org-block-end-line :foreground ,+solarized-s-base1 :italic t)
  `(org-document-info-keyword :foreground ,+solarized-s-base1 :italic t)

  `(org-level-1 :foreground ,+solarized-red)
  `(org-level-2 :foreground ,+solarized-green)
  `(org-level-3 :foreground ,+solarized-blue)
  `(org-level-4 :foreground ,+solarized-yellow)
  `(org-level-5 :foreground ,+solarized-cyan)
  `(org-level-6 :foreground ,+solarized-violet)
  `(org-level-7 :foreground ,+solarized-magenta)
  `(org-level-8 :foreground ,+solarized-blue))

Commands

(defun grfn/insert-new-src-block ()
  (interactive)
  (let* ((current-src-block (org-element-at-point))
         (src-block-head (save-excursion
                           (goto-char (org-element-property
                                       :begin current-src-block))
                           (let ((line (thing-at-point 'line t)))
                             (if (not (s-starts-with? "#+NAME:" (s-trim line)))
                                 line
                               (forward-line)
                               (thing-at-point 'line t)))))
         (point-to-insert
          (if-let (results-loc (org-babel-where-is-src-block-result))
              (save-excursion
                (goto-char results-loc)
                (org-element-property
                 :end
                 (org-element-at-point)))
            (org-element-property :end (org-element-at-point)))))
    (goto-char point-to-insert)
    (insert "\n")
    (insert src-block-head)
    (let ((contents (point-marker)))
      (insert "\n#+END_SRC\n")
      (goto-char contents))))

(defun grfn/+org-insert-item (orig direction)
  (interactive)
  (if (and (org-in-src-block-p)
           (equal direction 'below))
      (grfn/insert-new-src-block)
    (funcall orig direction)))

(advice-add #'+org--insert-item :around #'grfn/+org-insert-item)

Bindings

(map!
 (:after org
  :n "C-c C-x C-o" #'org-clock-out
  (:leader
   "n k" #'org-archive-subtree-default)

  (:map org-capture-mode-map
   :n "g RET" #'org-capture-finalize
   :n "g \\"  #'org-captue-refile)))

magit

(after! magit
  (map! :map magit-mode-map
        ;; :n "] ]" #'magit-section-forward
        ;; :n "[ [" #'magit-section-backward
        )

  (transient-define-suffix magit-commit-wip ()
    (interactive)
    (magit-commit-create '("-m" "wip")))

  (transient-append-suffix
    #'magit-commit
    ["c"]
    (list "W" "Commit WIP" #'magit-commit-wip))

  (transient-define-suffix magit-reset-head-back ()
    (interactive)
    (magit-reset-mixed "HEAD~"))

  (transient-define-suffix magit-reset-head-previous ()
    (interactive)
    (magit-reset-mixed "HEAD@{1}"))

  (transient-append-suffix
    #'magit-reset
    ["f"]
    (list "b" "Reset HEAD~"    #'magit-reset-head-back))
  (transient-append-suffix
    #'magit-reset
    ["f"]
    (list "o" "Reset HEAD@{1}" #'magit-reset-head-previous)))

elisp

Org config mode

The minor-mode for this file!

(after! smartparens
  (sp-local-pair 'org-config-mode "'" "'" :actions nil)
  (sp-local-pair 'org-config-mode "`" "`" :actions nil))

(define-minor-mode org-config-mode
  "Minor-mode for tangled org .el config"
  :group 'org
  :lighter "Org-config"
  :keymap '()
  (sp-update-local-pairs 'org-config-mode))

Bindings

(map!
 (:map emacs-lisp-mode-map
  :n "g SPC" #'eval-buffer
  :n "g RET" (λ! () (ert t)) ))

tuareg

Config

(defun aspen/tuareg-setup ()
  (setq-local sp-max-pair-length (->> '("begin" "sig" "struct")
                                      (--map (length it))
                                      (-max))
              whitespace-line-column 80))

(add-hook 'tuareg-mode-hook #'aspen/tuareg-setup)

(defun sp-tuareg-post-handler (id action context)
  (when (equal action 'insert)
    (save-excursion
      (insert "x")
      (newline)
      (indent-according-to-mode))
    (delete-char 1)))

(after! smartparens-ml
  (sp-local-pair 'tuareg-mode "module" "end" :actions nil)

  (dolist (pair-start '("begin" "sig" "struct"))
    (sp-local-pair 'tuareg-mode
                   pair-start "end"
                   :when '(("SPC" "RET" "<evil-ret>"))
                   :unless '(sp-in-string-p)
                   :actions '(insert navigate)
                   :post-handlers '(sp-tuareg-post-handler))))
nil
(after! dune-mode
  (add-hook 'dune-mode-hook 'paxedit-mode))

Bindings

(map!
 (:map tuareg-mode-map
  :n "g RET" (λ! () (compile "dune build @@runtest"))
  :n "g SPC" #'dune-promote
  :n "g \\" #'utop
  :n "g y" #'merlin-locate-type
  "C-c C-f" (λ! () (compile "dune fmt"))))

Theme overrides

(custom-set-faces!
  `(tuareg-font-lock-governing-face :foreground ,+solarized-s-base01 :bold t)
  `(tuareg-font-lock-label-face :foreground ,+solarized-blue)
  `(tuareg-font-lock-constructor-face :foreground ,+solarized-yellow)
  `(tuareg-font-lock-operator-face :foreground ,+solarized-red)
  `(tuareg-font-lock-attribute-face :foreground ,+solarized-red :bold nil)
  `(tuareg-font-lock-extension-node-face :background nil :inherit 'font-lock-preprocessor-face)
  `(merlin-eldoc-occurrences-face :background ,+solarized-s-base2)
  `(merlin-type-face :background ,+solarized-s-base2)
  `(utop-prompt :foreground ,+solarized-blue)
  `(utop-frozen :foreground ,+solarized-s-base1 :italic t)
  `(vertico-group-title :foreground ,+solarized-s-base1)
  `(vertico-group-header :foreground ,+solarized-s-base1))

clojure

Setup

(defun clojure-thing-at-point-setup ()
  (interactive)
  ;; Used by cider-find-dwim to parse the symbol at point
  (setq-local
   thing-at-point-file-name-chars
   (concat thing-at-point-file-name-chars
           "><!?")))

(defun +grfn/clojure-setup ()
  ;; (flycheck-select-checker 'clj-kondo)
  (require 'flycheck)
  (push 'clojure-cider-kibit flycheck-disabled-checkers)
  (push 'clojure-cider-eastwood flycheck-disabled-checkers)
  (push 'clojure-cider-typed flycheck-disabled-checkers)
  )

(after! clojure-mode
  (define-clojure-indent
    (PUT 2)
    (POST 2)
    (GET 2)
    (PATCH 2)
    (DELETE 2)
    (context 2)
    (checking 3)
    (match 1)
    (domonad 0)
    (describe 1)
    (before 1)
    (it 2))

  (add-hook 'clojure-mode-hook #'clojure-thing-at-point-setup)
  (add-hook 'clojure-mode-hook #'+grfn/clojure-setup))

(use-package! flycheck-clojure
  ;; :disabled t
  :after (flycheck cider)
  :config
  (flycheck-clojure-setup))

(after! clj-refactor
  (setq cljr-magic-requires :prompt
        cljr-clojure-test-declaration "[clojure.test :refer :all]"
        cljr-cljc-clojure-test-declaration"#?(:clj [clojure.test :refer :all]
:cljs [cljs.test :refer-macros [deftest is testing]])"
        )
  (add-to-list
   'cljr-magic-require-namespaces
   '("s" . "clojure.spec.alpha")))

(set-popup-rule! "^\\*cider-test-report" :size 0.4)
nil

Commands

(defun grfn/run-clj-or-cljs-test ()
  (interactive)
  (message "Running tests...")
  (cl-case (cider-repl-type-for-buffer)
    (cljs
     (cider-interactive-eval
      "(with-out-str (cljs.test/run-tests))"
      (nrepl-make-response-handler
       (current-buffer)
       (lambda (_ value)
         (with-output-to-temp-buffer "*cljs-test-results*"
           (print
            (->> value
                 (s-replace "\"" "")
                 (s-replace "\\n" "\n")))))
       nil nil nil)))
    (('clj 'multi)
     (funcall-interactively
      #'cider-test-run-ns-tests
      nil))))

(defun cider-copy-last-result ()
  (interactive)
  (cider-interactive-eval
   "*1"
   (nrepl-make-response-handler
    (current-buffer)
    (lambda (_ value)
      (kill-new value)
      (message "Copied last result (%s) to clipboard"
               (if (= (length value) 1) "1 char"
                 (format "%d chars" (length value)))))
    nil nil nil)))

Bindings

(map!
 (:after
  clojure-mode
  (:map clojure-mode-map
   :n "] f" 'forward-sexp
   :n "[ f" 'backward-sexp))

 (:after
  cider-mode
  (:map cider-mode-map
   :n "g SPC" 'cider-eval-buffer
   :n "g \\"  'cider-switch-to-repl-buffer
   :n "K"     'cider-doc
   :n "g K"   'cider-apropos
   :n "g d"   'cider-find-dwim
   :n "C-w ]" 'cider-find-dwim-other-window
   ;; :n "g RET" 'cider-test-run-ns-tests
   :n "g RET" 'grfn/run-clj-or-cljs-test
   :n "g r" #'cljr-rename-symbol

   "C-c C-r r" 'cljr-add-require-to-ns
   "C-c C-r i" 'cljr-add-import-to-ns

   (:localleader
    ;; :desc "Inspect last result" :n "i" 'cider-inspect-last-result
    ;; :desc "Search for documentation" :n "h s" 'cider-apropos-doc
    :desc "Add require to ns" :n "n r" 'cljr-add-require-to-ns
    :desc "Add import to ns" :n "n i" 'cljr-add-import-to-ns))
  (:map cider-repl-mode-map
   :n "g \\" 'cider-switch-to-last-clojure-buffer)))

rust

(defun aspen/rust-setup ()
  (interactive)
  (+evil-embrace-angle-bracket-modes-hook-h)
  (setq-local whitespace-line-column 100
              fill-column 100))

(add-hook 'rust-mode-hook #'aspen/rust-setup)

Bindings

(map!
 (:map rust-mode-map
  :n "g RET" #'lsp-rust-analyzer-run
  :n "g R" #'lsp-find-references
  :n "g d" #'lsp-find-definition
  :n "g Y" #'lsp-goto-type-definition
  (:localleader
   "m" #'lsp-rust-analyzer-expand-macro)))

Theme overrides

(custom-set-faces!
  `(rust-unsafe :foreground ,+solarized-red))

common-lisp

Commands

(defun aspen/sly-panettone ()
  (interactive)
  (sly
   (concat
    (s-trim
     (shell-command-to-string
      "nix-build -o sbcl -E 'with import ~/code/depot {}; nix.buildLisp.sbclWith [web.panettone]'"))
    "/bin/sbcl")))

(defun aspen/setup-lisp ()
  (interactive)
  (rainbow-delimiters-mode)
  (paxedit-mode 1)
  (flycheck-mode -1))

(add-hook 'common-lisp-mode-hook #'aspen/setup-lisp)

(defun sly-run-tests ()
  (interactive)
  ;; TODO: handle other test frameworks
  (let ((orig-window (get-buffer-window)))
    (sly-eval '(fiveam:run!))
    (funcall-interactively #'sly-mrepl-sync)
    (select-window orig-window)))

Bindings

(map!
 (:map sly-mode-map
  :n "g \\" #'sly-mrepl-sync
  :n "g d" #'sly-edit-definition
  :n "K" #'sly-documentation
  :n "g SPC" #'sly-compile-and-load-file
  :n "g RET" #'sly-run-tests)

 (:map sly-mrepl-mode-map
  "C-k" #'sly-mrepl-previous-prompt
  "C-r" #'isearch-backward))

Completion

Corfu

(setopt +corfu-want-ret-to-confirm nil)

(use-package! corfu
  :demand t
  :bind (:map corfu-map
              ("TAB" . corfu-next)
              ([tab] . corfu-next)
              ("S-TAB" . corfu-previous)
              ([backtab] . corfu-previous))
  :init (setopt corfu-on-exact-match 'insert
                corfu-preselect 'prompt
                completion-cycle-threshold 1
                corfu-quit-no-match t
                corfu-quit-at-boundary t)
  :config
  (map! :map corfu-map
        :i "TAB" #'corfu-next
        :i [tab] #'corfu-next
        :i "S-TAB" #'corfu-previous
        :i [backtab] #'corfu-previous))

Fuzzy search

(use-package! hotfuzz
  :after (orderless corfu)
  :config
  (setopt completion-styles '(hotfuzz basic)
          completion-ignore-case t))

Email

(after! notmuch
  (setq notmuch-saved-searches
        '((:name "inbox" :query "tag:inbox tag:important not tag:trash" :key "i")
          (:name "flagged" :query "tag:flagged" :key "f")
          (:name "sent" :query "tag:sent" :key "s")
          (:name "drafts" :query "tag:draft" :key "d")

          (:name "work" :query "tag:inbox and tag:important and path:work/**"
                 :key "w")
          (:name "personal" :query "tag:inbox and tag:important and path:personal/**"
                 :key "p"))
        message-send-mail-function 'message-send-mail-with-sendmail
        message-sendmail-f-is-evil 't
        message-sendmail-envelope-from 'header
        message-sendmail-extra-arguments '("--read-envelope-from")))

(defun aspen/notmuch-sync ()
  (interactive)
  (let* ((search-buffer (current-buffer))
         (proc (start-process-shell-command
                "notmuch-sync"
                "*notmuch-sync*"
                "cd ~/mail/personal/ && gmi sync"))
         (buf (process-buffer proc)))

    (set-process-sentinel
     proc
     (lambda (proc msg)
       (internal-default-process-sentinel proc msg)
       (when (and (string= msg "finished\n"))
         (kill-buffer buf)
         (with-current-buffer search-buffer
           (when (eq major-mode 'notmuch-search-mode)
             (notmuch-refresh-this-buffer))))))

    (with-current-buffer buf
      (+popup-buffer-mode))
    (display-buffer buf '(display-buffer-at-bottom . ()))))

(set-popup-rule!
  "^\\*notmuch-sync\\*$"
  :select nil
  :quit 'other)

(map! :map notmuch-search-mode-map
      :n "g SPC" #'aspen/notmuch-sync)

Bindings

(map!
 (:leader
  :desc "Email" :n "o m" #'notmuch-jump-search
  :desc "Search email" "s M" #'consult-notmuch))

Theme

(custom-set-faces!
  `(notmuch-message-summary-face
    :background ,+solarized-halfway-highlight))

Misc

TVL

(require 'tvl)

Matchit

(use-package! evil-matchit)

Direnv

(use-package! direnv
  :config (direnv-mode))

IRC

Connecting to IRC

(defvar irc-servers
  '("hackint"
    "libera"))

(defun irc-connect (server)
  (interactive
   (list (completing-read "Server: " irc-servers)))
  (let ((pw (-> (shell-command-to-string
                 (format "pass irccloud/%s" server))
                (s-trim)
                (s-lines)
                (-last-item)))
        (gnutls-verify-error nil))
    (erc-tls :server "bnc.irccloud.com"
             :port 6697
             :nick "aspen"
             :password (concat "bnc@"
                               (s-trim (shell-command-to-string "hostname"))
                               ":"
                               pw))))

(defun aspen/switch-to-erc-buffer-or-connect ()
  (interactive)
  (if (functionp 'erc-switch-to-buffer)
      (call-interactively #'erc-switch-to-buffer)
    (call-interactively #'irc-connect)))
(map! :leader "o I" #'irc-connect
      :leader "o i" #'aspen/switch-to-erc-buffer-or-connect)

IRC alerts

(use-package! alert)

(defgroup erc-alert nil
  "Alert me using alert.el for important ERC messages"
  :group 'erc)

(defcustom erc-noise-regexp
  "\\(Logging in:\\|Signing off\\|You're now away\\|Welcome back\\)"
  "This regexp matches unwanted noise."
  :type 'regexp
  :group 'erc)

(setq tvl-enabled? t)

(defun disable-tvl-notifications ()
  (interactive)
  (setq tvl-enabled? nil))

(defun enable-tvl-notifications ()
  (interactive)
  (setq tvl-enabled? t))

(defun erc-alert-important-p (info)
  (let ((message (plist-get info :message))
        (erc-message (-> info (plist-get :data) (plist-get :message)))
        (erc-channel (-> info (plist-get :data) (plist-get :channel))))
    (and erc-message
         (not (or (string-match "^\\** *Users on #" message)
                  (string-match erc-noise-regexp
                                message)))
         (or (and tvl-enabled?
                  (string-equal erc-channel "#tvl"))
             (string-match "grfn" message)))))

(comment
 last-info
 erc-noise-regexp
 (setq tvl-enabled? nil)
 )

(defun my-erc-hook (&optional match-type nick message)
  "Shows a notification, when user's nick was mentioned.
If the buffer is currently not visible, makes it sticky."
  (setq last-message message)
  (if (or (null match-type) (not (eq match-type 'fool)))
      (let (alert-log-messages)
        (alert (or message (buffer-string))
               :severity (if (string-match "grfn" (or message ""))
                             'high 'low)
               :title (or nick (buffer-name))
               :data `(:message ,(or message (buffer-string))
                                :channel ,(or nick (buffer-name)))))))

(add-hook 'erc-text-matched-hook 'my-erc-hook)
(add-hook 'erc-insert-modify-hook 'my-erc-hook)

(defun my-erc-define-alerts (&rest ignore)
  ;; Unless the user has recently typed in the ERC buffer, highlight the fringe
  (alert-add-rule
   :status   '(buried visible idle)
   :severity '(moderate high urgent)
   :mode     'erc-mode
   :predicate
   #'(lambda (info)
       (and (not (eq (current-buffer) (plist-get info :buffer)))
            (string-match "grfn:" (plist-get info :message))))
   :persistent
   #'(lambda (info)
       ;; If the buffer is buried, or the user has been idle for
       ;; `alert-reveal-idle-time' seconds, make this alert
       ;; persistent.  Normally, alerts become persistent after
       ;; `alert-persist-idle-time' seconds.
       (memq (plist-get info :status) '(buried idle)))
   :style 'message
   :continue t)

  (alert-add-rule
   :status 'buried
   :mode   'erc-mode
   :predicate #'erc-alert-important-p
   :style 'libnotify
   :append t)

  (alert-add-rule
   :status 'buried
   :mode   'erc-mode
   :predicate #'erc-alert-important-p
   :style 'message
   :append t)

  (alert-add-rule
   :mode 'erc-mode
   :predicate #'erc-alert-important-p
   :style 'log
   :append t)

  (alert-add-rule :mode 'erc-mode :style 'ignore :append t))

(add-hook 'erc-connect-pre-hook 'my-erc-define-alerts)

Don't send :q, etc, to the server

(defun fix-irc-message (msg)
  (let ((msg (s-trim msg)))
    (if (string-equal msg ":q") "" msg)))
(advice-add #'erc-user-input :filter-return #'fix-irc-message)

Theme overrides

(custom-set-faces!
  `(erc-button :foreground ,+solarized-blue))

TODO Nick rainbow colors

Stole this from https://github.com/jtdaugherty/emacs-config/blob/master/common/erc-nick-colors.el.

IT doesn't work though :(

(setq nick-face-list '())

;; Define the list of colors to use when coloring IRC nicks.
(setq-default erc-colors-list (list +solarized-yellow
                                    +solarized-orange
                                    +solarized-red
                                    +solarized-magenta
                                    +solarized-violet
                                    +solarized-blue
                                    +solarized-cyan
                                    +solarized-green))

(defun build-nick-face-list ()
  "build-nick-face-list builds a list of new faces using the
foreground colors specified in erc-colors-list.  The nick faces
created here will be used to format IRC nicks."
  (let ((i -1))
    (setq nick-face-list
          (mapcar
           (lambda (COLOR)
             (setq i (1+ i))
             (list (custom-declare-face
                    (make-symbol (format "erc-nick-face-%d" i))
                    (list (list t (list :foreground COLOR)))
                    (format "Nick face %d" i))))
           erc-colors-list))))

(defun erc-insert-nick-colors ()
  "This insert-modify hook looks for nicks in new messages and
computes md5(nick) and uses substring(md5_value, 0, 4) mod (length
nick-face-list) to index the face list and produce the same face for a
given nick each time it is seen.  We get a lot of collisions this way,
unfortunately, but it's better than some other methods I tried.
Additionally, if you change the order or size of the erc-colors-list,
you'll change the colors used for nicks."
  (if (null nick-face-list) (build-nick-face-list))
  (save-excursion
    (goto-char (point-min))
    (if (looking-at "<\\([^>]*\\)>")
        (let ((nick (match-string 1)))
          (put-text-property (match-beginning 1) (match-end 1)
                             'face (nth
                                    (mod (string-to-number
                                          (substring (md5 nick) 0 4) 16)
                                         (length nick-face-list))
                                    nick-face-list))))))

;; This adds the ERC message insert hook.
(add-hook 'erc-insert-modify-hook 'erc-insert-nick-colors)

Hacks

Not having this breaks elisp documentation :(

(defvar elisp-demos-user-files nil)