/.emacs.d

Vanilla, Evil, literate Emacs configuration

Primary LanguageMakefileMIT LicenseMIT

mpereira’s Emacs configuration

This is my vanilla, Evil, literate Emacs configuration. It enables most of my computing needs, since most of the time I’m on a computer I’m on Emacs.

It can be found at https://github.com/mpereira/.emacs.d.

I wouldn’t recommend others to use this configuration as-is. I’m sure there are sections, snippets, or settings that might be interesting, though.

If you’d like to know more about my relationship with Emacs, check out this thing I wrote: How to open a file in Emacs: a short story about Lisp, technology, and human progress.

One day I’ll include some screenshots here.

Installing Emacs

I mostly use GUI Emacs on macOS. For work I use TUI Emacs on Ubuntu via SSH. On macOS I install the excellent homebrew-emacs-head package created by Davide Restivo:

brew install emacs-head@28 \
     --with-cocoa \
     --with-imagemagick \
     --with-modern-icon-pen \
     --with-native-comp \
     --with-no-frame-refocus \
     --with-pdumper \
     --with-xwidgets

On Ubuntu I install Alex Murray’s GNU Emacs Snap package based on the “latest/edge” channel, which comes with native-comp and works great.

First make sure libgccjit is installed:

sudo apt install libgccjit-10-dev

Then install the Emacs snap:

sudo snap install emacs --edge --classic

Table of Contents

Dependencies

Some dependencies are installed with the setup.sh script, which is tangled from this file.

Getting the file name:

setup.sh preamble:

# This file is auto-generated by Emacs via `(org-babel-tangle-file "<<configuration-org-file()>>")'.

set -euxo pipefail

Other dependencies have to be manually set up:

  • GitHub personal token (for magit, gist, etc.)
  • Wolfram Alpha AppID (for wolfram)
  • TODO: Google Apps Calendar (for org-gcal)
  • ~/.emacs.d/circe-secrets.el
    • mpereira/secret-circe-nickserv-password
  • ~/.emacs.d/org-gcal-secrets.el
    • mpereira/secret-org-gcal-client-id
    • mpereira/secret-org-gcal-client-secret
    • mpereira/secret-org-gcal-file-alist
  • ~/.emacs.d/wolfram-secrets.el
    • mpereira/secret-wolfram-alpha-app-id

Silent exports for emacs lisp org babel code blocks

Having this as an org file property doesn’t seem to work for some reason.

:PROPERTIES:
:header-args: :results output silent :exports both
:END:

Set it with emacs lisp.

(setq org-babel-default-header-args:emacs-lisp '((:results . "output silent")))

quelpa and quelpa-use-package

(unless (package-installed-p 'quelpa)
  (with-temp-buffer
    (url-insert-file-contents "https://raw.githubusercontent.com/quelpa/quelpa/master/quelpa.el")
    (eval-buffer)
    (quelpa-self-upgrade)))

(quelpa '(quelpa-use-package
          :fetcher github
          :repo "quelpa/quelpa-use-package"))

(require 'quelpa-use-package)

Utility libraries

async

(use-package async)

aio

(use-package aio)

cl-lib

(use-package cl-lib)

s

(use-package s)

dash

(use-package dash)

thingatpt+

(use-package thingatpt+
  :ensure nil
  :quelpa (thingatpt+
           :url "https://raw.githubusercontent.com/emacsmirror/emacswiki.org/master/thingatpt+.el"
           :fetcher url))

help-fns+

(use-package help-fns+
  :ensure nil
  :quelpa (help-fns+
           :fetcher github
           :repo "emacsmirror/help-fns-plus"))

ts

(use-package ts
  :ensure nil
  :quelpa (ts
           :fetcher github
           :repo "alphapapa/ts.el"))

elx

(use-package elx
  :ensure nil
  :quelpa (elx
           :fetcher github
           :branch "dont-break-if-no-licensee"
           :repo "mpereira/elx"))

Foundational

general

(use-package general
  :custom
  (use-package-hook-name-suffix . nil))

paradox

(use-package paradox
  :config
  (paradox-enable)

  ;; Disable annoying "do you want to set up GitHub integration" prompt.
  ;; https://github.com/Malabarba/paradox/issues/23
  (setq paradox-github-token t))

exec-path-from-shell

This needs to be loaded before code that depends on PATH modifications, e.g. executable-find.

Check mpereira/dotfiles .profile for PATH modifications.

(use-package exec-path-from-shell
  :config
  (dolist (shell-variable '("SSH_AUTH_SOCK"
                            "SSH_AGENT_PID"))
    (add-to-list 'exec-path-from-shell-variables shell-variable))

  ;; Removing the "-l" flag so that it doesn't try to source /etc/profile. For
  ;; whatever reason it causes the paths from `/usr/libexec/path_helper' to be
  ;; at the head of `$PATH'.
  (setq exec-path-from-shell-arguments '("-i"))

  (exec-path-from-shell-initialize))

Variables

(setq mpereira/custom-file (expand-file-name "custom.el" user-emacs-directory))

(setq mpereira/leader ",")

;; NOTE(2023-01-25): switching from `doom-acario-light' because magit diffs look
;; bad.
(setq mpereira/light-theme 'modus-operandi)
(setq mpereira/dark-theme 'vscode-dark-plus)
(setq mpereira/initial-theme mpereira/dark-theme)

(setq mpereira/cloud-synced-directory
      (file-name-as-directory
       (expand-file-name
        "~/Library/Mobile Documents/com~apple~CloudDocs/")))
(setq mpereira/org-directory (expand-file-name "org" mpereira/cloud-synced-directory))

(setq mpereira/org-calendar-file (expand-file-name "gcal/calendar.org"
                                                   mpereira/org-directory))
(setq mpereira/org-calendar-buffer-name (file-name-nondirectory
                                         mpereira/org-calendar-file))
;; Empirically, 2 seconds seems to be good enough.
(setq mpereira/org-gcal-request-timeout 2)

(setq mpereira/magit-status-width 120)

(setq mpereira/org-agenda-width 120)

(setq mpereira/fill-column 80)
(setq mpereira/fill-column-wide 120)

(setq mpereira/eshell-prompt-max-directory-length 50)
(setq mpereira/mode-line-max-directory-length 15)

(setq mpereira/is-gnu-bash
      (with-temp-buffer
        (call-process insert-directory-program nil t nil "--version")
        (string-match-p "GNU" (buffer-string))))

Redefinitions, advices

Make org src buffer name shorter and nicer

Before

*Org Src configuration.org[ emacs-lisp ]*
*Org Src configuration.org[ emacs-lisp ]<2>*

After

configuration.org (org src)
configuration.org (org src)<2>
(defun org-src--construct-edit-buffer-name (org-buffer-name lang)
  "Construct the buffer name for a source editing buffer."
  (concat org-buffer-name " (org src)"))

Improve Lisp code indentation

Before

(:foo bar
      :baz qux)

After

(:foo bar
 :baz qux)

I got this from Fuco1/.emacs.d/site-lisp/my-redef.el.

(eval-after-load "lisp-mode"
  '(defun lisp-indent-function (indent-point state)
     "This function is the normal value of the variable `lisp-indent-function'.
The function `calculate-lisp-indent' calls this to determine if the arguments of
a Lisp function call should be indented specially. INDENT-POINT is the position
at which the line being indented begins. Point is located at the point to indent
under (for default indentation); STATE is the `parse-partial-sexp' state for
that position. If the current line is in a call to a Lisp function that has a
non-nil property `lisp-indent-function' (or the deprecated `lisp-indent-hook'),
it specifies how to indent. The property value can be: * `defun', meaning indent
`defun'-style \(this is also the case if there is no property and the function
has a name that begins with \"def\", and three or more arguments); * an integer
N, meaning indent the first N arguments specially
  (like ordinary function arguments), and then indent any further
  arguments like a body;
* a function to call that returns the indentation (or nil).
  `lisp-indent-function' calls this function with the same two arguments
  that it itself received.
This function returns either the indentation to use, or nil if the
Lisp function does not specify a special indentation."
     (let ((normal-indent (current-column))
           (orig-point (point)))
       (goto-char (1+ (elt state 1)))
       (parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
       (cond
        ;; car of form doesn't seem to be a symbol, or is a keyword
        ((and (elt state 2)
              (or (not (looking-at "\\sw\\|\\s_"))
                  (looking-at ":")))
         (if (not (> (save-excursion (forward-line 1) (point))
                     calculate-lisp-indent-last-sexp))
             (progn (goto-char calculate-lisp-indent-last-sexp)
                    (beginning-of-line)
                    (parse-partial-sexp (point)
                                        calculate-lisp-indent-last-sexp 0 t)))
         ;; Indent under the list or under the first sexp on the same
         ;; line as calculate-lisp-indent-last-sexp.  Note that first
         ;; thing on that line has to be complete sexp since we are
         ;; inside the innermost containing sexp.
         (backward-prefix-chars)
         (current-column))
        ((and (save-excursion
                (goto-char indent-point)
                (skip-syntax-forward " ")
                (not (looking-at ":")))
              (save-excursion
                (goto-char orig-point)
                (looking-at ":")))
         (save-excursion
           (goto-char (+ 2 (elt state 1)))
           (current-column)))
        (t
         (let ((function (buffer-substring (point)
                                           (progn (forward-sexp 1) (point))))
               method)
           (setq method (or (function-get (intern-soft function)
                                          'lisp-indent-function)
                            (get (intern-soft function) 'lisp-indent-hook)))
           (cond ((or (eq method 'defun)
                      (and (null method)
                           (> (length function) 3)
                           (string-match "\\`def" function)))
                  (lisp-indent-defform state indent-point))
                 ((integerp method)
                  (lisp-indent-specform method state
                                        indent-point normal-indent))
                 (method
                  (funcall method indent-point state)))))))))

Archive org subtrees under the same hierarchy as the original in the archive files

I got this from Fuco1/.emacs.d/files/org-defs.el.

FIXME: I’ve been having issues with archiving lately because this defadvice became incompatible with newer versions of org. Fuco1 is thinking of turning it into a package. For now I’m making this source block not be tangled and using andersjohansson/org-archive-hierarchically instead.

Not tangled!

(defadvice org-archive-subtree (around fix-hierarchy activate)
  (let* ((fix-archive-p (and (not current-prefix-arg)
                             (not (use-region-p))))
         (afile (org-extract-archive-file (org-get-local-archive-location)))
         (buffer (or (find-buffer-visiting afile) (find-file-noselect afile))))
    ad-do-it
    (when fix-archive-p
      (with-current-buffer buffer
        (goto-char (point-max))
        (while (org-up-heading-safe))
        (let* ((olpath (org-entry-get (point) "ARCHIVE_OLPATH"))
               (path (and olpath (split-string olpath "/")))
               (level 1)
               tree-text)
          (when olpath
            (org-mark-subtree)
            (setq tree-text (buffer-substring (region-beginning) (region-end)))
            (let (this-command) (org-cut-subtree))
            (goto-char (point-min))
            (save-restriction
              (widen)
              (-each path
                (lambda (heading)
                  (if (re-search-forward
                       (rx-to-string
                        `(: bol (repeat ,level "*") (1+ " ") ,heading)) nil t)
                      (org-narrow-to-subtree)
                    (goto-char (point-max))
                    (unless (looking-at "^")
                      (insert "\n"))
                    (insert (make-string level ?*)
                            " "
                            heading
                            "\n"))
                  (cl-incf level)))
              (widen)
              (org-end-of-subtree t t)
              (org-paste-subtree level tree-text))))))))

Provide counsel-symbol-at-point

counsel-symbol-at-point was removed from counsel so I’m adding a version I found on the internet here.

(defun counsel-symbol-at-point ()
  "Return current symbol at point as a string."
  (let ((s (thing-at-point 'symbol)))
    (and (stringp s)
         (if (string-match "\\`[`']?\\(.*?\\)'?\\'" s)
             (match-string 1 s)
           s))))

Make align-regexp not use tabs

Found on Stack Overflow.

(defadvice align-regexp (around align-regexp-with-spaces activate)
  (let ((indent-tabs-mode nil))
    ad-do-it))

Helper functions

Standard library type of things

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

(defun eshell-p (buffer)
  "Return t if BUFFER is an Eshell buffer."
  (with-current-buffer buffer
    (eq major-mode 'eshell-mode)))

(defun plist-each (function plist)
  "Iterate FUNCTION (a two-argument function) over PLIST."
  (when plist
    (funcall function (car plist) (cadr plist))
    (plist-each function (cddr plist))))

(defun queue-push (queue-sym element &optional bounded-limit)
  "TODO: docstring."
  (when (or (not bounded-limit)
            (< (length (symbol-value queue-sym))
               bounded-limit))
    (add-to-list queue-sym element t (lambda (a b) nil))))

(defun queue-pop (queue-sym)
  "TODO: docstring."
  (let* ((queue (symbol-value queue-sym))
         (popped-element (car queue)))
    (when popped-element
      (set queue-sym (cdr queue)))
    popped-element))

(defun unadvice (sym)
  "Remove all advices from symbol SYM."
  (interactive "aFunction symbol: ")
  (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))

Miscellaneous

(defmacro print-and-return (&rest body)
  "TODO: docstring."
  (let ((result-symbol (make-symbol "result")))
    `(let ((,result-symbol ,@body))
       (message "************************************************************")
       (pp ',@body)
       (message "||")
       (message "\\/")
       (print ,result-symbol)
       (message "************************************************************")
       ,result-symbol)))

(defun mpereira/hl-line-mode-disable ()
  "TODO: docstring."
  (interactive)
  (setq-local global-hl-line-mode nil))

(defun mpereira/hide-trailing-whitespace ()
  (interactive)
  (setq-local show-trailing-whitespace nil))

(defun mpereira/delete-file-and-buffer ()
  "Kill the current buffer and deletes the file it is visiting."
  (interactive)
  (let ((filename (buffer-file-name)))
    (when filename
      (if (vc-backend filename)
          (vc-delete-file filename)
        (progn
          (delete-file filename)
          (message "Deleted file %s" filename)
          (kill-buffer))))))

(defun mpereira/rename-file-and-buffer ()
  "Rename the current buffer and file it is visiting."
  (interactive)
  (let ((filename (buffer-file-name)))
    (if (not (and filename (file-exists-p filename)))
        (message "Buffer is not visiting a file!")
      (let ((new-name (read-file-name "New name: " filename)))
        (cond
         ((vc-backend filename) (vc-rename-file filename new-name))
         (t
          (rename-file filename new-name t)
          (set-visited-file-name new-name t t)))))))

;; https://zck.org/emacs-move-file
(defun mpereira/move-file-and-buffer (new-location)
  "Write this file to NEW-LOCATION, and delete the old one."
  (interactive (list (expand-file-name
                      (read-file-name "Move file to: "
                                      default-directory
                                      (expand-file-name (file-name-nondirectory (buffer-name))
                                                        default-directory)))))
  (if (file-regular-p new-location)
      (when (file-exists-p new-location)
        (delete-file new-location))
    (let ((old-location (expand-file-name (buffer-file-name))))
      (write-file new-location t)
      (when (and old-location
                 (file-exists-p new-location)
                 (not (string-equal old-location new-location)))
        (delete-file old-location)))))

(defun mpereira/pp-macroexpand-all ()
  "TODO: docstring."
  (interactive)
  (let ((form (macroexpand-all (sexp-at-point))))
    (with-current-buffer-window " *mpereira/pp-macroexpand-all*" nil nil
      (pp form)
      (emacs-lisp-mode))))

(require 'thingatpt)
(require 'thingatpt+)
(defun mpereira/eval-thing-at-or-around-point ()
  "Evaluate thing at or surrounding the point."
  (interactive)
  (save-excursion
    (let* ((string-thing (tap-string-at-point))
           (symbol-thing (tap-symbol-at-point))
           (sexp-thing (sexp-at-point)))
      (cond
       (string-thing
        (let* ((_ (message "string"))
               (bounds (tap-bounds-of-string-at-point))
               (string-form (substring-no-properties string-thing))
               (string-value (substring-no-properties
                              (tap-string-contents-at-point))))
          (message "%s%s" string-form string-form)
          (eros--eval-overlay string-value (cdr bounds))))
       (symbol-thing
        (let* ((_ (message "symbol"))
               (bounds (tap-bounds-of-symbol-at-point))
               (symbol-name (substring-no-properties
                             (tap-symbol-name-at-point)))
               (symbol-value (eval symbol-thing)))
          (message "%s" symbol-name)
          (message "")
          (message "%s" symbol-value)
          (eros--eval-overlay symbol-value (cdr bounds))))
       (sexp-thing
        (let* ((_ (message "sexp"))
               (bounds (tap-bounds-of-sexp-at-point))
               (value (eval sexp-thing)))
          (message "%s" sexp-thing)
          (message "")
          (message "%s" value)
          (eros--eval-overlay value (cdr bounds))))))))

(defun mpereira/split-window-below-and-switch ()
  "Split the window horizontally then switch to the new window."
  (interactive)
  (split-window-below)
  (balance-windows)
  (other-window 1))

(defun mpereira/split-window-right-and-switch ()
  "Split the window vertically then switch to the new window."
  (interactive)
  (split-window-right)
  (balance-windows)
  (other-window 1))

(defun mpereira/toggle-window-split ()
  (interactive)
  (if (= (count-windows) 2)
      (let* ((this-win-buffer (window-buffer))
             (next-win-buffer (window-buffer (next-window)))
             (this-win-edges (window-edges (selected-window)))
             (next-win-edges (window-edges (next-window)))
             (this-win-2nd (not (and (<= (car this-win-edges)
                                         (car next-win-edges))
                                     (<= (cadr this-win-edges)
                                         (cadr next-win-edges)))))
             (splitter
              (if (= (car this-win-edges)
                     (car (window-edges (next-window))))
                  'split-window-horizontally
                'split-window-vertically)))
        (delete-other-windows)
        (let ((first-win (selected-window)))
          (funcall splitter)
          (if this-win-2nd (other-window 1))
          (set-window-buffer (selected-window) this-win-buffer)
          (set-window-buffer (next-window) next-win-buffer)
          (select-window first-win)
          (if this-win-2nd (other-window 1))))
    (message "Can only toggle window split for 2 windows")))

(defun mpereira/indent-buffer ()
  "Indents the current buffer."
  (interactive)
  (indent-region (point-min) (point-max)))

(with-eval-after-load "lispy"
  (defun mpereira/inside-bounds-dwim ()
    ;; (when-let (lispy--bounds-dwim)
    ;;   (when (<)))
    )

  (defun mpereira/backward-sexp-begin (arg)
    "Moves to the beginning of the previous ARG nth sexp."
    (interactive "p")
    (if-let (bounds (lispyville--in-string-p))
        ;; Go to beginning of string.
        (goto-char (car bounds))
      ;; `backward-sexp' will enter list-like sexps when point is on the closing
      ;; character. So we move one character to the right.
      (when (looking-at lispy-right)
        (forward-char 1))
      (backward-sexp arg)))

  (defun mpereira/forward-sexp-begin (arg)
    "Moves to the beginning of the next ARG nth sexp. The fact that this doesn't
exist in any structured movement package is mind-boggling to me."
    (interactive "p")
    (when-let (bounds (lispyville--in-string-p))
      (goto-char (car bounds)))
    (dotimes (_ arg)
      (forward-sexp 1)
      (if (looking-at lispy-right)
          ;; Prevent moving forward from last element in current level.
          (backward-sexp 1)
        (progn
          (forward-sexp 1)
          (backward-sexp 1)))))

  ;; Idea: move up to the parent sexp, count the number of sexps inside it with
  ;; `scan-lists' or `scan-sexps' or `paredit-scan-sexps-hack' to know whether
  ;; or not we're at the last sexp.
  (defun mpereira/forward-sexp-end (arg)
    "Moves to the end of the next ARG nth sexp. The fact that this doesn't exist
in any structured movement package is mind-boggling to me."
    (interactive "p")
    (let ((region-was-active (region-active-p)))
      ;; If a region is selected, pretend it's not so that `lispy--bounds-dwim'
      ;; doesn't return the bounds of the region. We want the bounds of the
      ;; actual thing under the point.
      (cl-letf (((symbol-function 'region-active-p) #'(lambda () nil)))
        (when-let (bounds (lispy--bounds-dwim))
          (let ((end (- (cdr bounds) 1)))
            (if (< (point) end)
                ;; Move to the end of the current sexp if not already there.
                (progn
                  (goto-char end)
                  ;; When a region is active we need to move right an extra
                  ;; character.
                  (when (and region-was-active)
                    (forward-char 1)))
              (progn
                ;; Move one character to the right in case point is on a list-like
                ;; closing character so that the subsequent `lispy--bounds-dwim'
                ;; start is right.
                (when (looking-at lispy-right)
                  (forward-char 1))
                ;; Go to the beginning of the current sexp so that
                ;; `mpereira/forward-sexp-begin' works.
                (when-let (bounds (lispy--bounds-dwim))
                  (goto-char (car bounds)))
                ;; Move to the beginning of the next sexp.
                (mpereira/forward-sexp-begin arg)
                ;; Go to the end of the sexp.
                (when-let (bounds (lispy--bounds-dwim))
                  (goto-char (- (cdr bounds) 1))
                  ;; When a region is active and we're not at the last sexp we
                  ;; need to move right an extra character.
                  (when (and region-was-active
                             ;; TODO
                             ;; (not last-sexp)
                             )
                    (forward-char 1)))))))))))

(with-eval-after-load "evil"
  (with-eval-after-load "lispyville"
    (defun mpereira/insert-to-beginning-of-list (arg)
      (interactive "p")
      (lispyville-backward-up-list)
      (evil-forward-char)
      (evil-insert arg))

    (defun mpereira/append-to-end-of-list (arg)
      (interactive "p")
      (lispyville-up-list)
      (evil-insert arg))))

(defun mpereira/org-sort-parent-entries (&rest args)
  ;; `org-sort-entries' doesn't respect `save-excursion'.
  (let ((origin (point)))
    (org-up-heading-safe)
    (apply #'org-sort-entries args)
    (goto-char origin)))

(defun mpereira/org-cycle-cycle ()
  (org-cycle)
  ;; https://www.mail-archive.com/emacs-orgmode@gnu.org/msg86779.html
  (ignore-errors
    (org-cycle)))

(defun mpereira/call-interactively-with-prefix-arg (prefix-arg func)
  (let ((current-prefix-arg prefix-arg))
    (call-interactively func)))

(defun mpereira/perspective-switch (perspective-name
                                    &optional after-perspective-creation-function)
  "TODO: docstring."
  (let ((perspective (gethash perspective-name (perspectives-hash))))
    (if perspective
        ;; Perspective already exists and is not the current.
        (when (not (equal perspective (persp-curr)))
          (persp-switch perspective-name))
      ;; Perspective doesn't exist.
      (progn
        (persp-switch perspective-name)
        (and after-perspective-creation-function
             (funcall after-perspective-creation-function perspective-name))))))

(defun mpereira/projectile-default-project-name (project-root)
  "TODO: PROJECT-ROOT docstring."
  (let* ((default-directory project-root)
         (suffix (if (file-remote-p project-root)
                     (format " @ %s" (mpereira/remote-host))
                   "")))
    (concat (file-name-nondirectory (directory-file-name default-directory))
            suffix)))

(defun mpereira/projectile-switch-project-action (project-root)
  "TODO: PROJECT-ROOT docstring."
  (let ((perspective-name (funcall
                           projectile-project-name-function
                           project-root)))
    (mpereira/perspective-switch perspective-name
                                 (lambda (perspective-name)
                                   (if (file-remote-p project-root)
                                       (let ((default-directory project-root))
                                         (mpereira/maybe-projectile-dired))
                                     (counsel-projectile-switch-project-action-dired
                                      project-root))))))

(defun mpereira/counsel-projectile-perspective-switch-project (&optional default-action)
  "TODO: docstring."
  (interactive)
  (ivy-read (projectile-prepend-project-name "Switch to project: ")
            (projectile-relevant-known-projects)
            :preselect (and (projectile-project-p)
                            (projectile-project-root))
            :action (or default-action
                        'mpereira/projectile-switch-project-action)
            :require-match t
            :sort 'ivy-prescient-sort-function
            :caller 'mpereira/counsel-projectile-perspective-switch-project))

(with-eval-after-load "ivy"
  (ivy-configure 'mpereira/counsel-projectile-perspective-switch-project
    :display-transformer-fn 'mpereira/projectile-default-project-name))

(with-eval-after-load "find-file-in-project"
  (defun mpereira/find-directory ()
    (interactive)
    (ffip-find-files "" nil t)))

(with-eval-after-load "projectile"
  (defun mpereira/maybe-projectile-dired ()
    (interactive)
    (if (projectile-project-p)
        (projectile-dired)
      (dired ".")))

  (defun mpereira/maybe-projectile-ibuffer ()
    (interactive)
    (if (projectile-project-p)
        (projectile-ibuffer nil)
      (ibuffer ".")))

  (with-eval-after-load "eshell"
    (defun mpereira/maybe-projectile-eshell ()
      (interactive)
      (if (projectile-project-p)
          (projectile-run-eshell t)
        (eshell t))))

  (with-eval-after-load "find-file-in-project"
    (with-eval-after-load "counsel-projectile"
      (defun mpereira/maybe-projectile-switch-buffer ()
        (interactive)
        (if (projectile-project-p)
            (counsel-projectile-switch-to-buffer)
          (ivy-switch-buffer)))

      (defun mpereira/maybe-projectile-find-file ()
        (interactive)
        (if (projectile-project-p)
            (counsel-projectile-find-file)
          (fast-project-find-file)))

      (defun mpereira/maybe-projectile-find-directory ()
        (interactive)
        (if (projectile-project-p)
            (counsel-projectile-find-dir)
          (mpereira/find-directory))))))

(defun mpereira/enable-line-numbers ()
  (setq display-line-numbers t))

(defun mpereira/disable-line-numbers ()
  (setq display-line-numbers nil))

(defun mpereira/maybe-enable-aggressive-indent-mode ()
  "TODO: docstring."
  (when (not (or (cl-member-if #'derived-mode-p aggressive-indent-excluded-modes)
                 (-contains? aggressive-indent-excluded-buffers (buffer-name))
                 buffer-read-only))
    (aggressive-indent-mode)))

(defun mpereira/lock-screen ()
  "TODO: docstring."
  (interactive)
  ;; TODO: make file path joining portable.
  (let ((command (concat "/System"
                         "/Library"
                         "/CoreServices"
                         "/Menu\\ Extras"
                         "/User.menu"
                         "/Contents"
                         "/Resources"
                         "/CGSession"
                         " "
                         "-suspend")))
    (shell-command command)))

(defun mpereira/epoch-at-point-to-timestamp ()
  "TODO: docstring"
  (interactive)
  (if-let (thing (counsel-symbol-at-point))
      (let* ((seconds (string-to-number thing))
             (time (seconds-to-time seconds))
             (timestamp (format-time-string "%Y-%m-%d %a %H:%M:%S" time)))
        (kill-new timestamp)
        (message timestamp)
        timestamp)))

(defun mpereira/pwd ()
  "TODO: docstring"
  (interactive)
  (let ((pwd (if (eshell-p (current-buffer))
                 (eshell/pwd)
               (buffer-file-name))))
    (kill-new pwd)
    (message pwd)
    pwd))

(defun mpereira/bm-counsel-get-list (bookmark-overlays)
  "TODO: docstring.
Arguments: BOOKMARK-OVERLAYS."
  (-map (lambda (bm)
          (with-current-buffer (overlay-buffer bm)
            (let* ((line (replace-regexp-in-string
                          "\n$"
                          ""
                          (buffer-substring (overlay-start bm)
                                            (overlay-end bm))))
                   ;; line numbers start on 1
                   (line-num (+ 1 (count-lines (point-min) (overlay-start bm))))
                   (name (format "%s:%d - %s" (buffer-name) line-num line)))
              `(,name . ,bm))))
        bookmark-overlays))

(defun mpereira/bm-counsel-find-bookmark ()
  "TODO: docstring.
Arguments: none."
  (interactive)
  (let* ((bm-list (mpereira/bm-counsel-get-list (bm-overlays-lifo-order t)))
         (bm-hash-table (make-hash-table :test 'equal))
         (search-list (-map (lambda (bm) (car bm)) bm-list)))
    (-each bm-list (lambda (bm)
                     (puthash (car bm) (cdr bm) bm-hash-table)))
    (ivy-read "Find bookmark: "
              search-list
              :require-match t
              :keymap counsel-describe-map
              :action (lambda (chosen)
                        (let ((bookmark (gethash chosen bm-hash-table)))
                          (switch-to-buffer (overlay-buffer bookmark))
                          (bm-goto bookmark)))
              :sort t)))

(defun mpereira/narrow-or-widen-dwim (p)
  "Widen if buffer is narrowed, narrow-dwim otherwise.
Dwim means: region, org-src-block, org-subtree, or defun, whichever applies
first. Narrowing to org-src-block actually calls `org-edit-src-code'.

With prefix P, don't widen, just narrow even if buffer is already narrowed."
  (interactive "P")
  (declare (interactive-only))
  (cond ((and (buffer-narrowed-p) (not p)) (widen))
        ((region-active-p)
         (narrow-to-region (region-beginning)
                           (region-end)))
        ((derived-mode-p 'org-mode)
         ;; `org-edit-src-code' is not a real narrowing command. Remove this
         ;; first conditional if you don't want it.
         (cond ((ignore-errors (org-edit-src-code) t)
                (delete-other-windows))
               ((ignore-errors (org-narrow-to-block) t))
               (t (org-narrow-to-subtree))))
        ((derived-mode-p 'latex-mode)
         (LaTeX-narrow-to-environment))
        (t (narrow-to-defun))))

(defun mpereira/uuid ()
  "Return a UUID and make it the latest kill in the kill ring."
  (interactive)
  (kill-new (format "%04x%04x-%04x-%04x-%04x-%06x%06x"
                    (random (expt 16 4))
                    (random (expt 16 4))
                    (random (expt 16 4))
                    (random (expt 16 4))
                    (random (expt 16 4))
                    (random (expt 16 6))
                    (random (expt 16 6)))))

;; TODO: make this better.
(defun mpereira/kill-last-kbd-macro ()
  "Save last executed macro definition in the kill ring."
  (let ((name (gensym "kill-last-kbd-macro-")))
    (name-last-kbd-macro name)
    (with-temp-buffer
      (insert-kbd-macro name)
      (kill-new (buffer-substring-no-properties (point-min) (point-max))))))

(defun mpereira/load-light-theme ()
  "TODO: docstring."
  (interactive)
  (counsel-load-theme-action (symbol-name mpereira/light-theme)))

(defun mpereira/load-dark-theme ()
  "TODO: docstring."
  (interactive)
  (counsel-load-theme-action (symbol-name mpereira/dark-theme)))

(defun mpereira/process-using-port ()
  "Show list of processes listening on ports via TCP.
  Copies the selected process's PID to the clipboard."
  (interactive)
  (let ((sort-fn (lambda (name candidates)
                   candidates))
        (ivy-sort-functions-alist '((t . sort-fn)))
        (candidates (split-string (shell-command-to-string
                                   "lsof -nP -iTCP | grep LISTEN")
                                  "\n"
                                  t)))
    (ivy-read "Port: "
              candidates
              :action (lambda (project-path)
                        (kill-new (cadr (split-string project-path " " t)))))))

(defun mpereira/ps ()
  "Show list of system processes.
Copies the selected process's PID to the clipboard."
  (interactive)
  (let ((ps-sort (lambda (name candidates)
                   candidates))
        (ivy-sort-functions-alist '((t . ps-sort)))
        (ps (split-string (shell-command-to-string
                           "ps axco user,pid,%cpu,%mem,start,time,command -r")
                          "\n"
                          t)))
    (ivy-read "Process: "
              ps
              :action (lambda (project-path)
                        (kill-new (cadr (split-string project-path " " t)))))))

(defun mpereira/kill-buffer-and-maybe-window ()
  "TODO."
  (interactive)
  (if (window-prev-buffers)
      (let ((previous-buffer (car (window-prev-buffers))) ; not using this.
            (current-buffer* (current-buffer)))
        (kill-buffer current-buffer*))
    (kill-buffer-and-window)))

;; TODO: make it be able to get indirect buffer file names.
(defun mpereira/file-metadata ()
  "TODO."
  (interactive)
  (let* ((fname (buffer-file-name))
         (data (file-attributes fname))
         (access (current-time-string (nth 4 data)))
         (mod (current-time-string (nth 5 data)))
         (change (current-time-string (nth 6 data)))
         (size (nth 7 data))
         (mode (nth 8 data))
         (output (format
                  "%s:

Accessed: %s
Modified: %s
Changed:  %s
Size:     %s bytes
Mode:     %s"
                  fname access mod change size mode)))
    (kill-new output)
    (message output)
    output))

(defun mpereira/buffer-project-directory (project-root-directory
                                          buffer-directory
                                          &optional max-length)
  "Returns a possibly left-truncated relative directory for a project buffer."
  (let* ((truncation-string (if (char-displayable-p ?…) "…/" ".../"))
         (relative-directory (s-chop-prefix project-root-directory buffer-directory))
         (abbreviated-directory (abbreviate-file-name relative-directory))
         (max-length (or max-length 1.0e+INF)))
    ;; If it fits, return the string.
    (if (and max-length
             (<= (string-width abbreviated-directory) max-length))
        abbreviated-directory
      ;; If it doesn't, shorten it.
      (let ((path (reverse (split-string abbreviated-directory "/")))
            (output ""))
        (when (and path (equal "" (car path)))
          (setq path (cdr path)))
        (let ((max (- max-length (string-width truncation-string))))
          ;; Concat as many levels as possible, leaving 4 chars for safety.
          (while (and path (<= (string-width (concat (car path) "/" output))
                               max))
            (setq output (concat (car path) "/" output))
            (setq path (cdr path))))
        ;; If we had to shorten, prepend …/.
        (when path
          (setq output (concat truncation-string output)))
        output))))

(defun mpereira/short-directory-path (directory &optional max-length)
  "Returns a potentially trimmed-down version of the directory DIRECTORY,
replacing parent directories with their initial characters to try to get the
character length of directory (sans directory slashes) down to MAX-LENGTH."
  (let* ((components (split-string (abbreviate-file-name directory) "/"))
         (max-length (or max-length 1.0e+INF))
         (len (+ (1- (length components))
                 (cl-reduce '+ components :key 'length)))
         (str ""))
    (while (and (> len max-length)
                (cdr components))
      (setq str (concat str
                        (cond ((= 0 (length (car components))) "/")
                              ((= 1 (length (car components)))
                               (concat (car components) "/"))
                              (t
                               (if (string= "."
                                            (string (elt (car components) 0)))
                                   (concat (substring (car components) 0 2)
                                           "/")
                                 (string (elt (car components) 0) ?/)))))
            len (- len (1- (length (car components))))
            components (cdr components)))
    (concat str (cl-reduce (lambda (a b) (concat a "/" b)) components))))

(defun mpereira/elpy-shell-clear-shell ()
  "Clear the current shell buffer."
  (interactive)
  (with-current-buffer (process-buffer (elpy-shell-get-or-create-process))
    (comint-clear-buffer)))

(defun mpereira/prevent-buffer-kill ()
  "Prevents the current buffer from being killed."
  (interactive)
  (emacs-lock-mode 'kill))

(defun mpereira/exec-path-from-shell-initialize ()
  "Clears PATH before running `exec-path-from-shell-initialize' so that there's
no duplicate or conflicting entries."
  (interactive)
  (setenv "PATH" "")
  (exec-path-from-shell-initialize))

(defun mpereira/org-todo-with-date (&optional arg)
  (interactive "P")
  (cl-letf* ((org-read-date-prefer-future nil)
             (my-current-time (org-read-date t t nil "when:" nil nil nil))
             ((symbol-function #'org-current-effective-time)
              #'(lambda () my-current-time)))
    (org-todo arg)))

(defun iso8601-date-string ()
  "TODO: docstring."
  (interactive)
  (let* ((time-zone-part (format-time-string "%z"))
         (iso8601-date-string (concat
                               (format-time-string "%Y-%m-%dT%T")
                               (substring time-zone-part 0 3)
                               ":"
                               (substring time-zone-part 3 5))))
    (message iso8601-date-string)
    (kill-new iso8601-date-string)))

Toggle buffer maximize

(defvar mpereira/toggle-buffer-maximize-window-configuration nil
  "A window configuration to return to when unmaximizing the buffer.")

(defvar mpereira/toggle-buffer-maximize-point nil
  "A point to return to when unmaximizing the buffer.")

(defvar mpereira/toggle-buffer-maximize-centered-p nil
  "Whether or not the buffer was maximixed in centered mode.")

(defun mpereira/toggle-buffer-maximize (&optional centered-p)
  "Saves the current window configuration and makes the current buffer occupy
the whole window. Calling it a second time will restore the saved window
configuration."
  (interactive)
  (if (bound-and-true-p mpereira/toggle-buffer-maximize-window-configuration)
      (progn
        (set-window-configuration mpereira/toggle-buffer-maximize-window-configuration)
        (setq mpereira/toggle-buffer-maximize-window-configuration nil)
        (goto-char mpereira/toggle-buffer-maximize-point)
        (setq mpereira/toggle-buffer-maximize-point nil)
        (when mpereira/toggle-buffer-maximize-centered-p
          (call-interactively 'olivetti-mode)
          (setq mpereira/toggle-buffer-maximize-centered-p nil)))
    (progn
      (setq mpereira/toggle-buffer-maximize-window-configuration
            (current-window-configuration))
      (setq mpereira/toggle-buffer-maximize-point (point))
      (setq mpereira/toggle-buffer-maximize-centered-p centered-p)
      (delete-other-windows)
      (when centered-p
        (call-interactively 'olivetti-mode)))))

Native compilation

(use-package emacs
  :custom
  (native-comp-async-report-warnings-errors nil))

Reload directory local variables when saving .dir-locals.el files

Taken from Stack Overflow.

(defun mpereira/reload-dir-locals-for-current-buffer ()
  "Reload directory local variables on the current buffer."
  (interactive)
  (let ((enable-local-variables :all))
    (hack-dir-local-variables-non-file-buffer)))

(defun mpereira/reload-dir-locals-for-all-buffer-in-this-directory ()
  "Reload directory local variables on every buffer with the same
`default-directory' as the current buffer."
  (interactive)
  (let ((dir default-directory))
    (dolist (buffer (buffer-list))
      (with-current-buffer buffer
        (when (equal default-directory dir))
        (mpereira/reload-dir-locals-for-current-buffer)))))

(defun mpereira/enable-autoreload-for-dir-locals ()
  (when (and (buffer-file-name)
             (equal dir-locals-file
                    (file-name-nondirectory (buffer-file-name))))
    (add-hook (make-variable-buffer-local 'after-save-hook)
              'mpereira/reload-dir-locals-for-all-buffer-in-this-directory)))

(add-hook 'emacs-lisp-mode-hook #'mpereira/enable-autoreload-for-dir-locals)

Tramp

(require 'tramp)

Disable version control on tramp buffers to avoid freezes.

(setq vc-ignore-dir-regexp
      (format "\\(%s\\)\\|\\(%s\\)"
              vc-ignore-dir-regexp
              tramp-file-name-regexp))

Don’t clean up recentf tramp buffers.

(setq recentf-auto-cleanup 'never)

Make Emacs not crazy slow under TRAMP.

Yes, this is still needed.

(defadvice projectile-project-root (around ignore-remote first activate)
  (unless (file-remote-p default-directory 'no-identification) ad-do-it))

This is supposedly faster than the default, scp.

(setq tramp-default-method "ssh")

SSH controlmaster settings are set in ~/.ssh/config.

(setq tramp-use-ssh-controlmaster-options nil)

This will put in effect PATH changes in the remote ~/.profile.

(add-to-list 'tramp-remote-path 'tramp-own-remote-path)

Store TRAMP auto-save files locally.

(setq tramp-auto-save-directory
      (expand-file-name "tramp-auto-save" user-emacs-directory))

A more representative name for this file.

(setq tramp-persistency-file-name
      (expand-file-name "tramp-connection-history" user-emacs-directory))

Cache SSH passwords during the whole Emacs session.

(setq password-cache-expiry nil)

Reuse SSH connections. Taken from the TRAMP FAQ.

Not tangled for now because it seems to affect remote LSP buffers under rust-analyzer.

[2020-08-17 Mon] Tangling this again to see if it helps with TRAMP slowness and freezes.

(customize-set-variable 'tramp-ssh-controlmaster-options
                        (concat
                         "-o ControlPath=/tmp/ssh-tramp-%%r@%%h:%%p "
                         "-o ControlMaster=auto -o ControlPersist=yes"))

counsel-tramp

(use-package counsel-tramp)

Server

(require 'server)

(unless (server-running-p)
  (server-start))

Options

;; Don't append customizations to init.el.
(setq custom-file mpereira/custom-file)
(load custom-file 'noerror)

;; Don't ask whether custom themes are safe.
(setq custom-safe-themes t)

;; Avoid loading old bytecode instead of newer source.
(setq load-prefer-newer t)

;; Automatically scroll compilation buffers to the bottom.
(setq compilation-scroll-output t)

;; Show CRLF characters.
;; http://pragmaticemacs.com/emacs/dealing-with-dos-line-endings/
(setq inhibit-eol-conversion t)

;; Enable narrowing commands.
(put 'narrow-to-region 'disabled nil)

;; Don't complain when calling `list-timers'.
(put 'list-timers 'disabled nil)

;; Show matching parens.
(setq show-paren-delay 0)
(show-paren-mode 1)

;; Disable eldoc.
(global-eldoc-mode -1)

;; Break lines automatically in "text" buffers.

(setq mpereira/auto-fill-disabled-text-modes '(yaml-mode))

(defun mpereira/maybe-enable-auto-fill-mode ()
  "TODO: docstring."
  (interactive)
  (when (not (-contains? mpereira/auto-fill-disabled-text-modes major-mode))
    (auto-fill-mode 1)))
(add-hook 'text-mode-hook 'mpereira/maybe-enable-auto-fill-mode)

;; Highlight current line.
(global-hl-line-mode t)

;; Provide undo/redo commands for window changes.
(winner-mode t)

;; Don't lock files.
(setq create-lockfiles nil)

;; Make Finder's "Open with Emacs" create a buffer in the existing Emacs frame.
(setq ns-pop-up-frames nil)

;; macOS modifiers.
(when (display-graphic-p)
  (setq mac-command-modifier 'meta)
  ;; Setting "Option" to nil allows me to type umlauts with "Option+u".
  (setq mac-option-modifier nil)
  (setq mac-control-modifier 'control)
  (setq ns-function-modifier 'hyper))

;; By default Emacs thinks a sentence is a full-stop followed by 2 spaces. Make
;; it a full-stop and 1 space.
(setq sentence-end-double-space nil)

;; Switch to help buffer when it's opened.
(setq help-window-select t)

;; Don't recenter buffer point when point goes outside window. This prevents
;; centering the buffer when scrolling down its last line.
(setq scroll-conservatively 100)

;; Keep cursor position when scrolling.
(setq scroll-preserve-screen-position 1)

(dolist (hook '(prog-mode-hook text-mode-hook))
  (add-hook hook #'mpereira/enable-line-numbers))

;; Better unique buffer names for files with the same base name.
(require 'uniquify)
(setq uniquify-buffer-name-style 'forward)

;; Remember point position between sessions.
(require 'saveplace)
(save-place-mode t)

;; Save a bunch of session state stuff.
(require 'savehist)
(setq savehist-additional-variables '(regexp-search-ring)
      savehist-autosave-interval 60
      savehist-file (expand-file-name "savehist" user-emacs-directory))
(savehist-mode t)

;; `setq', `setq-default' and `setq-local' don't seem to work with symbol
;; variables, hence the absence of a `dolist' here.
(setq-default whitespace-line-column mpereira/fill-column
              fill-column mpereira/fill-column
              comment-column mpereira/fill-column)

(setq emacs-lisp-docstring-fill-column 'fill-column)

;; UTF8 stuff.
(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)

;; Tab first tries to indent the current line, and if the line was already
;; indented, then try to complete the thing at point.
(setq tab-always-indent 'complete)

;; Make it impossible to insert tabs.
(setq-default indent-tabs-mode nil)

;; Make TABs be displayed with a width of 2.
(setq-default tab-width 2)

;; Force packages relying on this general indentation variable (e.g., lsp-mode)
;; to indent with 2 spaces.
(setq-default standard-indent 2)

;; Week start on monday.
(setq calendar-week-start-day 1)

(setq select-enable-clipboard t
      select-enable-primary t
      save-interprogram-paste-before-kill t
      apropos-do-all t
      mouse-yank-at-point t
      require-final-newline t
      save-place-file (concat user-emacs-directory "places"))

(setq display-time-world-list '(("Europe/Berlin" "Munich")
                                ("America/Sao_Paulo" "São Paulo")))

File backups

make-backup-files and auto-save-default are set to t by default.

(setq backup-directory-alist `(("." . ,(concat user-emacs-directory "file-backups"))))
(setq tramp-backup-directory-alist `(("." . ,(concat user-emacs-directory "remote-file-backups"))))
(setq auto-save-file-name-transforms `((".*" ,(concat user-emacs-directory "auto-saves") t)))

Performance

Increase the amount of data read from processes

https://emacs-lsp.github.io/lsp-mode/page/performance/

(setq read-process-output-max (* 1024 1024)) ; 1mb.

Asynchronous Org Babel tangling and byte recompilation

I have a file-local expression set at the end of the file for this. Note that the fourth argument to add-hook is important so that the hook is only installed for this file.

# Local Variables:
# eval: (add-hook 'before-save-hook 'async-literate-org-queue-run nil t)
# End:
(defcustom async-literate-org-org-file-name
  (expand-file-name "configuration.org" user-emacs-directory)
  "TODO: docstring.")

(defcustom async-literate-org-el-file-name
  (expand-file-name "configuration.el" user-emacs-directory)
  "TODO: docstring.")

(defvar async-literate-org-cached-load-path
  (list (file-name-directory (locate-library "org"))
        (file-name-directory (locate-library "ob-tangle"))))

(defcustom async-literate-org-interval-seconds 20
  "TODO: docstring."
  :group 'async-literate-org
  :type 'integer)

(defcustom async-literate-org-queue-size-limit 3
  "TODO: docstring."
  :group 'async-literate-org
  :type 'integer)

(defvar async-literate-org-requests nil)

(comment
 async-literate-org-requests
 (queue-pop 'async-literate-org-requests))

(defvar async-literate-org-timer nil)

(defun async-literate-org-disable ()
  (interactive)
  (and (timerp async-literate-org-timer)
       (cancel-timer async-literate-org-timer)))

(defun async-literate-org-enable ()
  (interactive)
  (async-literate-org-disable)
  (setq async-literate-org-timer
        (run-with-timer
         nil
         async-literate-org-interval-seconds
         (lambda ()
           (when-let ((request (queue-pop 'async-literate-org-requests)))
             (message "Starting `async-literate-org-tangle-and-byte-compile' run")
             (async-literate-org-tangle-and-byte-compile))))))

(defun async-literate-org-queue-run ()
  (interactive)
  (queue-push 'async-literate-org-requests
              'run
              async-literate-org-queue-size-limit))

(defun async-literate-org-tangle-and-byte-compile ()
  "TODO: docstring."
  (interactive)
  (let ((configuration-org-file-name async-literate-org-org-file-name)
        (async-literate-org-el-file-name async-literate-org-el-file-name)
        (org-babel-initialize 'mpereira/org-babel-initialize))
    (async-start
     `(lambda ()
        (nconc load-path ,async-literate-org-cached-load-path)

        (defalias 'org-babel-initialize
          ,(symbol-function org-babel-initialize))

        (with-output-to-string
          (require 'org)
          (require 'ob-tangle)
          (org-babel-initialize)
          (find-file ,configuration-org-file-name)
          (org-babel-tangle)
          (byte-compile-file ,async-literate-org-el-file-name)))
     `(lambda (result)
        (let ((inhibit-message t))
          (message (format (concat "`org-babel-tangle' and `byte-compile-file' called "
                                   "asynchronously for %s%s")
                           ,configuration-org-file-name
                           (if (string= "" result)
                               ""

                             (format ". output: %s" result)))))))))

Don’t save minibuffer winner-mode state

Winner mode adds this hook by default.

(remove-hook 'minibuffer-setup-hook 'winner-save-unconditionally)

Don’t show buffer remote path in minibuffer

(with-eval-after-load "ivy-rich"
  (setq ivy-rich-parse-remote-buffer nil))

Show garbage collections in minibuffer

(setq garbage-collection-messages t)

Prevent garbage collecting when opening the minibuffer

The following are set in init.el:

  • mpereira/gc-cons-percentage-maximum
  • mpereira/gc-cons-percentage-normal
  • mpereira/gc-cons-threshold-maximum
  • mpereira/gc-cons-threshold-normal

This seems to cause garbage collection when exiting the minibuffer though…

(defun mpereira/gc-cons-set-maximum ()
  (when (fboundp 'mpereira/gc-cons-threshold-maximum)
    (setq gc-cons-threshold mpereira/gc-cons-threshold-maximum)
    (setq gc-cons-percentage mpereira/gc-cons-percentage-maximum)))

(defun mpereira/gc-cons-set-normal ()
  ;; Defer it so that commands launched immediately after will enjoy the
  ;; benefits.
  (when (fboundp 'mpereira/gc-cons-threshold-maximum)
    (run-at-time
     1 nil (lambda ()
             (setq gc-cons-threshold mpereira/gc-cons-threshold-normal)
             (setq gc-cons-percentage mpereira/gc-cons-percentage-normal)))))

(add-hook 'minibuffer-setup-hook #'mpereira/gc-cons-set-maximum)
(add-hook 'minibuffer-exit-hook #'mpereira/gc-cons-set-normal)

Garbage collection magic hack

(use-package gcmh
  :config
  (gcmh-mode 1))

**Don’t** delete trailing whitespace on save

The code below is just for demonstration purposes. It is not tangled.

(add-hook 'before-save-hook #'delete-trailing-whitespace)

Make cursor movement an order of magnitude faster

From: https://emacs.stackexchange.com/questions/28736/emacs-pointcursor-movement-lag/28746

(setq auto-window-vscroll nil)

https://www.reddit.com/r/emacs/comments/gaub11/poor_scrolling_performance_in_doom_emacs/fp392eh/

(setq fast-but-imprecise-scrolling 't)
;; NOTE: setting this to `0' like it was recommended in the article above seems
;; to cause fontification to happen in real time, which can be pretty slow in
;; large buffers. Giving it a delay seems to be better.
(setq jit-lock-defer-time 0.25)

Start-up profiler: esup

(use-package esup
  :pin melpa
  :commands (esup))

explain-pause-mode

(use-package explain-pause-mode
  :disabled
  :ensure nil
  :quelpa (explain-pause-mode
           :fetcher github
           :repo "lastquestion/explain-pause-mode")
  :init
  (setq explain-pause-alert-via-message nil)
  :config
  ;; Override to use `profiler-report-profile-other-window'.
  (defun explain--profile-report-click-profile (button)
    "Click-handler when profile BUTTON is clicked in event profile report view."
    (let ((profile (button-get button 'profile)))
      (profiler-report-profile profile)))

  (add-hook 'after-init-hook #'explain-pause-mode))

Color themes

Sources:

My favorite Dark themes:

  1. modus-vivendi
  2. doom-one
  3. chocolate
  4. doom-molokai
  5. monokai
  6. material
  7. nimbus
  8. doom-Ioskvem
  9. doom-dracula
  10. srcery

My favorite light themes:

  1. modus-operandi
  2. doom-one-light
  3. doom-acario-light
  4. doom-nord-light
  5. github
  6. material-light
  7. twilight-bright
  8. espresso
(use-package material-theme :defer t)
(use-package monokai-theme :defer t)
(use-package github-theme :defer t)
(use-package srcery-theme :defer t)
(use-package nimbus-theme :defer t)
(use-package espresso-theme :defer t)
(use-package twilight-bright-theme :defer t)
(use-package modus-themes :defer t)
(use-package doom-themes
  :defer t
  :config
  (doom-themes-org-config))
(use-package tron-legacy-theme
  :ensure nil
  :defer t
  :quelpa (tron-legacy-theme
           :fetcher github
           :repo "ianpan870102/tron-legacy-emacs-theme"))
(use-package chocolate-theme
  :ensure nil
  :defer t
  :quelpa (chocolate-theme
           :fetcher github
           :repo "SavchenkoValeriy/emacs-chocolate-theme"))
(use-package vscode-dark-plus-theme)

(add-hook 'after-init-hook
          (lambda () (counsel-load-theme-action (symbol-name mpereira/initial-theme)))
          'append)

Create hook for theme change

(defvar after-load-theme-hook nil
  "Hook run after a color theme is loaded using `load-theme'.")

(defadvice load-theme (after run-after-load-theme-hook activate)
  "Run `after-load-theme-hook'."
  (run-hooks 'after-load-theme-hook))

Change themes when changing macOS light or dark appearance

(add-hook 'ns-system-appearance-change-functions
          (lambda (appearance)
            (pcase appearance
              ('light (mpereira/load-light-theme))
              ('dark (mpereira/load-dark-theme)))))

Configure Mode Line

(with-eval-after-load "projectile"
  (with-eval-after-load "eshell"
    (with-eval-after-load "magit"
      (with-eval-after-load "lsp-mode"
        (defconst mpereira/mode-line-projectile
          '(:eval
            (let ((face 'bold))
              (if (mpereira/remote-p)
                  "-"
                (when-let (project-name (projectile-project-name))
                  (concat
                   (propertize " " 'face face)
                   (propertize (format "%s" project-name) 'face face)
                   (propertize " " 'face face)))))))

        (defconst mpereira/mode-line-buffer
          '(:eval
            (let ((modified-or-ro-symbol (cond
                                          ((and buffer-file-name
                                                (buffer-modified-p))
                                           "~")
                                          (buffer-read-only ":RO")
                                          (t "")))
                  ;; Not using %b because it sometimes prepends the directory
                  ;; name.
                  (buffer-name* (file-name-nondirectory (buffer-name)))
                  (directory-face 'italic)
                  (buffer-name-face 'bold)
                  (modified-or-ro-symbol-face 'font-lock-comment-face)
                  (directory (if (mpereira/remote-p)
                                 ""
                               (let ((project-root (fast-project-find-file-project-root)))
                                 (if (and buffer-file-name project-root)
                                     (mpereira/short-directory-path
                                      (mpereira/buffer-project-directory
                                       project-root
                                       default-directory)
                                      mpereira/mode-line-max-directory-length)
                                   "")))))
              (concat
               (propertize " " 'face buffer-name-face)
               (propertize (format "%s" directory) 'face directory-face)
               (propertize (format "%s" buffer-name*) 'face buffer-name-face)
               (propertize modified-or-ro-symbol 'face modified-or-ro-symbol-face)
               (propertize " " 'face buffer-name-face)))))

        (defconst mpereira/mode-line-major-mode
          '(:eval
            (propertize " %m  " 'face 'font-lock-comment-face)))

        (defconst mpereira/mode-line-buffer-position
          '(:eval
            (unless eshell-mode
              (propertize " %p %l,%c " 'face 'font-lock-comment-face))))

        (setq-default mode-line-format (list mpereira/mode-line-projectile
                                             mpereira/mode-line-buffer
                                             mpereira/mode-line-major-mode
                                             mpereira/mode-line-buffer-position
                                             mode-line-misc-info
                                             mode-line-end-spaces))

        (defun mpereira/set-mode-line-padding ()
          (dolist (face '(mode-line mode-line-inactive))
            (let ((background (face-attribute face :background)))
              (set-face-attribute face nil :box `(:line-width 5
                                                  :color ,background)))))

        (mpereira/set-mode-line-padding)

        ;; Set modeline padding after running `load-theme'.
        (advice-add 'load-theme
                    :after
                    (lambda (&rest _)
                      (mpereira/set-mode-line-padding)))))))

Configure Header Line

(defun mpereira/set-header-line-format ()
  (interactive)
  (setq header-line-format '((which-function-mode ("" which-func-format " ")))))

(defun mpereira/clear-header-line-format ()
  (interactive)
  (setq header-line-format nil))

(setq which-func-unknown "")

;; TODO: do I want this?
;; (add-hook 'prog-mode-hook #'which-function-mode)
;; (add-hook 'prog-mode-hook #'mpereira/set-header-line-format)

Vi emulation

evil

(use-package evil
  :general
  (:keymaps '(evil-motion-state-map)
   ";" #'evil-ex
   ":" #'evil-command-window-ex)

  :init
  ;; Setup for `evil-collection'.
  (setq evil-want-integration t)
  (setq evil-want-keybinding nil)

  ;; FIXME: this correctly causes '*' to match on whole symbols (e.g., on a
  ;; Clojure file pressing '*' on 'foo.bar' matches the whole thing, instead of
  ;; just 'foo' or 'bar', BUT, it won't match 'foo.bar' in something like
  ;; '(foo.bar/baz)', which I don't like.
  (setq-default evil-symbol-word-search t)

  (setq-default evil-shift-width 2)
  (setq evil-jumps-cross-buffers nil)
  (setq evil-want-Y-yank-to-eol t)
  (setq evil-want-C-u-scroll t)
  (setq evil-search-module 'evil-search)

  ;; Prevent the cursor from moving beyond the end of line.
  (setq evil-move-cursor-back nil)
  (setq evil-move-beyond-eol nil)

  :config
  (add-hook 'after-init-hook 'evil-normalize-keymaps)

  (evil-mode t)

  ;; Don't create a kill entry on every visual movement.
  ;; More details: https://emacs.stackexchange.com/a/15054:
  (fset 'evil-visual-update-x-selection 'ignore))

evil-org

(use-package evil-org
  :after evil org
  :config
  ;; evil-org unconditionally remaps `evil-quit' to `org-edit-src-abort' which I
  ;; don't like because it results in `evil-quit' keybinding invocations to not
  ;; quit the window.
  (when (command-remapping 'evil-quit nil org-src-mode-map)
    (define-key org-src-mode-map [remap evil-quit] nil))

  (add-hook 'org-mode-hook 'evil-org-mode)
  (add-hook 'evil-org-mode-hook
            (lambda ()
              (evil-org-set-key-theme '(operators
                                        navigation
                                        textobjects)))))

evil-exchange

(use-package evil-exchange
  :after evil
  :config
  (evil-exchange-install))

evil-nerd-commenter

(use-package evil-nerd-commenter
  :after evil)

evil-surround

(use-package evil-surround
  :after evil
  :config
  (global-evil-surround-mode t))

evil-matchit

(use-package evil-matchit
  :after evil
  :config
  (global-evil-matchit-mode 1)

  ;; https://github.com/redguardtoo/evil-matchit/pull/141
  (evilmi-load-plugin-rules '(js-mode
                              json-mode
                              js2-mode
                              js3-mode
                              javascript-mode
                              rjsx-mode
                              js2-jsx-mode
                              react-mode
                              typescript-mode
                              typescript-tsx-mode
                              tsx-ts-mode)
                            '(simple javascript html)))

evil-lion

(use-package evil-lion
  :after evil
  :config
  (evil-lion-mode))

evil-string-inflection

(use-package evil-string-inflection
  :after evil)

evil-goggles

(use-package evil-goggles
  :after evil
  :config
  (evil-goggles-mode)
  (evil-goggles-use-diff-faces))

evil-multiedit

(use-package evil-multiedit
  :after evil
  :config
  (setq evil-multiedit-follow-matches t)

  ;; ;; This is so that C-n and C-p don't get mapped to `evil-multiedit-next' and
  ;; ;; `evil-multiedit-prev' respectively.
  ;; (setq evil-multiedit-dwim-motion-keys t)

  ;; Make matching case-sensitive.
  ;; https://github.com/hlissner/evil-multiedit/issues/48#issuecomment-1011418580
  (defun make-evil-multiedit-case-sensitive (fn &rest args)
    (let ((case-fold-search (not iedit-case-sensitive)))
      (apply fn args)))

  (advice-add #'evil-multiedit-match-and-next :around #'make-evil-multiedit-case-sensitive)

  (general-define-key
   :states '(normal)
   "C-RET" 'evil-multiedit-toggle-marker-here
   "RET" 'evil-multiedit-toggle-or-restrict-region
   "C-k" 'evil-multiedit-prev
   "C-j" 'evil-multiedit-next
   "C-n" 'evil-multiedit-match-and-next
   "C-p" 'evil-multiedit-match-and-prev
   "C-S-n" 'evil-multiedit-match-all)

  (general-define-key
   :states '(visual)
   "C-RET" 'evil-multiedit-toggle-marker-here
   "C-k" 'evil-multiedit-prev
   "C-j" 'evil-multiedit-next
   "C-n" 'evil-multiedit-match-symbol-and-next
   "C-p" 'evil-multiedit-match-symbol-and-prev
   "C-S-n" 'evil-multiedit-match-all)

  (general-define-key
   :states '(normal insert)
   :keymaps '(evil-multiedit-mode-map)
   "RET" 'evil-multiedit-toggle-or-restrict-region
   "C-n" 'evil-multiedit-match-symbol-and-next
   "C-p" 'evil-multiedit-match-symbol-and-prev
   "C-k" 'evil-multiedit-prev
   "C-j" 'evil-multiedit-next))

evil-owl

evil-owl-extra-posframe-args is set so that the evil-owl frame looks exactly the same as the ivy-posframe one.

(use-package evil-owl
  :after (evil ivy-posframe)
  :config
  (setq evil-owl-max-string-length 50)
  (setq evil-owl-display-method 'posframe)

  (defun mpereira/update-evil-owl-posframe-args ()
    (interactive)
    (setq evil-owl-extra-posframe-args
          `(:width 80
            :height 20
            :background-color ,(face-attribute 'ivy-posframe :background nil t)
            :foreground-color ,(face-attribute 'ivy-posframe :foreground nil t)
            :internal-border-width ,ivy-posframe-border-width
            :internal-border-color ,(face-attribute 'ivy-posframe-border
                                                    :background
                                                    nil
                                                    t))))

  ;; This needs to run after the initial theme load.
  (add-hook 'after-init-hook 'mpereira/update-evil-owl-posframe-args 'append)
  (add-hook 'after-load-theme-hook 'mpereira/update-evil-owl-posframe-args)

  (evil-owl-mode))

evil-collection

(use-package evil-collection
  :after evil
  :config
  (condition-case err
      (evil-collection-init)
    (error (message "Error initializing evil-collection-init: %S" err))))

Org

org-mode

(setq org-directory (expand-file-name "org" mpereira/cloud-synced-directory))

(setq org-modules '(org-habit
                    org-info
                    org-protocol
                    org-tempo))
;; Requiring these modules because org mode only does that for `org-modules'
;; defined prior to loading it.
(require 'org-habit)
(require 'org-protocol)
(require 'org-tempo)

(add-hook 'org-mode-hook
          (lambda ()
            (setq-local electric-pair-inhibit-predicate
                        `(lambda (c)
                           (if (char-equal c ?<) t (,electric-pair-inhibit-predicate c))))))

;; Pretty ellipsis.
(setq org-ellipsis "")

(setq org-log-done 'time)

;; Indent content at the outline level.
(setq org-adapt-indentation t)

(setq org-image-actual-width 640)

;; When this is set to `nil':
;; - `org-insert-heading' will insert a heading *before* the current heading.
;; - `org-insert-heading-after-current' will insert a heading *after* the
;;   current heading.
(setq org-insert-heading-respect-content nil)

;; TODO: is this needed?
(setq org-catch-invisible-edits 'show)

;; Show empty line between collapsed trees if they are separated by just 1
;; line break.
(setq org-cycle-separator-lines 1)

(setq org-attach-auto-tag "attachment")

(add-hook 'org-mode-hook #'mpereira/disable-line-numbers)

(setq org-tags-column -80)

;; FIXME: don't use hard-coded color.
;; (face-spec-set 'org-tag '((t :box (:color "gray30" :line-width 1))))

;; Don't ask when trying to edit a src block with an existing buffer.
(setq org-src-ask-before-returning-to-edit-buffer nil)

;; Don't indent src block content.
(setq org-edit-src-content-indentation 0)

;; Don't close all other windows when exiting the src buffer.
(setq org-src-window-setup 'current-window)

;; Open indirect buffer in the same window as the src buffer.
(setq org-indirect-buffer-display 'current-window)

;; Fontify code in code blocks.
(setq org-src-fontify-natively t)

;; Make TAB act as if it were issued in a buffer of the language’s major mode.
(setq org-src-tab-acts-natively t)

(setq org-todo-keywords '((sequence "TODO(t!)"
                                    "DOING(d!)"
                                    "NEXT(n!)"
                                    "WAITING(w@/!)"
                                    "|"
                                    "SOMEDAY(s@/!)"
                                    "DONE(D!)"
                                    "CANCELLED(c@/!)")))

(setq org-capture-templates
      '(("t" "To-do" entry
         (file "inbox.org")
         "* TODO %i%?")
        ("c" "Calendar" entry
         (file mpereira/org-calendar-file)
         "* %i%?\n  :PROPERTIES:\n  :calendar-id: %(caar mpereira/secret-org-gcal-file-alist)\n  :END:\n:org-gcal:\n%^{When?}t\n:END:")
        ("a" "Appointment" entry
         (file "appointments.org")
         "* %i%?\n  %^{When?}t")
        ("j" "Journal for today" entry
         (file+olp+datetree "journal.org" "Journal")
         "* %U %^{Title}\n  %?"
         :tree-type week
         :empty-lines-after 1)
        ("p" "Web page" entry
         (file+datetree "~/org/cpb.org")
         "* %(org-web-tools--org-link-for-url) :website:

%U %?" :clock-in t :clock-resume t :empty-lines 1)
        ("J" "Journal for some other day" entry
         (file+olp+datetree "journal.org" "Journal")
         "* %(format-time-string \"[%Y-%m-%d \\%a %H:%M]\") %^{Title}\n  %?"
         :tree-type week
         :time-prompt t)))

;; Start org note and capture buffers in insert state.
(add-hook 'org-log-buffer-setup-hook #'evil-insert-state)
(add-hook 'org-capture-mode-hook #'evil-insert-state)

;; Only refile to a few files.
(setq mpereira/org-refile-files
      (-map (lambda (file-name)
              (expand-file-name file-name mpereira/org-directory))
            '("blog.org"
              "life.org"
              "projects.org"
              "work.org")))

(setq org-refile-targets '((mpereira/org-refile-files :maxlevel . 1)))

(setq org-outline-path-complete-in-steps nil)
(setq org-refile-allow-creating-parent-nodes 'confirm)
(setq org-refile-use-cache t)
(setq org-refile-use-outline-path 'file)

;; `org-reverse-note-order' set to true along with the two following hooks gets
;; us two things after refiling:
;; 1. Line breaks between top-level headings are maintained.
;; 2. Entries are sorted and top-level heading visibility is set to CHILDREN.
(setq org-reverse-note-order t)

(add-hook 'org-after-refile-insert-hook
          (lambda ()
            (interactive)
            (mpereira/org-sort-parent-entries nil ?o)))

(add-hook 'org-after-sorting-entries-or-items-hook #'mpereira/org-cycle-cycle)

;; Save org buffers after some operations.
(dolist (hook '(org-refile
                org-agenda-add-note
                org-agenda-deadline
                org-agenda-kill
                org-agenda-refile
                org-agenda-schedule
                org-agenda-set-property
                org-agenda-set-tags))
  ;; https://github.com/bbatsov/helm-projectile/issues/51
  (advice-add hook :after (lambda (&rest _) (org-save-all-org-buffers))))

(defun mpereira/org-unfill-toggle ()
  "Toggle filling/unfilling of the current region, or current paragraph if no
region active."
  (interactive)
  (let (deactivate-mark
        (fill-column
         (if (eq last-command this-command)
             (progn (setq this-command nil)
                    most-positive-fixnum)
           fill-column)))
    (call-interactively 'org-fill-paragraph)))

(defun mpereira/org-insert-heading ()
  "`org-insert-heading' will break the current heading unless the pointer is at
the beginning of the line. This fixes that."
  (interactive)
  (move-beginning-of-line nil)
  (org-insert-heading))

(general-define-key
 :keymaps '(org-mode-map)
 :states '(visual)
 "C-n" 'evil-multiedit-match-and-next
 "C-p" 'evil-multiedit-match-and-prev)

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal)
 "t" 'org-todo
 "T" 'mpereira/org-insert-heading
 "M-t" 'org-insert-heading-after-current
 "(" 'org-up-element
 ")" 'org-down-element
 "k" 'evil-previous-visual-line
 "j" 'evil-next-visual-line
 "C-S-h" 'org-metaleft
 "C-S-j" 'org-metadown
 "C-S-k" 'org-metaup
 "C-S-l" 'org-metaright
 ;; TODO: make this call `org-babel-next-src-block' if there are no
 ;; sibling headings.
 "C-j" 'org-forward-heading-same-level
 ;; TODO: make this call `org-babel-previous-src-block' if there are
 ;; no sibling headings.
 "C-k" 'org-backward-heading-same-level
 ;; TODO: remove temporary keybinding.
 "C-n" 'org-babel-next-src-block
 ;; TODO: remove temporary keybinding.
 "C-p" 'org-babel-previous-src-block
 ;; TODO: add binding for `org-down-element'. Lisp analogous:
 ;; `lispyville-next-opening'.
 )

;; org source blocks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun mpereira/maybe-org-edit-src-save ()
  (interactive)
  (if (buffer-modified-p)
      (org-edit-src-save)
    (message "(No changes need to be saved)")))

(general-define-key
 :states '(normal visual)
 :keymaps '(org-src-mode-map)
 :prefix mpereira/leader
 ;; Originally bound to `save-buffer' via the global keymap.
 "w" 'mpereira/maybe-org-edit-src-save
 ;; Originally bound to `org-edit-src-abort'.
 ;; FIXME: doesn't seem to be working?
 "q" 'evil-quit)

;; org capture buffer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :states '(normal visual)
 :keymaps '(org-capture-mode-map)
 :prefix mpereira/leader
 ;; Originally bound to `save-buffer' via the global keymap.
 "or" 'org-capture-refile)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal visual)
 :prefix mpereira/leader
 "c" (lambda ()
       (interactive)
       (org-clone-subtree-with-time-shift 1)))

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "f"
 "o" 'counsel-org-goto)

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "e"
 "e" 'org-babel-execute-src-block)

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal visual)
 "gq" 'mpereira/org-unfill-toggle)

(general-define-key
 :keymaps '(org-mode-map text-mode-map)
 :states '(normal visual insert)
 "M-q" 'mpereira/org-unfill-toggle)

(general-define-key
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "o"
 "a" #'mpereira/open-or-build-main-org-agenda
 "A" #'mpereira/open-or-build-review-org-agenda
 "c" 'counsel-org-capture
 "Ci" 'org-clock-in
 "Co" 'org-clock-out
 "Cg" 'org-clock-goto
 "D" 'org-check-deadlines
 "l" 'org-store-link)

(general-define-key
 :keymaps '(org-mode-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "o"
 "!" 'org-time-stamp-inactive
 "." 'org-time-stamp
 "/" 'org-search-view
 "\\" '(lambda ()
         (interactive)
         (mpereira/call-interactively-with-prefix-arg
          '(4)
          'org-tags-sparse-tree))
 "|" 'org-columns
 "Cc" 'org-clock-cancel
 "Cd" 'org-clock-display
 "Ci" 'org-clock-in
 "Cl" 'org-clock-in-last
 "Co" 'org-clock-out
 "d" 'org-deadline
 "D" 'org-archive-hierarchically
 "b" (lambda ()
       (interactive)
       (mpereira/call-interactively-with-prefix-arg
        '(4)
        'org-tree-to-indirect-buffer))
 "B" 'outline-show-branches
 "f" 'org-attach
 "i" 'org-insert-link
 "d" 'org-cut-subtree
 "n" 'org-add-note
 "p" 'org-insert-link ; "p" for "paste".
 "P" 'org-priority
 "r" 'org-refile
 "X" (lambda ()
       (interactive)
       (mpereira/call-interactively-with-prefix-arg
        '(4) 'org-babel-remove-result-one-or-many))
 "Rd" (lambda ()
        (interactive)
        (mpereira/call-interactively-with-prefix-arg
         '(4) 'org-deadline))
 "Rs" (lambda ()
        (interactive)
        (mpereira/call-interactively-with-prefix-arg
         '(4) 'org-schedule))
 "s" 'org-schedule
 "S" 'org-sort-entries
 "t" 'counsel-org-tag
 "u" 'org-toggle-link-display
 "w" 'org-web-tools-insert-web-page-as-entry
 "x" 'org-export-dispatch
 "y" 'org-store-link
 "Y" 'org-copy-subtree)

(general-define-key
 :keymaps '(org-columns-map)
 "s" (lambda ()
       (interactive)
       (org-columns-quit)
       (org-sort-entries nil ?r)
       (org-columns)))

Org Babel

verb

(use-package verb
  :config
  (setq tempo-template-org-verb '("#+begin_src verb :wrap src ob-verb-response"
                                  nil '> n p n
                                  "#+end_src" >))
  (add-to-list 'org-tempo-tags '("<h" . tempo-template-org-verb)))

org-babel

(defun mpereira/org-babel-initialize ()
  "TODO: docstring."
  (org-babel-do-load-languages 'org-babel-load-languages
                               '((shell . t)
                                 (emacs-lisp . t)
                                 (python . t)
                                 (verb . t)))

  (setq org-confirm-babel-evaluate nil)

  ;; By default, don't evaluate src blocks when exporting.
  (setq org-export-use-babel nil)

  ;; REVIEW: doing this causes :e to load the whole file contents into the src
  ;; block buffer.
  ;; (defadvice org-edit-src-code (around set-buffer-file-name activate compile)
  ;;   (let ((file-name (buffer-file-name)))
  ;;     ad-do-it
  ;;     (setq buffer-file-name file-name)))
  )

(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)

(mpereira/org-babel-initialize)

Prevent o/O (evil-open-below/above) from scrolling window

It calls indent-according-to-mode which does the undesired scrolling.

emacs-evil/evil#1068

(defun mpereira/evil-open-no-auto-indent (oldfun arg)
  (if (and evil-auto-indent
           (eq major-mode 'org-mode))
      (let ((evil-auto-indent nil))
        (funcall oldfun arg))
    (funcall oldfun arg)))

(advice-add #'evil-open-above :around #'mpereira/evil-open-no-auto-indent)
(advice-add #'evil-open-below :around #'mpereira/evil-open-no-auto-indent)

Align all tags in the buffer on tag changes

(defun mpereira/org-align-all-tags ()
  "Aligns all org tags in the buffer."
  (interactive)
  (when (eq major-mode 'org-mode)
    (org-align-tags t)))

(add-hook 'org-after-tags-change-hook #'mpereira/org-align-all-tags)

Paste images in the clipboard directly into org buffers

(defun mpereira/org-paste-clipboard-image ()
  "TODO: docstring."
  (interactive)
  (if (executable-find "pngpaste")
      (let ((image-file (concat temporary-file-directory
                                (make-temp-name "org-image-paste-")
                                ".png")))
        (call-process-shell-command (concat "pngpaste " image-file))
        (insert (concat  "#+CAPTION: " (read-string "Caption: ") "\n"))
        (insert (format "[[file:%s]]" image-file))
        (org-display-inline-images))
    (message "Requires pngpaste in PATH")))

Sort org entries by multiple properties

I have org trees for projects which I like sorted by:

PriorityOrderProperty
1ascTODO
2ascPRIORITY
3ascALLTAGS
4descCLOSED
5descCREATED
6ascITEM

I get that with M-x mpereira/org-sort-entries.

(defun mpereira/todo-to-int (todo)
  "Returns incrementally bigger integers for todo values.

Example: | todo  | int |
         |-------+-----|
         | TODO  |   0 |
         | DOING |   1 |
         | DONE  |   2 |"
  (first (-non-nil
          (mapcar (lambda (keywords)
                    (let ((todo-seq
                           (-map (lambda (x) (first (split-string  x "(")))
                                 (rest keywords))))
                      (cl-position-if (lambda (x) (string= x todo)) todo-seq)))
                  org-todo-keywords))))

(defun mpereira/todo-to-int-fixed (todo)
  "TODO: TODO docstring."
  (cdr (assoc todo '((DOING . 0)
                     (NEXT . 1)
                     (WAITING . 2)
                     (TODO . 3)
                     (SOMEDAY . 4)
                     (DONE . 5)
                     (CANCELLED . 6)))))

(defun mpereira/escape-string (s)
  "Makes strings safe to be printed with `message'."
  (s-replace-all '(("%" . "%%")) s))

(defun mpereira/org-todo-completed? (todo)
  (or (string= "DONE" todo)
      (string= "CANCELLED" todo)))

(defun mpereira/org-sort-key ()
  "Returns a sort key for an org entry based on:

| Priority | Order | Property |
|----------+-------+----------|
|        1 | asc   | TODO     |
|        2 | asc   | PRIORITY |
|        3 | asc   | ALLTAGS  |
|        4 | desc  | CLOSED   |
|        5 | desc  | CREATED  |
|        6 | asc   | ITEM     |

if they aren't DONE or CANCELLED. In that case the sort key disregards tags,
giving priority to CREATED:

| Priority | Order | Property |
|----------+-------+----------|
|        1 | asc   | TODO     |
|        2 | asc   | PRIORITY |
|        4 | desc  | CLOSED   |
|        5 | desc  | CREATED  |
|        6 | asc   | ITEM     |
"
  (interactive)
  (let* ((todo-max (apply #'max (mapcar #'length org-todo-keywords)))
         (todo (org-entry-get (point) "TODO"))
         (todo-int (if (and todo (mpereira/todo-to-int-fixed (intern todo)))
                       (mpereira/todo-to-int-fixed (intern todo))
                     todo-max))
         (priority (org-entry-get (point) "PRIORITY"))
         (priority-int (if priority (string-to-char priority) org-default-priority))
         (date-int-min 10000000000000) ; YYYY=1000 mm=00 dd=00 HH=00 MM=00 SS=00
         (date-int-max 30000000000000) ; YYYY=3000 mm=00 dd=00 HH=00 MM=00 SS=00
         (closed (org-entry-get (point) "CLOSED"))
         (closed-int (if closed
                         (string-to-number
                          (ts-format "%Y%m%d%H%M%S" (ts-parse-org closed)))
                       date-int-min))
         (created (org-entry-get (point) "CREATED"))
         (created-int (if created
                          (string-to-number
                           (ts-format "%Y%m%d%H%M%S" (ts-parse-org created)))
                        date-int-min))
         (alltags-default "zzzzzzzzzz")
         (alltags (or (org-entry-get (point) "ALLTAGS")
                      alltags-default))
         (item (org-entry-get (point) "ITEM"))
         (sort-key (format "%03d %03d %s %.10f %.10f %s"
                           todo-int
                           priority-int
                           (if (mpereira/org-todo-completed? todo)
                               alltags-default
                             alltags)
                           (/ (float date-int-max) closed-int)
                           (/ (float date-int-max) created-int)
                           (mpereira/escape-string item))))
    sort-key))

(defun mpereira/org-sort-entries ()
  "Sorts child entries based on `mpereiera/ort-sort-key'."
  (interactive)
  (save-excursion
    (org-sort-entries nil ?f #'mpereira/org-sort-key)))

Org clock

;; org-clock stuff.
(setq org-clock-idle-time 15)
(setq org-clock-mode-line-total 'current)
;; Maybe automatically switching to DOING is not the best idea. Leaving it
;; commented for now.
;; (setq org-clock-in-switch-to-state "DOING")

;; Resume clocking task when emacs is restarted.
(org-clock-persistence-insinuate)
;; Save the running clock and all clock history when exiting Emacs, load it on
;; startup.
(setq org-clock-persist t)
;; Resume clocking task on clock-in if the clock is open.
(setq org-clock-in-resume t)
;; Do not prompt to resume an active clock, just resume it.
(setq org-clock-persist-query-resume nil)
;; Clock out when moving task to a done state.
(setq org-clock-out-when-done t)
;; Include current clocking task in clock reports.
(setq org-clock-report-include-clocking-task t)
;; Use pretty things for the clocktable.
(setq org-pretty-entities nil)

org-gcal

(use-package org-gcal
  :config
  (setq mpereira/org-gcal-directory (expand-file-name "gcal" org-directory))

  (load-file (expand-file-name "org-gcal-secrets.el" user-emacs-directory))

  (setq org-gcal-client-id mpereira/secret-org-gcal-client-id)
  (setq org-gcal-client-secret mpereira/secret-org-gcal-client-secret)
  (setq org-gcal-file-alist mpereira/secret-org-gcal-file-alist)
  (setq org-gcal-auto-archive nil)
  (setq org-gcal-notify-p nil))

Org agenda

(require 'org-agenda)

(setq org-agenda-files (list org-directory
                             mpereira/org-gcal-directory))

;; Full screen org-agenda.
;; NOTE: this also makes stuff like `org-search-view' full screen.
(setq org-agenda-window-setup 'only-window)

;; Don't destroy window splits.
(setq org-agenda-restore-windows-after-quit t)

;; Show only the current instance of a repeating timestamp.
(setq org-agenda-repeating-timestamp-show-all nil)

;; Don't look for free-form time string in headline.
(setq org-agenda-search-headline-for-time nil)

(setq org-agenda-tags-column (* -1 mpereira/org-agenda-width))

(setq org-agenda-format-date 'mpereira/org-agenda-format-date)

;; Redo agenda after capturing.
(add-hook 'org-capture-after-finalize-hook 'org-agenda-maybe-redo)

;; Don't show empty agenda sections.
(add-hook 'org-agenda-finalize-hook #'mpereira/org-agenda-delete-empty-blocks)

;; Disable `evil-lion-mode' so that "g" keeps the mapping to
;; `org-agenda-maybe-redo'.
(add-hook 'org-agenda-finalize-hook (lambda () (evil-lion-mode -1)))

(defun mpereira/org-gcal-entry-at-point-p ()
  (when-let ((link (org-entry-get (point) "LINK")))
    (string-match "Go to gcal web page" link)))

(evil-set-initial-state 'org-agenda-mode 'normal)

(general-define-key
 :keymaps '(org-agenda-mode-map)
 :states '(normal emacs)
 "/" 'org-agenda-filter-by-regexp
 "<" #'org-agenda-filter-by-category
 "c" (lambda ()
       (interactive)
       ;; When capturing to a calendar org-gcal sends a network request that
       ;; reorders the calendar headings on completion, causing them to have a
       ;; different order than the agenda entries. Here we install a buffer
       ;; local hook that will sync the agenda entries with the calendar
       ;; headings.
       (add-hook 'org-capture-after-finalize-hook
                 (lambda ()
                   (interactive)
                   (run-at-time mpereira/org-gcal-request-timeout
                                nil
                                #'org-agenda-maybe-redo))
                 nil
                 t)
       (org-agenda-capture))
 "d" #'org-agenda-deadline
 "f" #'org-attach
 "F" #'org-gcal-sync
 "g" #'mpereira/build-org-agenda
 "h" nil
 "i" #'org-agenda-clock-in
 "j" #'org-agenda-next-item
 "k" #'org-agenda-previous-item
 "l" nil
 "o" #'org-agenda-clock-out
 "n" #'org-agenda-add-note
 "q" #'org-agenda-quit
 "r" #'org-agenda-refile
 "s" #'org-agenda-schedule
 "q" #'mpereira/close-org-agenda
 "t" #'org-agenda-todo
 "T" #'org-agenda-set-tags
 "u" #'org-agenda-undo
 "w" nil
 "x" (lambda ()
       (interactive)
       (save-window-excursion
         (let ((agenda-buffer (current-buffer)))
           (org-agenda-goto)
           (if (mpereira/org-gcal-entry-at-point-p)
               (progn
                 (org-gcal-delete-at-point)
                 ;; org-gcal only removes the calendar headings after the
                 ;; network request finishes.
                 (run-at-time mpereira/org-gcal-request-timeout
                              nil
                              #'org-agenda-maybe-redo))
             (progn
               (quit-window)
               (org-agenda-kill))))))
 "C-j" #'org-agenda-next-item
 "C-k" #'org-agenda-previous-item
 "C-f" #'scroll-up-command
 "C-b" #'scroll-down-command)

(defmacro calendar-action (func)
  `(lambda ()
     "TODO: docstring."
     (interactive)
     (org-eval-in-calendar #'(,func 1))))

;; TODO: programmatically sync this with `calendar-mode-map' instead of
;; hard-coding keybindings.
(general-define-key
 :keymaps '(org-read-date-minibuffer-local-map)
 "q" 'minibuffer-keyboard-quit
 "h" (calendar-action calendar-backward-day)
 "l" (calendar-action calendar-forward-day)
 "k" (calendar-action calendar-backward-week)
 "j" (calendar-action calendar-forward-week)
 "{" (calendar-action calendar-backward-month)
 "}" (calendar-action calendar-forward-month)
 "[" (calendar-action calendar-backward-year)
 "]" (calendar-action calendar-forward-year)
 "(" (calendar-action calendar-beginning-of-month)
 ")" (calendar-action calendar-end-of-month)
 "0" (calendar-action calendar-beginning-of-week)
 "$" (calendar-action calendar-end-of-week))

My custom persistent (cached) org agendas

My agendas are a bit heavy to build so I don’t kill their buffers (I use set-window-configuration instead to go back to the window configuration state right before opening the agenda, which is bound to q). I have keybindings (<leader> O a for the main agenda and <leader> O A for the review agenda) that display an existing agenda buffer, or build and display a fresh agenda buffer.

I’m also planning to add automatic and periodic background refreshing (and perhaps exporting) of the agenda buffers with run-with-idle-timer soon.

Agenda library

These are functions that I use in the actual custom agenda definitions.

(defun mpereira/org-current-subtree-state-p (state)
  (string= state (org-get-todo-state)))

(defun mpereira/org-up-heading-top-level ()
  "Move to the top level heading."
  (while (not (= 1 (org-outline-level)))
    (org-up-heading-safe)))

(defun mpereira/org-skip-all-but-first ()
  "Skip all but the first non-done entry."
  (let (should-skip-entry)
    (unless (mpereira/org-current-subtree-state-p "TODO")
      (setq should-skip-entry t))
    (save-excursion
      (while (and (not should-skip-entry) (org-goto-sibling t))
        (when (mpereira/org-current-subtree-state-p "TODO"))
        (setq should-skip-entry t)))
    (when should-skip-entry
      (or (outline-next-heading)
          (goto-char (point-max))))))

(defun mpereira/org-skip-subtree-if-habit ()
  "Skip an agenda entry if it has a STYLE property equal to \"habit\"."
  (let ((subtree-end (save-excursion (org-end-of-subtree t))))
    (if (string= (org-entry-get nil "STYLE") "habit")
        subtree-end
      nil)))

(defun mpereira/org-skip-subtree-unless-habit ()
  "Skip an agenda entry unless it has a STYLE property equal to \"habit\"."
  (let ((subtree-end (save-excursion (org-end-of-subtree t))))
    (if (string= (org-entry-get nil "STYLE") "habit")
        nil
      subtree-end)))

(defun mpereira/org-skip-inbox ()
  "Skip agenda entries coming from the inbox."
  (let ((subtree-end (save-excursion (org-end-of-subtree t))))
    (if (string= (org-get-category) "inbox")
        subtree-end
      nil)))

(defun mpereira/org-skip-someday-projects-subheadings ()
  "Skip agenda entries under a project with state \"SOMEDAY\"."
  (let ((subtree-end (save-excursion (org-end-of-subtree t))))
    (mpereira/org-up-heading-top-level)
    (if (mpereira/org-current-subtree-state-p "SOMEDAY")
        subtree-end
      nil)))

(defun mpereira/org-entry-at-point-get (property)
  (org-entry-get (point) property))

(defun mpereira/org-entry-parent-root-heading ()
  "Returns the root heading for the entry at point. Makes the root heading
available in the kill ring if called interactively.

For example, in an org file like

* Emacs
** TODO Periodically refresh org agenda

the \"parent root heading\" for the TODO entry would be \"Emacs\".
the \"parent root heading\" for the \"Emacs\" entry would be nil.
"
  (interactive)
  (let* ((outline-path (condition-case err
                           (org-get-outline-path t)
                         (error
                          (message "Error calling `org-get-outline-path' with heading (%s): %s"
                                   (org-get-heading)
                                   (error-message-string err))
                          "?")))
         (parent-heading-name (when (< 1 (length outline-path))
                                (car outline-path))))
    (when (and parent-heading-name
               (called-interactively-p 'any))
      (kill-new parent-heading-name))
    ;; `concat' turns nil into an empty string.
    (concat parent-heading-name)))

(defun mpereira/timestamp-type ()
  (interactive)
  (cond
   ((mpereira/org-entry-at-point-get "DEADLINE") "Deadline")
   ((mpereira/org-entry-at-point-get "SCHEDULED") "Scheduled")
   ((mpereira/org-entry-at-point-get "TIMESTAMP") "Timestamp")
   ((mpereira/org-entry-at-point-get "TIMESTAMP_IA") "Timestamp (inactive)")))

(defun mpereira/org-agenda-tags-prefix-format ()
  "Used in the \"tags\" section of the main org agenda.

This function is only necessary because multiple EXPRESSIONs would be required
to achieve the same outcome just with a single `org-agenda-prefix-format', and
that's not allowed."
  (interactive)
  (let* ((timestamp (or (mpereira/org-entry-at-point-get "DEADLINE")
                        (mpereira/org-entry-at-point-get "SCHEDULED")
                        (mpereira/org-entry-at-point-get "TIMESTAMP")))
         (current (calendar-date-string (calendar-current-date)))
         (days (time-to-number-of-days (time-subtract
                                        (org-read-date nil t timestamp)
                                        (org-read-date nil t current))))
         (date (format-time-string "%d %b" (org-read-date t t timestamp))))
    (concat (format "%-20s"
                    (s-truncate 18
                                (mpereira/org-entry-parent-root-heading)
                                ""))
            (format "%11s: " (mpereira/timestamp-type))
            " "
            (format "%6s" (format "In %dd" days))
            " "
            (format "%8s" (format "(%s)" date)))))

(defun mpereira/org-agenda-format-date (date)
  "Format a DATE string for display in the daily/weekly agenda.
This function makes sure that dates are aligned for easy reading."
  (let* ((dayname (calendar-day-name date))
         (day (cadr date))
         (day-of-week (calendar-day-of-week date))
         (month (car date))
         (monthname (calendar-month-name month))
         (year (nth 2 date)))
    (format "\n%-9s %2d %s"
            dayname day monthname year)))

(defun mpereira/yesterday ()
  (time-subtract (current-time) (days-to-time 1)))

(defun mpereira/time-to-calendar-date (time)
  (let* ((decoded-time (decode-time time))
         (day (nth 3 decoded-time))
         (month (nth 4 decoded-time))
         (year (nth 5 decoded-time)))
    (list month day year)))

(defun mpereira/format-calendar-date-Y-m-d (calendar-date)
  (format-time-string "%Y-%m-%d"
                      (mpereira/calendar-date-to-time calendar-date)))

(defun mpereira/format-calendar-date-d-m-Y (calendar-date)
  (format-time-string "%d %B %Y"
                      (mpereira/calendar-date-to-time calendar-date)))

(defun mpereira/calendar-date-to-time (calendar-date)
  (let* ((day (calendar-extract-day calendar-date))
         (month (calendar-extract-month calendar-date))
         (year (calendar-extract-year calendar-date)))
    (encode-time 0 0 0 day month year)))

(defun mpereira/calendar-read-date (string)
  (mpereira/time-to-calendar-date (org-read-date t t string)))

(defun mpereira/org-agenda-date-week-start (string)
  "Returns the first day of the week at DATE."
  (let* ((calendar-date (mpereira/calendar-read-date string)))
    (mpereira/format-calendar-date-Y-m-d
     (mpereira/time-to-calendar-date
      (time-subtract
       (mpereira/calendar-date-to-time calendar-date)
       (days-to-time (if (zerop (calendar-day-of-week calendar-date))
                         6 ;; magic.
                       (- (calendar-day-of-week calendar-date)
                          calendar-week-start-day))))))))

(defun mpereira/org-agenda-date-week-end (string)
  "Returns the last day of the week at DATE."
  (let* ((calendar-date (mpereira/calendar-read-date string)))
    (if (= (calendar-week-end-day) (calendar-day-of-week calendar-date))
        string
      (mpereira/format-calendar-date-Y-m-d
       (mpereira/time-to-calendar-date
        (time-add
         (mpereira/calendar-date-to-time calendar-date)
         (days-to-time (- 7 (calendar-day-of-week calendar-date)))))))))

(defun mpereira/org-agenda-review-prefix-format ()
  (let* ((timestamp (or (mpereira/org-entry-at-point-get "CLOSED")
                        (mpereira/org-entry-at-point-get "DEADLINE")
                        (mpereira/org-entry-at-point-get "TIMESTAMP")
                        (mpereira/org-entry-at-point-get "TIMESTAMP_IA")
                        (mpereira/org-entry-at-point-get "SCHEDULED")))
         (calendar-date (mpereira/calendar-read-date timestamp)))
    (format "%-20s  %s"
            (s-truncate 18 (mpereira/org-entry-parent-root-heading) "")
            (mpereira/format-calendar-date-Y-m-d calendar-date))))

(defun mpereira/org-agenda-review-search (start end)
  (concat "CLOSED>=\"<" start ">\""
          "&"
          "CLOSED<=\"<" end ">\""
          "|"
          "TIMESTAMP_IA>=\"<" start ">\""
          "&"
          "TIMESTAMP_IA<=\"<" end ">\""
          "|"
          "TIMESTAMP>=\"<" start ">\""
          "&"
          "TIMESTAMP<=\"<" end ">\""))

;; https://lists.gnu.org/archive/html/emacs-orgmode/2015-06/msg00266.html
(defun mpereira/org-agenda-delete-empty-blocks ()
  "Remove empty agenda blocks.
A block is identified as empty if there are fewer than 2 non-empty
lines in the block (excluding the line with
`org-agenda-block-separator' characters)."
  (when org-agenda-compact-blocks
    (user-error "Cannot delete empty compact blocks"))
  (setq buffer-read-only nil)
  (save-excursion
    (goto-char (point-min))
    (let* ((blank-line-re "^\\s-*$")
           (content-line-count (if (looking-at-p blank-line-re) 0 1))
           (start-pos (point))
           (block-re (if (stringp org-agenda-block-separator)
                         org-agenda-block-separator
                       (format "%c\\{10,\\}" org-agenda-block-separator))))
      (while (and (not (eobp)) (forward-line))
        (cond
         ((looking-at-p block-re)
          (when (< content-line-count 2)
            (delete-region start-pos (1+ (point-at-bol))))
          (setq start-pos (point))
          (forward-line)
          (setq content-line-count (if (looking-at-p blank-line-re) 0 1)))
         ((not (looking-at-p blank-line-re))
          (setq content-line-count (1+ content-line-count)))))
      (when (< content-line-count 2)
        (delete-region start-pos (point-max)))
      (goto-char (point-min))
      ;; The above strategy can leave a separator line at the beginning of the
      ;; buffer.
      (when (looking-at-p block-re)
        (delete-region (point) (1+ (point-at-eol))))))
  (setq buffer-read-only t))

Main agenda

(defvar mpereira/main-org-agenda-buffer-name "*Main Org Agenda*"
  "The name of the main org agenda.")

(defvar mpereira/main-org-agenda-last-built nil
  "The last time the main org agenda was built.")

(defvar mpereira/main-org-agenda-previous-window-configuration nil
  "A window configuration to return to when closing the main org agenda.")

(defvar mpereira/main-org-agenda-previous-point nil
  "A point to return to when closing the main org agenda.")

(defun mpereira/build-main-org-agenda ()
  "Build and display the main org agenda."
  (interactive)
  ;; Remember that EXPRESSION (e.g. "%(foo)") can be used only once per
  ;; `org-agenda-prefix-format'.
  (let* ((todo-prefix-format
          (concat "  "
                  ;; CATEGORY property or file name.
                  "%-10c"
                  " "
                  ;; Truncated root heading.
                  "%-20(s-truncate 18 (mpereira/org-entry-parent-root-heading) \"\")"
                  " "
                  ;; Time of day specification.
                  "%?-12t"
                  " "
                  ;; Scheduling/Deadline information.
                  "%-12s"))
         (tags-prefix-format
          (concat "  "
                  ;; CATEGORY property or file name.
                  "%-10c"
                  " "
                  "%(mpereira/org-agenda-tags-prefix-format)"
                  "  "))
         (agenda-ignore-todos '(list "DOING" "WAITING" "DONE" "CANCELLED"))
         (settings
          `((todo "DOING"
                  ((org-agenda-overriding-header "\nDoing\n")
                   (org-agenda-prefix-format ,todo-prefix-format)))
            (todo "WAITING"
                  ((org-agenda-overriding-header "\nWaiting\n")
                   (org-agenda-prefix-format ,todo-prefix-format)))
            (agenda ""
                    ((org-agenda-overriding-header
                      (concat
                       "\nToday "
                       "(" (format-time-string "%A, %B %d" (current-time)) ")"))
                     (org-deadline-warning-days 0)
                     (org-agenda-span 'day)
                     (org-agenda-use-time-grid t)
                     (org-agenda-format-date "")
                     (org-agenda-prefix-format ,todo-prefix-format)
                     (org-habit-show-habits nil)
                     ;; Not using something like (org-agenda-skip-entry-if
                     ;; 'nottodo '("TODO")) here because I want non-TODO
                     ;; headings (e.g. calendar events) showing up here as well.
                     (org-agenda-skip-function
                      (quote (org-agenda-skip-entry-if 'todo ,agenda-ignore-todos)))))
            (agenda ""
                    ((org-agenda-overriding-header "\nNext 7 Days")
                     (org-agenda-start-day "+1d")
                     (org-agenda-span 'week)
                     (org-agenda-start-on-weekday nil)
                     (org-agenda-prefix-format ,todo-prefix-format)
                     ;; Not using something like (org-agenda-skip-entry-if
                     ;; 'nottodo '("TODO")) here because I want non-TODO
                     ;; headings (e.g. calendar events) showing up here as well.
                     (org-agenda-skip-function
                      (quote (org-agenda-skip-entry-if 'todo ,agenda-ignore-todos)))))
            (tags (concat "SCHEDULED>=\"<+8d>\"&SCHEDULED<=\"<+30d>\""
                          "|"
                          "DEADLINE>=\"<+8d>\"&DEADLINE<=\"<+30d>\""
                          "|"
                          "TIMESTAMP>=\"<+8d>\"&TIMESTAMP<=\"<+30d>\""
                          "|"
                          "TIMESTAMP_IA>=\"<+8d>\"&TIMESTAMP_IA<=\"<+30d>\""
                          "/-DONE")
                  ((org-agenda-overriding-header "\nComing up\n")
                   (org-agenda-prefix-format ,tags-prefix-format)
                   (org-agenda-sorting-strategy '(timestamp-up))))))
         (inbox-file (expand-file-name "inbox.org" org-directory))
         (inbox-buffer (find-file-noselect inbox-file))
         (inbox (with-current-buffer inbox-buffer
                  (org-element-contents (org-element-parse-buffer 'headline))))
         (_ (when inbox
              (add-to-list
               'settings
               `(todo "TODO"
                      ((org-agenda-overriding-header "\nInbox\n")
                       (org-agenda-prefix-format ,todo-prefix-format)
                       (org-agenda-files (list ,inbox-file)))))))
         (org-agenda-buffer-name mpereira/main-org-agenda-buffer-name)
         (org-agenda-custom-commands (list
                                      (list
                                       "c" "Main agenda"
                                       settings
                                       `((org-agenda-block-separator
                                          ,(s-repeat mpereira/org-agenda-width "-")))))))
    (org-agenda nil "c")
    (with-current-buffer (get-buffer mpereira/main-org-agenda-buffer-name)
      (setq-local olivetti-body-width mpereira/org-agenda-width)
      (olivetti-mode))
    (setq mpereira/main-org-agenda-last-built (ts-now))))

(defun mpereira/open-or-build-main-org-agenda ()
  "Display main org agenda if it was already built. Build and display it
otherwise."
  (interactive)
  (let ((org-agenda-buffer (get-buffer mpereira/main-org-agenda-buffer-name)))
    (setq mpereira/main-org-agenda-previous-window-configuration
          (current-window-configuration))
    (setq mpereira/main-org-agenda-previous-point (point))
    (if (and (bufferp org-agenda-buffer)
             mpereira/main-org-agenda-last-built)
        (progn
          (switch-to-buffer org-agenda-buffer)
          (delete-other-windows)
          (let ((last-built (ts-human-format-duration
                             (ts-difference (ts-now)
                                            mpereira/main-org-agenda-last-built))))
            (message (format "Last built %s ago" last-built))))
      (progn
        (mpereira/build-main-org-agenda)
        (message "Built now")))))

Review agenda

(defvar mpereira/review-org-agenda-buffer-name "*Review Org Agenda*"
  "The name of the review org agenda.")

(defvar mpereira/review-org-agenda-last-built nil
  "The last time the review org agenda was built.")

(defvar mpereira/review-org-agenda-previous-window-configuration nil
  "A window configuration to return to when closing the review org agenda.")

(defvar mpereira/review-org-agenda-previous-point nil
  "A point to return to when closing the review org agenda.")

(defun mpereira/build-review-org-agenda ()
  "Build and display the review org agenda."
  (interactive)
  (let* ((single-day-prefix-format " %-10c %?-12t% s")
         (multi-day-prefix-format " %-10c %(mpereira/org-agenda-review-prefix-format) ")
         (settings
          `((tags ,(mpereira/org-agenda-review-search "today" "+1d")
                  ((org-agenda-overriding-header
                    (concat
                     "\nDone today "
                     "(" (format-time-string "%A, %B %d" (current-time)) ")\n"))
                   (org-agenda-prefix-format ,single-day-prefix-format)))
            (tags ,(mpereira/org-agenda-review-search "-1d" "today")
                  ((org-agenda-overriding-header
                    (concat
                     "\nDone yesterday "
                     "(" (format-time-string "%A, %B %d" (mpereira/yesterday)) ")\n"))
                   (org-agenda-prefix-format ,single-day-prefix-format)))
            (tags ,(mpereira/org-agenda-review-search
                    (mpereira/org-agenda-date-week-start
                     (mpereira/format-calendar-date-Y-m-d
                      (mpereira/calendar-read-date "today")))
                    (mpereira/format-calendar-date-Y-m-d
                     (mpereira/calendar-read-date "today")))
                  ((org-agenda-overriding-header "\nDone this week\n")
                   (org-agenda-prefix-format ,multi-day-prefix-format)
                   (org-agenda-sorting-strategy '(timestamp-up))
                   (org-agenda-show-all-dates t)
                   (org-agenda-sorting-strategy '(timestamp-down))))
            (tags (mpereira/org-agenda-review-search
                   (mpereira/org-agenda-date-week-start
                    (mpereira/format-calendar-date-Y-m-d
                     (mpereira/calendar-read-date "-1w")))
                   (mpereira/org-agenda-date-week-end
                    (mpereira/format-calendar-date-Y-m-d
                     (mpereira/calendar-read-date "-1w"))))
                  ((org-agenda-overriding-header "\nDone last week\n")
                   (org-agenda-prefix-format ,multi-day-prefix-format)
                   (org-agenda-show-all-dates t)
                   (org-agenda-sorting-strategy '(timestamp-down))))))
         (org-agenda-buffer-name mpereira/review-org-agenda-buffer-name)
         (org-agenda-custom-commands (list
                                      (list
                                       "c" "Review agenda"
                                       settings
                                       `((org-agenda-block-separator
                                          ,(s-repeat mpereira/org-agenda-width "-")))))))
    (org-agenda nil "c")
    (with-current-buffer (get-buffer mpereira/review-org-agenda-buffer-name)
      (setq-local olivetti-body-width mpereira/org-agenda-width)
      (olivetti-mode))
    (setq mpereira/review-org-agenda-last-built (ts-now))))

(defun mpereira/open-or-build-review-org-agenda ()
  "Display review org agenda if it was already built. Build and display it
otherwise."
  (interactive)
  (let ((org-agenda-buffer (get-buffer mpereira/review-org-agenda-buffer-name)))
    (setq mpereira/review-org-agenda-previous-window-configuration
          (current-window-configuration))
    (setq mpereira/review-org-agenda-previous-point (point))
    (if (bufferp org-agenda-buffer)
        (progn
          (switch-to-buffer org-agenda-buffer)
          (delete-other-windows)
          (let ((last-built (ts-human-format-duration
                             (ts-difference
                              (ts-now)
                              mpereira/review-org-agenda-last-built))))
            (message (format "Last built %s ago" last-built))))
      (progn
        (mpereira/build-review-org-agenda)
        (message "Built now")))))

Common

(defun mpereira/build-org-agenda ()
  "Build the last opened org agenda."
  (interactive)
  (cond
   ((and mpereira/main-org-agenda-previous-window-configuration
         (not mpereira/review-org-agenda-previous-window-configuration))
    (funcall #'mpereira/build-main-org-agenda))
   ((and mpereira/review-org-agenda-previous-window-configuration
         (not mpereira/main-org-agenda-previous-window-configuration))
    (funcall #'mpereira/build-review-org-agenda))
   ((and mpereira/main-org-agenda-previous-window-configuration
         mpereira/review-org-agenda-previous-window-configuration)
    (if (ts<= mpereira/main-org-agenda-last-built
              mpereira/review-org-agenda-last-built)
        (funcall #'mpereira/build-review-org-agenda)
      (funcall #'mpereira/build-main-org-agenda)))))

(defun mpereira/close-org-agenda ()
  "Close the currently opened org agenda and restore the previous window
configuration and point position."
  (interactive)
  (let ((close-review-org-agenda
         (lambda ()
           (set-window-configuration
            mpereira/review-org-agenda-previous-window-configuration)
           (setq mpereira/review-org-agenda-previous-window-configuration nil)
           (goto-char mpereira/review-org-agenda-previous-point)
           (setq mpereira/review-org-agenda-previous-point nil)))
        (close-main-org-agenda
         (lambda ()
           (set-window-configuration
            mpereira/main-org-agenda-previous-window-configuration)
           (setq mpereira/main-org-agenda-previous-window-configuration nil)
           (goto-char mpereira/main-org-agenda-previous-point)
           (setq mpereira/main-org-agenda-previous-point nil))))
    (cond
     ((string= mpereira/main-org-agenda-buffer-name (buffer-name))
      (funcall close-main-org-agenda))
     ((string= mpereira/review-org-agenda-buffer-name (buffer-name))
      (funcall close-review-org-agenda))
     (t (mpereira/kill-buffer-and-maybe-window)))))

shrface

(use-package shrface
  :config
  (shrface-basic)
  (shrface-trial)
  (with-eval-after-load 'eww
    (add-hook 'eww-after-render-hook 'shrface-mode)))

outshine

(use-package outorg
  :ensure nil
  :quelpa (outorg
           :fetcher github
           :repo "alphapapa/outorg")
  :config
  (defun mpereira/outorg-edit-as-org ()
    "TODO: docstring."
    (interactive)
    (let ((byte-compile-warnings '(not obsolete)))
      (outorg-edit-as-org)))

  (defun mpereira/outorg-copy-edits-and-exit ()
    "TODO: docstring."
    (interactive)
    (if (string= outorg-edit-buffer-name (buffer-name))
        (outorg-copy-edits-and-exit)
      (message "Not in the %s buffer" outorg-edit-buffer-name))))

(use-package outshine
  :ensure nil
  :quelpa (outshine
           :fetcher github
           :repo "alphapapa/outshine")
  :config
  (add-hook 'emacs-lisp-mode-hook 'outshine-mode))

org-download

It’s very convenient to capture a screenshot to the clipboard with macOS (Shift-Cmd-5) and then paste it into an Org buffer with org-download-clipboard.

(defun filesystem-friendly-file-path (s)
  "Sanitizes string to be filesystem friendly."
  (replace-regexp-in-string "[^[:alpha:]_-]" "_" s))

(use-package org-download
  :custom
  (org-download-screenshot-method "screencapture -i %s")
  (org-download-image-dir (concat mpereira/org-directory "/download"))
  :config
  ;; MONKEYPATCH(org-download).
  (defun org-download-get-heading (lvl)
    "Return the heading of the current entry's LVL level parent."
    (save-excursion
      (let ((cur-lvl (org-current-level)))
        (if cur-lvl
            (progn
              (unless (= cur-lvl 1)
                (org-up-heading-all (- (1- (org-current-level)) lvl)))
              (let ((heading (nth 4 (org-heading-components))))
                (if heading
                    (filesystem-friendly-file-path
                     (replace-regexp-in-string
                      " " "_"
                      heading))
                  "")))
          "")))))

org-web-tools

org-web-tools-insert-web-page-as-entry is so useful. I use it to capture websites into my to-read list.

(use-package org-web-tools)

org-insert-link-dwim

From Emacs DWIM: do what ✨I✨ mean.

(declare-function org-in-regexp
                  "ext:org-macs.el"
                  (regexp &optional nlines visually))

(defun mpereira/org-insert-link-dwim ()
  "Like `org-insert-link' but with personal dwim preferences."
  (interactive)
  (let* ((point-in-link (org-in-regexp org-link-any-re 1))
         (clipboard-url (when (string-match-p "^http" (current-kill 0))
                          (current-kill 0)))
         (region-content (when (region-active-p)
                           (buffer-substring-no-properties (region-beginning)
                                                           (region-end)))))
    (cond ((and region-content clipboard-url (not point-in-link))
           (delete-region (region-beginning) (region-end))
           (insert (org-make-link-string clipboard-url region-content)))
          ((and clipboard-url (not point-in-link))
           (insert (org-make-link-string
                    clipboard-url
                    (read-string "title: "
                                 (with-current-buffer (url-retrieve-synchronously clipboard-url)
                                   (dom-text (car
                                              (dom-by-tag (libxml-parse-html-region
                                                           (point-min)
                                                           (point-max))
                                                          'title))))))))
          (t
           (call-interactively 'org-insert-link)))))

org-id

Add a unique ID property to headings when they are created.

(add-hook 'org-insert-heading-hook 'org-id-get-create)

Have org-store-link copy an org-id reference link instead of a file reference link.

(setq org-id-link-to-org-use-id 'create-if-interactive)

org-expiry

(add-to-list 'org-modules 'org-expiry)

(require 'org-expiry)

(setq org-expiry-inactive-timestamps t)

(org-expiry-insinuate)

(add-hook 'org-capture-before-finalize-hook 'org-expiry-insert-created)

org-bullets

(use-package org-bullets
  :after org
  :config
  (add-hook 'org-mode-hook (lambda () (org-bullets-mode 1))))

org-make-toc

(use-package org-make-toc
  :after org)

org-tree-slide

(use-package org-tree-slide)

org-sidebar

(use-package org-sidebar)

org-pomodoro

(use-package org-pomodoro
  :config
  (setq org-pomodoro-format "%s"))

org-archive-hierarchically

FIXME: this seems to insert unwanted whitespace between the parent and the first child tree.

(use-package org-archive-hierarchically
  :ensure nil
  :quelpa (org-archive-hierarchically
           :fetcher gitlab
           :repo "andersjohansson/org-archive-hierarchically"))

org-autonum

(use-package org-autonum
  :ensure nil
  :quelpa (org-autonum
           :fetcher github
           :repo "nma83/org-autonum"))

(defun re-seq (regexp string)
  "Get a list of all regexp matches in a string."
  (save-match-data
    (let ((pos 0)
          matches)
      (while (string-match regexp string pos)
        (push (match-string 0 string) matches)
        (setq pos (match-end 0)))
      matches)))

;; FIXME: the `'tree' scope doesn't seem to be working. Calling this
;; function on a heading with subsequent siblings will consider the
;; first heading the root of all the other ones.
;; This is because of the promote/demote hack.
(defun mpereira/org-enumerate-headings ()
  "TODO: docstring."
  (interactive)
  (save-excursion
    (let ((spacing nil)
          (current-level (org-current-level))
          (enumeration '()))
      (org-back-to-heading)
      (dotimes (i (- current-level 1))
        (org-promote-subtree))
      (org-map-entries
       (lambda ()
         ;; We subtract 1 because we want the relevant outlines being
         ;; considered to have level 1.
         (setq level (- (org-outline-level) 1))
         (print (list (list 'level level) (list 'enumeration enumeration)))
         ;; Skip the tree root entry.
         (when (> level 0)
           ;; Move to start of heading text.
           (re-search-forward "\\* " (line-end-position) t)
           (if (< (length enumeration) level)
               ;; Expand enumeration to next level.
               (setq enumeration (append enumeration '(0)))
             (if (not (= (length enumeration) level))
                 ;; Prune enumeration to current level.
                 (setq enumeration (butlast enumeration
                                            (- (length enumeration)
                                               level)))))
           ;; Increment last enumeration number.
           (setq enumeration (append (butlast enumeration 1)
                                     (list (1+ (car (last enumeration 1))))))
           (setq enumeration-string (concat
                                     (mapconcat
                                      'number-to-string enumeration ".")
                                     ". "))
           ;; FIXME: this isn't working.
           (if (re-search-forward (concat "* "
                                          "\\("
                                          "[[:digit:]]+\."
                                          "\\([[:digit:]]+\.\\)*"
                                          "\\)"
                                          " ")
                                  (line-end-position)
                                  t)
               ;; Replace existing enumeration if it's different.
               (unless (string= (match-string 0) enumeration-string)
                 (replace-match enumeration-string nil nil))
             ;; Insert new enumeration.
             (insert enumeration-string))))
       t
       'tree)
      (dotimes (i (- current-level 1))
        (org-demote-subtree)))))

ob-async

(use-package ob-async)

ox-jira

(use-package ox-jira)

ox-twbs

(use-package ox-twbs)

ox-gfm

(use-package ox-gfm)

ox-hugo

(use-package ox-hugo)

ox-pandoc

(use-package ox-pandoc)

File management

dired

(setq dired-recursive-copies 'always)
(setq dired-recursive-deletes 'always)
(setq delete-by-moving-to-trash t)

(if mpereira/is-gnu-bash
    (setq dired-listing-switches "-AFhlv --group-directories-first")
  (setq dired-listing-switches "-alh"))

(setq find-ls-option ;; applies to `find-name-dired'
      '("-print0 | xargs -0 ls -AFlv --group-directories-first" . "-AFlv --group-directories-first"))

(add-hook 'dired-mode-hook 'dired-hide-details-mode)

(dired-async-mode 1)

(require 'wdired)
(setq wdired-allow-to-change-permissions t)

(require 'dired-x)
(add-hook 'dired-mode-hook 'dired-omit-mode)

(general-define-key
 :keymaps '(dired-mode-map)
 :states '(normal visual)
 "(" 'dired-subtree-up
 ";" nil ; originally the first keystroke for encryption-related bindings.
 "C-9" 'dired-hide-details-mode
 "C-j" 'dired-next-dirline
 "C-k" 'dired-prev-dirline
 "M-c" 'dired-ranger-copy
 "M-v" 'dired-ranger-paste)

dired-quick-sort

(use-package dired-quick-sort
  :general (:keymaps '(dired-mode-map)
            :states '(normal visual)
            ;; NOTE: "s" isn't used, and the default "S" is overriden by
            ;; evil-collection probably.
            "s" #'hydra-dired-quick-sort/body)
  :config
  (dired-quick-sort-setup))

dired-ranger

(use-package dired-ranger)

dired-plus

Disabled for now. Too overwhelming when combined with all-the-icons-dired.

(use-package dired-plus
  :disabled
  :ensure nil
  :quelpa (dired+
           :fetcher github
           :repo "emacsmirror/dired-plus"))

dired-show-readme

Disabled for now. Doesn’t allow navigating README, doesn’t render markdown.

(use-package dired-show-readme
  :disabled
  :ensure nil
  :quelpa (dired-show-readme
           :fetcher gitlab
           :repo "kisaragi-hiu/dired-show-readme")
  :config
  (add-hook 'dired-mode-hook 'dired-show-readme-mode))

dired-subtree

(use-package dired-subtree
  :after dired)

Make dired-subtree work with all-the-icons-dired by reverting the buffer on cycling so that icons are rendered. Disabled by default for now because it impacts performance heavily in large directories.

(use-package dired-subtree
  :disabled
  :after dired
  :init
  (defun mpereira/dired-subtree-toggle ()
    (interactive)
    (dired-subtree-toggle)
    (revert-buffer))

  (defun mpereira/dired-subtree-cycle ()
    (interactive)
    (dired-subtree-cycle)
    (revert-buffer))

  :bind (:map dired-mode-map
         ("<tab>" . mpereira/dired-subtree-toggle)
         ("<S-tab>" . mpereira/dired-subtree-cycle)))

reveal-in-osx-finder

(use-package reveal-in-osx-finder)

Shell, terminal

with-editor

(use-package with-editor
  :config
  (add-hook 'eshell-mode-hook 'with-editor-export-editor)
  (add-hook 'term-exec-hook 'with-editor-export-editor)
  (add-hook 'shell-mode-hook 'with-editor-export-editor)

  (add-hook 'with-editor-mode-hook 'evil-insert-state))

shell

(add-hook 'shell-mode-hook 'buffer-disable-undo)

(general-define-key
 :keymaps '(shell-mode-map)
 :states '(insert)
 "C-l" 'comint-clear-buffer)

eshell

(require 'eshell)
(require 'em-dirs) ;; for `eshell/pwd'.
(require 'em-smart)
(require 'em-tramp)

;; Don't display the "Welcome to the Emacs shell" banner.
(setq eshell-banner-message "")

;; Make it possible to get a remote eshell buffer.
(add-to-list 'eshell-modules-list 'eshell-tramp)

(setenv "LANG" "en_US.UTF-8")
(setenv "LC_ALL" "en_US.UTF-8")
(setenv "LC_CTYPE" "en_US.UTF-8")

;; Don't page shell output.
(setenv "PAGER" "cat")

(setq eshell-scroll-to-bottom-on-input 'all)
(setq eshell-buffer-maximum-lines 20000)
(setq eshell-history-size 1000000)
(setq eshell-error-if-no-glob t)
(setq eshell-hist-ignoredups t)
(setq eshell-save-history-on-exit t)
;; `find` and `chmod` behave differently on eshell than unix shells. Prefer unix
;; behavior.
(setq eshell-prefer-lisp-functions nil)

(defun eshell/clear ()
  "Clears buffer while preserving input."
  (let* ((inhibit-read-only t)
         (input (eshell-get-old-input)))
    (eshell/clear-scrollback)
    (eshell-emit-prompt)
    (insert input)
    ;; This fixes the scenario where `ivy-completion-in-region-action' tries to
    ;; delete a region delimited by these two variables after they went out of
    ;; sync due to clearing an eshell buffer. The symptoms are broken completion
    ;; insertion and messages like: "Args out of range: #<buffer *eshell*>,
    ;; 237506, 237518" in the messages buffer. Should probably check with the
    ;; ivy people if this should be handled by ivy itself instead?
    (setq ivy-completion-beg nil)
    (setq ivy-completion-end nil)))

(defun mpereira/eshell-clear ()
  (interactive)
  (eshell/clear))

;; Inspired by Prot's.
(defun mpereira/eshell-complete-recent-directory (&optional arg)
  "Switch to a recent `eshell' directory using completion.
With \\[universal-argument] also open the directory in a `dired' buffer."
  (interactive "P")
  (ivy-read "Switch to recent dir: "
            (delete-dups (ring-elements eshell-last-dir-ring))
            :action (lambda (x)
                      (insert dir)
                      (eshell-send-input)
                      (when arg
                        (dired dir)))))

;; Inspired by Prot's.
(defun mpereira/eshell-switch-to-last-output-buffer ()
  "Produce a buffer with output of last `eshell' command."
  (interactive)
  (let ((eshell-output (kill-region (eshell-beginning-of-output)
                                    (eshell-end-of-output))))
    (with-current-buffer (get-buffer-create "*last-eshell-output*")
      (erase-buffer)
      ;; TODO: do it with `insert' and `delete-region'?
      (yank)
      (goto-char (point-min))
      (display-buffer (current-buffer)))))

;; Inspired by Prot's.
(defun mpereira/eshell-complete-redirect-to-buffer ()
  "Complete the syntax for appending to a buffer via `eshell'."
  (interactive)
  (end-of-line)
  (insert
   (concat " >>> #<" (read-buffer-to-switch "Redirect to buffer:") ">")))

;; I don't use `counsel-esh-history' because it doesn't take into consideration
;; the current input.
(defun mpereira/eshell-history ()
  "Browse Eshell history."
  (interactive)
  (let ((candidates (delete-dups (ring-elements eshell-history-ring)))
        (input (let ((input-start (save-excursion (eshell-bol)))
                     (input-end (save-excursion (end-of-line) (point))))
                 (buffer-substring-no-properties input-start input-end))))
    (ivy-read "Command: "
              candidates
              :action (lambda (candidate)
                        (end-of-line)
                        (eshell-kill-input)
                        (insert (string-trim candidate)))
              :caller 'mpereira/eshell-history
              :initial-input input)))

;; FIXME: this needs to be manually evaluated after init. Why?
(with-eval-after-load "ivy"
  (ivy-set-display-transformer
   'mpereira/eshell-history
   (lambda (candidate)
     (->> candidate
          ;; Don't display multiline commands.
          (s-replace "\n" "; ")
          ;; Limit command width to ivy-posframe frame width.
          (s-truncate ivy-posframe-width)))))

;; eshell-mode-map needs to be configured in an `eshell-mode-hook'.
;; https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-02/msg01532.html
(defun mpereira/initialize-eshell ()
  (interactive)
  ;; Completion functions depend on pcomplete.
  ;; Don't use TAB for cycling through candidates.
  (setq pcomplete-cycle-completions nil)
  (setq pcomplete-ignore-case t)

  (eshell/alias "e" "find-file $1")

  ;; Eshell needs this variable set in addition to the PATH environment variable.
  (setq eshell-path-env (getenv "PATH"))

  (general-define-key
   :keymaps '(eshell-mode-map)
   "C-c C-c" 'eshell-interrupt-process
   "C-S-k" 'mpereira/eshell-switch-to-last-output-buffer
   "C->" 'mpereira/eshell-complete-redirect-to-buffer)

  (general-define-key
   :states '(normal visual)
   :keymaps '(eshell-mode-map)
   "0" 'eshell-bol
   "C-j" 'eshell-next-prompt
   "C-k" 'eshell-previous-prompt)

  (general-define-key
   :states '(insert)
   :keymaps '(eshell-mode-map)
   ;; TODO: `eshell-{previous,next}-matching-input-from-input' only work with
   ;; prefix inputs, like "git". They don't do fuzzy matching.
   ;;
   ;; TODO: when on an empty prompt and going up and back down (or down and back
   ;; up), make it so that the prompt is empty again instead of cycling back to
   ;; the first input.
   "<tab>" 'completion-at-point
   "C-k" 'eshell-previous-matching-input-from-input
   "C-j" 'eshell-next-matching-input-from-input
   "C-/" 'mpereira/eshell-history
   ;; https://github.com/ksonney/spacemacs/commit/297945a45696e235c6983a78acdf05b5f0e015ca
   "C-l" 'mpereira/eshell-clear)

  ;; REVIEW(maybe-unnecessary): workaround for a bug. When an eshell buffer is
  ;; created the `eshell-mode-map' mappings are not set up, even through
  ;; `eshell-mode-map' is correctly defined. Going to normal state sets them up
  ;; for some reason.
  (evil-normal-state)
  (evil-insert-state)
  (forward-char))

(add-hook 'eshell-mode-hook 'mpereira/initialize-eshell)

;; Disable a few possibly-global modes.
(add-hook 'eshell-mode-hook (lambda () (undo-tree-mode -1)) t)

(defun mpereira/remote-p ()
  (tramp-tramp-file-p default-directory))

(defun mpereira/remote-user ()
  "Return remote user name."
  (or (tramp-file-name-user (tramp-dissect-file-name default-directory))
      (eshell/whoami)))

(defun mpereira/remote-host ()
  "Return remote host."
  ;; `tramp-file-name-real-host' is removed and replaced by
  ;; `tramp-file-name-host' in Emacs 26, see
  ;; https://github.com/kaihaosw/eshell-prompt-extras/issues/18
  (if (fboundp 'tramp-file-name-real-host)
      (tramp-file-name-real-host (tramp-dissect-file-name default-directory))
    (tramp-file-name-host (tramp-dissect-file-name default-directory))))

(defun mpereira/eshell-prompt ()
  (let ((user-name (if (mpereira/remote-p)
                       (mpereira/remote-user)
                     (user-login-name)))
        (host-name (if (mpereira/remote-p)
                       (mpereira/remote-host)
                     (system-name))))
    (concat
     (propertize user-name 'face '(:foreground "green"))
     " "
     (propertize "at" 'face 'eshell-ls-unreadable)
     " "
     (propertize host-name 'face '(:foreground "cyan"))
     " "
     (propertize "in" 'face 'eshell-ls-unreadable)
     " "
     (propertize (mpereira/short-directory-path
                  (eshell/pwd)
                  mpereira/eshell-prompt-max-directory-length)
                 'face 'dired-directory)
     "\n"
     (propertize (if (= (user-uid) 0)
                     "#"
                   "$")
                 'face 'eshell-prompt)
     " ")))

(setq eshell-prompt-function 'mpereira/eshell-prompt)
(setq eshell-prompt-regexp "^[$#] ")

;; Make eshell append to history after each command.
;; https://emacs.stackexchange.com/questions/18564/merge-history-from-multiple-eshells
;; (setq eshell-save-history-on-exit nil)
;; (defun eshell-append-history ()
;;   "Call `eshell-write-history' with the `append' parameter set to `t'."
;;   (when eshell-history-ring
;;     (let ((newest-cmd-ring (make-ring 1)))
;;       (ring-insert newest-cmd-ring (car (ring-elements eshell-history-ring)))
;;       (let ((eshell-history-ring newest-cmd-ring))
;;         (eshell-write-history eshell-history-file-name t)))))
;; (add-hook 'eshell-pre-command-hook #'eshell-append-history)

;; Shared history.
;; https://github.com/Ambrevar/dotfiles/blob/25e2ed350b898c3fc2df3148630b5778a3db4ee7/.emacs.d/lisp/init-eshell.el#L205
;; TODO: make this per project?
(defvar mpereira/eshell-history-global-ring nil
  "The history ring shared across Eshell sessions.")

(defun mpereira/eshell-hist-use-global-history ()
  "Make Eshell history shared across different sessions."
  (unless mpereira/eshell-history-global-ring
    (when eshell-history-file-name
      (eshell-read-history nil t))
    (setq mpereira/eshell-history-global-ring
          (or eshell-history-ring (make-ring eshell-history-size))))
  (setq eshell-history-ring mpereira/eshell-history-global-ring))

(add-hook 'eshell-mode-hook #'mpereira/eshell-hist-use-global-history)

Fix eshell autocomplete

This fix provided by Ethan Leba essentially reverts the commit which introduced the bug. Tracking bug in debbugs: #48995.

Without it the following happens when trying to autocomplete a file:

$ ls
foo.sh
$ ./f<TAB>
$ foo.sh

With it:

$ ls
foo.sh
$ ./f<TAB>
$ ./foo.sh

And other weird completions.

This is apparently fixed on Emacs 29 so it’s loaded conditionally.

(use-package emacs
  :when (< emacs-major-version 29)
  (defun eshell--complete-commands-list ()
    "Generate list of applicable, visible commands."
    (let ((filename (pcomplete-arg)) glob-name)
      (if (file-name-directory filename)
          (if eshell-force-execution
              (pcomplete-dirs-or-entries nil #'file-readable-p)
            (pcomplete-executables))
        (if (and (> (length filename) 0)
                 (eq (aref filename 0) eshell-explicit-command-char))
            (setq filename (substring filename 1)
                  pcomplete-stub filename
                  glob-name t))
        (let* ((paths (eshell-get-path))
               (cwd (file-name-as-directory
                     (expand-file-name default-directory)))
               (path "") (comps-in-path ())
               (file "") (filepath "") (completions ()))
          ;; Go thru each path in the search path, finding completions.
          (while paths
            (setq path (file-name-as-directory
                        (expand-file-name (or (car paths) ".")))
                  comps-in-path
                  (and (file-accessible-directory-p path)
                       (file-name-all-completions filename path)))
            ;; Go thru each completion found, to see whether it should
            ;; be used.
            (while comps-in-path
              (setq file (car comps-in-path)
                    filepath (concat path file))
              (if (and (not (member file completions)) ;
                       (or (string-equal path cwd)
                           (not (file-directory-p filepath)))
                       (if eshell-force-execution
                           (file-readable-p filepath)
                         (file-executable-p filepath)))
                  (setq completions (cons file completions)))
              (setq comps-in-path (cdr comps-in-path)))
            (setq paths (cdr paths)))
          ;; Add aliases which are currently visible, and Lisp functions.
          (pcomplete-uniquify-list
           (if glob-name
               completions
             (setq completions
                   (append (if (fboundp 'eshell-alias-completions)
                               (eshell-alias-completions filename))
                           (eshell-winnow-list
                            (mapcar
                             (lambda (name)
                               (substring name 7))
                             (all-completions (concat "eshell/" filename)
                                              obarray #'functionp))
                            nil '(eshell-find-alias-function))
                           completions))
             (append (and (or eshell-show-lisp-completions
                              (and eshell-show-lisp-alternatives
                                   (null completions)))
                          (all-completions filename obarray #'functionp))
                     completions))))))))

vterm

(use-package vterm
  :if (executable-find "cmake")
  ;; Disabling hl-line-mode in vterm buffers because typing causes the highlight
  ;; to flicker.
  :hook (vterm-mode-hook . mpereira/hl-line-mode-disable)
  :init
  (setq vterm-always-compile-module t)
  :config
  (general-define-key
   :states '(normal visual)
   :keymaps '(vterm-mode-map)
   ;; REVIEW(necessary?)
   "C-l" nil))

term

(setq explicit-shell-file-name "bash")

;; Infinite buffer.
(setq term-buffer-maximum-size 0)

;; This defaults to `t' which causes the point to not be movable from the
;; process mark.
(setq term-char-mode-point-at-process-mark nil)

;; REVIEW(maybe-unnecessary).
(general-define-key
 :keymaps '(term-raw-map)
 :states '(normal)
 "p" 'term-paste
 "M-x" 'execute-extended-command)

;; REVIEW(maybe-unnecessary).
(general-define-key
 :keymaps '(term-raw-map)
 :states '(insert)
 "M-v" 'term-paste)

;; REVIEW(maybe-unnecessary).
(general-define-key
 ;; Are both necessary? C-c C-c wasn't working just with `term-raw-map' so I
 ;; added `term-mode-map' and re-evaluated, started working in a term buffer.
 :keymaps '(term-raw-map term-mode-map)
 :prefix "C-c"
 ;; https://github.com/noctuid/general.el#how-do-i-prevent-key-sequence-starts-with-non-prefix-key-errors
 "" nil
 "C-c" #'term-interrupt-subjob)

(add-hook 'term-mode-hook #'mpereira/hide-trailing-whitespace)

eterm-256color

(use-package eterm-256color
  :config
  (add-hook 'term-mode-hook #'eterm-256color-mode))

bash-completion

Disabled because it doesn’t work in Eshell buffers.

(use-package bash-completion
  :disabled
  :init
  (setq bash-completion-use-separate-processes nil)
  :config
  (bash-completion-setup))

fish-completion

Disabling this for now because it breaks path completion. The issue below says that the issue is fixed on recent versions of emacs-fish-completion, but it doesn’t really seem to be. https://gitlab.com/ambrevar/emacs-fish-completion/issues/3.

I wrote the above a while ago. Maybe it’s fixed now?

This doesn’t work on remote Eshell buffers even when fish is installed on the remote machines.

(use-package fish-completion
  :disabled
  :config
  (if (executable-find "fish")
      (global-fish-completion-mode)
    (message "fish executable not found, not enabling fish-completion-mode"))

  (setq fish-completion-fallback-on-bash-p t)

  ;; TODO: implement this.
  ;; WIP: Adds support for showing completion descriptions.
  ;; https://github.com/emacs-helm/helm-fish-completion/blob/master/helm-fish-completion.el
  (defun mpereira/fish-completion-complete (input)
    "Complete RAW-PROMPT (any string) using the fish shell.

If `fish-completion-fallback-on-bash-p' is non-nil and if the `bash-completion'
package is available, fall back on bash in case no completion was found with
fish."
    (interactive)
    (while
        (pcomplete-here
         (let ((completions
                (let* (;; Keep spaces at the end with OMIT-NULLS=nil in
                       ;; `split-string'.
                       (tokens* (split-string input
                                              split-string-default-separators
                                              nil))
                       ;; The first non-empty `car' is the command. Discard
                       ;; leading empty strings.
                       (tokens (progn (while (string= (car tokens*) "")
                                        (setq tokens* (cdr tokens*)))
                                      tokens*))
                       ;; Fish does not support subcommand completion. We make a
                       ;; special case of 'sudo' and 'env' since they are the most
                       ;; common cases involving subcommands. See
                       ;; https://github.com/fish-shell/fish-shell/issues/4093.
                       (prompt (if (not (member (car tokens) '("sudo" "env")))
                                   input
                                 (setq tokens (cdr tokens))
                                 (while (and tokens
                                             (or (string-match "^-.*" (car tokens))
                                                 (string-match "=" (car tokens))))
                                   ;; Skip env/sudo parameters, like LC_ALL=C.
                                   (setq tokens (cdr tokens)))
                                 (mapconcat 'identity tokens " "))))
                  ;; Completion result can be a filename. pcomplete expects
                  ;; cannonical file names (i.e. without '~') while fish preserves
                  ;; non-cannonical results. If the result contains a directory,
                  ;; expand it.
                  (split-string
                   (with-output-to-string
                     (with-current-buffer standard-output
                       (call-process fish-completion-command
                                     nil
                                     t
                                     nil
                                     "-c"
                                     (format "complete -C%s"
                                             (shell-quote-argument prompt)))))
                   "\n"
                   t))))
           (if (and fish-completion-fallback-on-bash-p
                    (or (not completions)
                        (file-exists-p (car completions)))
                    (require 'bash-completion nil t))
               ;; Remove trailing spaces of bash completion entries. (Does this
               ;; only occurs when there is 1 completion item?)
               ;; TODO: Maybe this should be fixed in bash-completion instead.
               (mapcar 'string-trim-right
                       (mapcar (lambda (s)
                                 ;; bash-completion inserts "\" to escape white
                                 ;; spaces, we need to remove them since pcomplete
                                 ;; does that too.
                                 (replace-regexp-in-string (regexp-quote "\\") "" s))
                               (nth 2 (bash-completion-dynamic-complete-nocomint
                                       (save-excursion (eshell-bol) (point)) (point)))))
             (if (and completions (file-exists-p (car completions)))
                 (pcomplete-dirs-or-entries)
               (let ((formatted-completions
                      (mapcar
                       (lambda (e)
                         (multiple-value-bind (flag description) (split-string e "\t")
                           ;; Remove trailing spaces to avoid it being converted
                           ;; into "\ ".
                           (string-trim-right
                            (if description
                                (replace-regexp-in-string
                                 (regexp-quote " ")
                                 ""
                                 (format "%-50s %s" flag description))
                              flag))))
                       completions)))
                 formatted-completions))))))))

load-bash-alias

(use-package load-bash-alias
  :config
  (setq load-bash-alias-bashrc-file "~/.aliases"))

UI

Settings

(setq confirm-kill-emacs 'y-or-n-p)
(fset 'yes-or-no-p 'y-or-n-p)

(menu-bar-mode -1)
(scroll-bar-mode -1)
(tool-bar-mode -1)
(blink-cursor-mode -1)
(setq frame-resize-pixelwise t)

;; Don't show UI-based dialogs from mouse events.
(setq use-dialog-box nil)

;; Shh...
(setq inhibit-startup-echo-area-message t)
(setq inhibit-startup-screen t)
(setq initial-scratch-message nil)
(setq ring-bell-function 'ignore)

;; Make cursor the width of the character it is under e.g. full width of a TAB.
(setq x-stretch-cursor t)

;; Minimal titlebar for macOS.
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(ns-appearance . dark))
(setq ns-use-proxy-icon nil)
(setq frame-title-format nil)

;; Start in full-screen.
(add-hook 'after-init-hook #'toggle-frame-fullscreen)

Make profiler report columns wider

(use-package profiler
  :config
  (setf (caar profiler-report-cpu-line-format) 100
        (caar profiler-report-memory-line-format) 100))

tree-sitter

(use-package tree-sitter
  :config
  (add-to-list 'tree-sitter-major-mode-language-alist '(tsx-ts-mode . tsx))
  (add-hook 'prog-mode-hook #'turn-on-tree-sitter-mode)
  (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode))

(use-package tree-sitter-langs
  :config
  (tree-sitter-langs-install-grammars)
  (tree-sitter-require 'javascript)
  (tree-sitter-require 'tsx)
  (tree-sitter-require 'typescript))

(use-package treesit-auto
  :config
  (setq treesit-auto-install t)
  (global-treesit-auto-mode))

ts-fold

Doesn’t do nested folds for JavaScript yet.

(use-package ts-fold
  :ensure nil
  :quelpa (ts-fold
           :fetcher github
           :repo "emacs-tree-sitter/ts-fold")
  :config
  (add-to-list 'ts-fold-range-alist
               '(tsx-ts-mode
                 (export_clause . ts-fold-range-seq)
                 (statement_block . ts-fold-range-seq)
                 (comment . ts-fold-range-c-like-comment)))
  (add-to-list 'ts-fold-summary-parsers-alist
               '(tsx-ts-mode . ts-fold-summary-javadoc)))

default-text-scale

(use-package default-text-scale)

Font sizes

A M-x disable-theme followed by a M-x load-theme is required after changing the default font size with default-text-scale-increase and default-text-scale-decrease.

I tried using frame-text-cols to get the frame width in characters, but it seems that its return value only changes after there has been some user interaction with the frame (like switching buffers), even after the font size has been changed programmatically. Because of this, I fell back to using

(/ (frame-text-width)
   (frame-char-width))

to get the actual frame width in characters.

(setq mpereira/font-family "Hack")
(setq mpereira/font-size-external-monitor 170)
(setq mpereira/font-size-external-monitor-posframe-width-multiplier 0.1)
(setq mpereira/font-size-laptop 150)
(setq mpereira/font-size-laptop-posframe-width-multiplier 0.5)
(setq mpereira/font-size-posframe-minimum-width 100)

(setq mpereira/font-size-initial mpereira/font-size-external-monitor)

(defun mpereira/font-size-normalize (font-size)
  (- font-size (mod font-size 2)))

(defun mpereira/font-size-handle-change (new-font-size)
  (interactive)
  (let* ((default-font-family (face-attribute 'default :family))
         (frame-column-width (/ (frame-text-width)
                                (frame-char-width)))
         (ivy-posframe-width-multiplier 0.5)
         (ivy-posframe-font-size-multiplier 1.1)
         (ivy-posframe-font-size (mpereira/font-size-normalize
                                  (truncate
                                   (* ivy-posframe-font-size-multiplier
                                      (/ new-font-size 10))))))
    (with-eval-after-load "ivy-posframe"
      (message (format "setting ivy-posframe-font to '%s'"
                       (format "%s %s"
                               default-font-family
                               ivy-posframe-font-size)))
      (setq ivy-posframe-font (format "%s %s"
                                      default-font-family
                                      ivy-posframe-font-size))
      (message (format "setting ivy-posframe-width to '%d'"
                       (truncate (* ivy-posframe-width-multiplier
                                    frame-column-width))))
      (setq ivy-posframe-width (truncate (* ivy-posframe-width-multiplier
                                            frame-column-width))))
    (setq mpereira/company-box-icon-size-pixels (mpereira/font-size-normalize
                                                 (truncate (/ new-font-size 10))))
    (with-eval-after-load "company-box"
      (company-box-icons-resize mpereira/company-box-icon-size-pixels))))

(defun mpereira/font-size-change (change-fn)
  (interactive)
  (let* ((previous-default-font-size (face-attribute 'default :height))
         (_ (message "frame-column-width before: %d" (/ (frame-text-width)
                                                        (frame-char-width))))
         (_ (funcall change-fn previous-default-font-size))
         (_ (message "frame-column-width after: %d" (/ (frame-text-width)
                                                       (frame-char-width))))
         (increased-default-font-size (face-attribute 'default :height)))
    (mpereira/font-size-handle-change increased-default-font-size)))

(add-hook 'after-setting-font-hook
          (lambda ()
            (message "frame-column-width after after-setting-font-hook: %d"
                     (/ (frame-text-width)
                        (frame-char-width)))))

(defun mpereira/font-size-increase ()
  (interactive)
  (mpereira/font-size-change (lambda (actual-font-size)
                               (default-text-scale-increase))))

(defun mpereira/font-size-decrease ()
  (interactive)
  (mpereira/font-size-change (lambda (actual-font-size)
                               (default-text-scale-decrease))))

(defun mpereira/font-size-set (desired-font-size)
  (interactive)
  (mpereira/font-size-change
   (lambda (actual-font-size)
     (let ((delta (- desired-font-size actual-font-size)))
       (default-text-scale-increment (mpereira/font-size-normalize delta))))))

(defun mpereira/font-size-set-preset (font-size
                                      posframe-width-multiplier
                                      reload-theme?)
  (mpereira/font-size-set font-size)
  (when reload-theme?
    (when-let ((current-theme (car custom-enabled-themes)))
      (disable-theme (symbol-name (car custom-enabled-themes)))
      (load-theme current-theme))))

(defun mpereira/font-size-set-external-monitor ()
  (interactive)
  (mpereira/font-size-set-preset
   mpereira/font-size-external-monitor
   mpereira/font-size-external-monitor-posframe-width-multiplier
   (called-interactively-p 'any)))

(defun mpereira/font-size-set-laptop ()
  (interactive)
  (mpereira/font-size-set-preset
   mpereira/font-size-laptop
   mpereira/font-size-laptop-posframe-width-multiplier
   (called-interactively-p 'any)))

(defun mpereira/font-initialize ()
  (interactive)
  (when (x-list-fonts "Hack")
    (set-face-attribute 'default nil :family mpereira/font-family))
  (set-face-attribute 'default nil :height mpereira/font-size-initial)
  (mpereira/font-size-handle-change mpereira/font-size-initial))

(add-hook 'after-init-hook #'mpereira/font-initialize 'append)
(add-hook 'after-init-hook #'mpereira/font-size-set-external-monitor 'append)

posframe

(use-package posframe)

ivy-posframe

(use-package ivy-posframe
  :after (ivy posframe)
  :custom
  (ivy-posframe-border-width 1)
  (ivy-posframe-display-functions-alist '((swiper . ivy-display-function-fallback)
                                          (t . ivy-posframe-display-at-frame-center)))
  :config
  (ivy-posframe-mode))

so-long

(use-package so-long
  :config
  (global-so-long-mode))

too-long-lines-mode

(use-package too-long-lines-mode
  :ensure nil
  :quelpa (too-long-lines-mode
           :fetcher github
           :repo "rakete/too-long-lines-mode")
  :config
  (too-long-lines-mode))

company-box

Changing themes usually breaks the company-box frames appearance. This setup fixes that.

Check out the Layout Parameters documentation for configuring company-box-frame-parameters below.

Since I upgraded Emacs to contain the frame border fix for macOS, company-box has been causing Emacs to die sporadically. Disabling it until I fix this.

(use-package company-box
  :after (company ivy-posframe)
  :hook (company-mode-hook . company-box-mode)
  :init
  (setq company-box-doc-delay 0)
  (setq company-box-max-candidates 1000)
  (setq company-box-show-single-candidate t)
  (setq company-tooltip-align-annotations t)

  (setq company-box-doc-frame-parameters
        `((internal-border-width . ,ivy-posframe-border-width)))

  (defun mpereira/company-box-doc-sync-frame-with-current-theme ()
    "TODO: docstring."
    (interactive)
    (setq company-box-doc-frame-parameters
          `((background-color . ,(face-attribute 'ivy-posframe :background nil t))
            (foreground-color . ,(face-attribute 'ivy-posframe :foreground nil t))
            (internal-border-width . ,ivy-posframe-border-width)
            (internal-border-color . ,(face-attribute 'ivy-posframe-border
                                                      :background
                                                      nil
                                                      t)))))

  (defun mpereira/company-box-update-frames-and-parameters ()
    "TODO: docstring."
    (interactive)
    (mpereira/company-box-doc-sync-frame-with-current-theme)
    (company-box--set-frame (company-box--make-frame (company-box--get-buffer)))
    (frame-local-setq company-box-doc-frame
                      (company-box-doc--make-frame (company-box--get-buffer
                                                    "doc"))))

  ;; FIXME: this is causing the company-box frame to have a small,
  ;; non-expandable width.
  ;; (add-hook 'after-init-hook
  ;;           (lambda (&optional _)
  ;;             ;; This needs to run after the hook that sets the initial theme.
  ;;             (mpereira/company-box-doc-sync-frame-with-current-theme)
  ;;             ;; Adding this hook only after Emacs startup so that the initial
  ;;             ;; theme setting doesn't trigger it. It was causing the
  ;;             ;; company-box frame width to be truncated, for some reason.
  ;;             (add-hook 'after-load-theme-hook 'mpereira/company-box-update-frames-and-parameters))
  ;;           'append)
  )

minibuffer-line

(use-package minibuffer-line
  :config
  (setq minibuffer-line-format
        '((:eval
           (let ((time-string (format-time-string "%a %b %d %R")))
             (concat
              (propertize (make-string (- (frame-text-cols)
                                          (string-width time-string))
                                       ?\s)
                          'face 'default)
              time-string)))))
  (minibuffer-line-mode t))

highlight-indent-guides

Mode not enabled by default.

(use-package highlight-indent-guides
  :config
  (setq highlight-indent-guides-method 'character))

origami

(use-package origami
  :config
  (add-hook 'prog-mode-hook #'origami-mode))

beacon

Disabling beacon because it makes some modes really slow:

  • magit-diff buffers
(use-package beacon
  :disabled
  :config
  (add-to-list 'beacon-dont-blink-major-modes 'eshell-mode)
  (beacon-mode 1)
  (setq beacon-size 40))

rainbow-delimiters

(use-package rainbow-delimiters
  :config
  (add-hook 'lisp-mode-hook 'rainbow-delimiters-mode))

diff-hl

Disabling for now because it was making buffers slow.

(use-package diff-hl
  :disabled
  :config
  (global-diff-hl-mode t)
  (diff-hl-flydiff-mode t)

  ;; FIXME(slow).
  ;; (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)

  (set-face-foreground 'diff-hl-insert "diff-nonexistent")
  (set-face-background 'diff-hl-insert "green4")
  (set-face-foreground 'diff-hl-change "diff-nonexistent")
  (set-face-background 'diff-hl-change "yellow3")
  (set-face-foreground 'diff-hl-delete "diff-nonexistent")
  (set-face-background 'diff-hl-delete "red4"))

dimmer

This package seems to cause Emacs to become sluggish after being enabled. Disabling it doesn’t help, only restarting Emacs.

(use-package dimmer
  :after (ivy-posframe company-posframe)
  :config
  (setq dimmer-fraction 0.5)
  (setq dimmer-exclusion-predicates '(window-minibuffer-p))
  ;; Seems to improve performance a bit.
  (setq dimmer-watch-frame-focus-events nil)
  (setq dimmer-exclusion-regexp-list
        `(,ivy-posframe-buffer
          ,company-posframe-buffer
          ,company-posframe-quickhelp-buffer
          "^\\*Minibuf-[0-9]+\\*$"
          "^\\*Echo.*\\*$"
          " \\*transient\\*"
          ".*which-key.*"
          ".*Messages.*"
          ".*Async.*"
          ".*Warnings.*")))

all-the-icons

(use-package all-the-icons)

dired-sidebar

(use-package dired-sidebar
  :commands (dired-sidebar-toggle-sidebar))

all-the-icons-dired

Run M-x all-the-icons-install-fonts after installing.

(use-package all-the-icons-dired
  :after (all-the-icons dired)
  :commands (all-the-icons-dired-mode)
  :config
  (add-hook 'dired-mode-hook #'all-the-icons-dired-mode))

emojify

Mode not enabled by default.

(use-package emojify)

ivy-rich

This mode affects performance for some functions

(use-package ivy-rich
  :after ivy
  :init
  (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line))

Run ivy-rich-mode only after loading all-the-icons-ivy-rich

(use-package emacs
  :after (ivy-rich all-the-icons-ivy-rich)
  :config
  ;; Toggle `ivy-rich-mode' after modifying `ivy-rich-display-transformers-list'.
  (setq ivy-rich-display-transformers-list
        `(ivy-switch-buffer
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-switch-buffer-transformer
                      (:width 0.2))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.2))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          ivy-switch-buffer-other-window
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-rich-candidate
                      (:width 30))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda
                                (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.3))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          counsel-switch-buffer
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-rich-candidate
                      (:width 30))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.3))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          counsel-projectile-switch-to-buffer
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-rich-candidate
                      (:width 30))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.3))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          counsel-switch-buffer-other-window
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-rich-candidate
                      (:width 30))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.3))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          package-install
          (:columns ((ivy-rich-candidate (:width 30))
                     (ivy-rich-package-version (:width 16 :face font-lock-comment-face)) ; package version
                     (ivy-rich-package-archive-summary (:width 7 :face font-lock-builtin-face)) ; archive summary
                     (ivy-rich-package-install-summary (:face font-lock-doc-face)))) ; package description
          persp-switch-to-buffer
          (:columns ((all-the-icons-ivy-rich-buffer-icon)
                     (ivy-rich-candidate
                      (:width 30))
                     (ivy-rich-switch-buffer-size
                      (:width 7))
                     (ivy-rich-switch-buffer-indicators
                      (:width 4 :face error :align right))
                     (ivy-rich-switch-buffer-major-mode
                      (:width 18 :face warning))
                     (ivy-rich-switch-buffer-project
                      (:width 15 :face success))
                     (ivy-rich-switch-buffer-path
                      (:width (lambda (candidate)
                                (ivy-rich-switch-buffer-shorten-path
                                 candidate (ivy-rich-minibuffer-width 0.3))))))
           :predicate (lambda (candidate)
                        (get-buffer candidate))
           :delimiter "	")
          counsel-M-x
          (:columns ((all-the-icons-ivy-rich-function-icon)
                     (counsel-M-x-transformer
                      (:width 50))
                     (ivy-rich-counsel-function-docstring
                      (:face font-lock-doc-face))))
          counsel-describe-function
          (:columns ((all-the-icons-ivy-rich-function-icon)
                     (counsel-describe-function-transformer
                      (:width 50))
                     (ivy-rich-counsel-function-docstring
                      (:face font-lock-doc-face))))
          counsel-describe-variable
          (:columns ((all-the-icons-ivy-rich-variable-icon
                      (:width 0.01))
                     (counsel-describe-variable-transformer
                      (:width 0.29))
                     (ivy-rich-counsel-variable-docstring
                      (:width 0.5
                       :face font-lock-doc-face))))
          counsel-set-variable
          (:columns ((all-the-icons-ivy-rich-variable-icon)
                     (counsel-describe-variable-transformer
                      (:width 50))
                     (ivy-rich-counsel-variable-docstring
                      (:face font-lock-doc-face))))
          counsel-apropos
          (:columns ((all-the-icons-ivy-rich-symbol-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-info-lookup-symbol
          (:columns ((all-the-icons-ivy-rich-symbol-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-descbinds
          (:columns ((all-the-icons-ivy-rich-keybinding-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-find-file
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-read-file-transformer))
           :delimiter "	")
          counsel-file-jump
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-dired
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-read-file-transformer))
           :delimiter "	")
          counsel-dired-jump
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-el
          (:columns ((all-the-icons-ivy-rich-symbol-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-fzf
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-git
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-recentf
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate
                      (:width 0.2))
                     (ivy-rich-file-last-modified-time
                      (:face font-lock-comment-face)))
           :delimiter "	")
          counsel-buffer-or-recentf
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (counsel-buffer-or-recentf-transformer
                      (:width 0.2))
                     (ivy-rich-file-last-modified-time
                      (:face font-lock-comment-face)))
           :delimiter "	")
          counsel-bookmark
          (:columns ((ivy-rich-bookmark-type)
                     (all-the-icons-ivy-rich-bookmark-name
                      (:width 40))
                     (ivy-rich-bookmark-info))
           :delimiter "	")
          counsel-bookmarked-directory
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-package
          (:columns ((all-the-icons-ivy-rich-package-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-fonts
          (:columns ((all-the-icons-ivy-rich-font-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-major
          (:columns ((all-the-icons-ivy-rich-function-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-find-library
          (:columns ((all-the-icons-ivy-rich-library-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-load-library
          (:columns ((all-the-icons-ivy-rich-library-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-load-theme
          (:columns ((all-the-icons-ivy-rich-theme-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-world-clock
          (:columns ((all-the-icons-ivy-rich-world-clock-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-tramp
          (:columns ((all-the-icons-ivy-rich-tramp-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-git-checkout
          (:columns ((all-the-icons-ivy-rich-git-branch-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-list-processes
          (:columns ((all-the-icons-ivy-rich-process-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-projectile-switch-project
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          projectile-persp-switch-project
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-projectile-find-file
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (counsel-projectile-find-file-transformer))
           :delimiter "	")
          counsel-projectile-find-dir
          (:columns ((all-the-icons-ivy-rich-project-icon)
                     (counsel-projectile-find-dir-transformer))
           :delimiter "	")
          counsel-minor
          (:columns ((all-the-icons-ivy-rich-mode-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          counsel-imenu
          (:columns ((all-the-icons-ivy-rich-imenu-icon)
                     (ivy-rich-candidate))
           :delimiter "	")
          treemacs-projectile
          (:columns ((all-the-icons-ivy-rich-file-icon)
                     (ivy-rich-candidate))
           :delimiter "	")))
  (ivy-rich-mode 1))

all-the-icons-ivy-rich

(use-package all-the-icons-ivy-rich
  :after (ivy-rich all-the-icons)
  :init
  ;; For some reason, if the icon size is 1 their widths are different.
  ;; https://github.com/seagle0128/all-the-icons-ivy-rich/issues/7
  ;;
  ;; Also, setting a size bigger than 0.7 seems to cause candidates in the
  ;; bottom to not appear in the frame.
  (setq all-the-icons-ivy-rich-icon-size 0.7)
  :config
  (all-the-icons-ivy-rich-mode 1))

Movement

combobulate

(use-package emacs
  :preface
  (defun mpereira/combobulate-setup-install-grammars ()
    "Install Tree-sitter grammars if they are absent."
    (interactive)
    (dolist (grammar
             '((css "https://github.com/tree-sitter/tree-sitter-css")
               (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "master" "src"))
               (python "https://github.com/tree-sitter/tree-sitter-python")
               (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src"))
               (yaml "https://github.com/ikatyang/tree-sitter-yaml")))
      (add-to-list 'treesit-language-source-alist grammar)
      ;; Only install `grammar' if we don't already have it
      ;; installed. However, if you want to *update* a grammar then
      ;; this obviously prevents that from happening.
      (unless (treesit-language-available-p (car grammar))
        (treesit-install-language-grammar (car grammar)))))

  ;; Optional, but recommended. Tree-sitter enabled major modes are distinct
  ;; from their ordinary counterparts.
  ;;
  ;; You can remap major modes with `major-mode-remap-alist'. Note that this
  ;; does *not* extend to hooks! Make sure you migrate them also.
  (dolist (mapping '((python-mode . python-ts-mode)
                     (css-mode . css-ts-mode)
                     (typescript-mode . tsx-ts-mode)
                     (js-mode . js-ts-mode)
                     (css-mode . css-ts-mode)
                     (yaml-mode . yaml-ts-mode)))
    (add-to-list 'major-mode-remap-alist mapping))

  :config
  (mpereira/combobulate-setup-install-grammars)
  (use-package combobulate
    :hook ((python-ts-mode-hook . combobulate-mode)
           (js-ts-mode-hook . combobulate-mode)
           (css-ts-mode-hook . combobulate-mode)
           (yaml-ts-mode-hook . combobulate-mode)
           (typescript-ts-mode-hook . combobulate-mode)
           (tsx-ts-mode-hook . combobulate-mode))
    :load-path ((expand-file-name "~/git/combobulate"))
    :config
    (setq combobulate-flash-node nil)

    (general-define-key
     :keymaps '(mpereira/combobulate-mode-map)
     :states '(normal visual)
     "+" #'combobulate-mark-node-dwim
     "{" #'combobulate-navigate-beginning-of-defun
     "}" #'combobulate-navigate-end-of-defun
     "(" #'combobulate-navigate-up-list-maybe
     ")" #'combobulate-navigate-down
     "B" #'combobulate-navigate-logical-previous
     "C-j" #'combobulate-navigate-next
     "C-k" #'combobulate-navigate-previous
     "E" #'combobulate-navigate-logical-next
     "W" #'combobulate-navigate-forward
     "C-S-j" #'combobulate-drag-down
     "C-S-k" #'combobulate-drag-up
     "C-k" #'combobulate-navigate-previous)

    (general-define-key
     :keymaps '(mpereira/combobulate-mode-map)
     :states '(normal visual)
     :prefix mpereira/leader
     "r" #'combobulate-splice-up
     "m" #'combobulate-mark-node-dwim
     "R" #'combobulate-vanish-node
     "k" #'combobulate-kill-node-dwim
     "(" #'combobulate-envelop-tsx-ts-mode-wrap-parentheses
     "<" #'combobulate-envelop-tsx-ts-mode-tag
     "{" #'combobulate-envelop-tsx-ts-mode-expression
     "c" #'combobulate-clone-node-dwim)))

;; Main use is to have my key bindings have the highest priority
;; https://github.com/kaushalmodi/.emacs.d/blob/master/elisp/modi-mode.el
(defvar mpereira/combobulate-mode-map (make-sparse-keymap)
  "Keymap for `mpereira/combobulate-mode'.")

;;;###autoload
(define-minor-mode mpereira/combobulate-mode
  "A minor mode so that my key settings override annoying major modes."
  ;; If init-value is not set to t, this mode does not get enabled in
  ;; `fundamental-mode' buffers even after doing \"(global-mpereira-combobulate-mode 1)\".
  ;; More info: http://emacs.stackexchange.com/q/16693/115
  :init-value nil
  :lighter " mpereira/combobulate-mode"
  :keymap mpereira/combobulate-mode-map)

;;;###autoload
(define-globalized-minor-mode global-mpereira-combobulate-mode
  mpereira/combobulate-mode
  mpereira/combobulate-mode)

;; The keymaps in `emulation-mode-map-alists' take precedence over
;; `minor-mode-map-alist'
(add-to-list 'emulation-mode-map-alists `((mpereira/combobulate-mode . ,mpereira/combobulate-mode-map)))

(defun mpereira/turn-off-mpereira-combobulate-mode ()
  "Turn off `mpereira/combobulate-mode'."
  (mpereira/combobulate-mode -1))

(add-hook 'minibuffer-setup-hook #'mpereira/turn-off-mpereira-combobulate-mode)

(dolist (hook '(python-ts-mode-hook
                css-ts-mode-hook
                tsx-ts-mode-hook
                js-ts-mode-hook
                css-ts-mode-hook
                yaml-ts-mode-hook))
  (add-hook hook #'mpereira/combobulate-mode))

(provide 'mpereira/combobulate-mode)

bm

(use-package bm)

avy

(use-package avy
  :config
  (setq avy-all-windows nil))

goto-address-mode

(general-define-key
 :keymaps '(goto-address-highlight-keymap)
 "C-c C-o" #'goto-address-at-point)

(add-hook 'prog-mode-hook #'goto-address-prog-mode)

dumb-jump

(use-package dumb-jump
  :config
  (setq dumb-jump-selector 'ivy))

frog-jump-buffer

(use-package frog-jump-buffer
  :ensure nil
  :quelpa (frog-jump-buffer
           :fetcher github
           :repo "waymondo/frog-jump-buffer"))

link-hint

(use-package link-hint)

Text search and manipulation

swiper

(use-package swiper
  :custom
  (swiper-action-recenter t))

ripgrep

(use-package rg
  :general (:keymaps '(rg-mode-map)
            :states '(normal visual)
            "<" 'rg-back-history
            ">" 'rg-forward-history
            "C-j" 'rg-next-file
            "C-k" 'rg-prev-file
            "G" 'evil-goto-line
            "gg" 'evil-goto-first-line
            "gr" 'rg-recompile)
  :config
  (setq rg-executable "rg")
  (setq rg-group-result t))

wgrep

(use-package wgrep
  :config
  (setq wgrep-auto-save-buffer t))

double-saber

(use-package double-saber
  :after (rg ivy)
  :general (:keymaps '(double-saber-mode-map)
            :states '(normal visual)
            "C-r" 'double-saber-redo
            "u" 'double-saber-undo
            "D" 'double-saber-delete
            "F" 'double-saber-narrow
            "T" '(lambda ()
                   (interactive)
                   (setq rg-group-result (not rg-group-result))
                   (rg-rerun))
            "S" 'double-saber-sort-lines)
  :hook ((rg-mode-hook . (lambda ()
                           (double-saber-mode)
                           (setq-local double-saber-start-line 6)
                           (setq-local double-saber-end-text "rg finished")))
         (grep-mode-hook . (lambda ()
                             (double-saber-mode)
                             (setq-local double-saber-start-line 5)
                             (setq-local double-saber-end-text "Grep finished")))
         (ivy-occur-grep-mode-hook . (lambda ()
                                       (double-saber-mode)
                                       (setq-local double-saber-start-line 5)))))

symbol-overlay

(use-package symbol-overlay)

expand-region

(use-package expand-region
  :config
  (general-define-key
   :states '(normal visual)
   "+" 'er/expand-region))

ialign

(use-package ialign)

yasnippet

I can’t get this to plan nice with `company-mode`, so I’m disabling it.

(use-package yasnippet
  :disabled
  :config
  (yas-reload-all)
  (add-hook 'prog-mode-hook #'yas-minor-mode))

yasnippet-snippets

(use-package yasnippet-snippets
  :disabled
  :after yasnippet)

(use-package doom-snippets
  :disabled
  :ensure nil
  :after yasnippet
  :quelpa (doom-snippets
           :fetcher github
           :repo "hlissner/doom-snippets"))

Add yasnippet support for all company backends

Got this from here.

Not tangled.

(defvar mpereira/company-mode-enable-yas t
  "Enable yasnippet for all backends.")

(defun mpereira/company-mode-add-yas-backend (backend)
  (if (or (not mpereira/company-mode-enable-yas)
          (and (listp backend)
               (member 'company-yasnippet backend)))
      backend
    (append (if (consp backend) backend (list backend))
            '(:with company-yasnippet))))

(setq company-backends (mapcar #'mpereira/company-mode-add-yas-backend
                               company-backends))

electric-pair-mode

Automatically close brackets, parens, etc. Bundled with Emacs.

(use-package elec-pair
  :config
  (electric-pair-mode 1))

undo-tree

(dolist (hook '(undo-tree-mode-hook
                undo-tree-visualizer-mode-hook))
  (add-hook hook 'mpereira/hide-trailing-whitespace))

(setq undo-tree-auto-save-history t)
(setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/undo")))
(setq undo-limit (* 100 1024 1024)) ;; 100MB.
(setq undo-strong-limit undo-limit)
(setq undo-tree-visualizer-timestamps t)
(setq undo-tree-visualizer-diff t)

(global-undo-tree-mode 1)

(defun undo-tree-visualizer-show-diff (&optional node)
  (setq undo-tree-visualizer-diff t)
  (let ((diff-buffer (with-current-buffer undo-tree-visualizer-parent-buffer
		                   (undo-tree-diff node)))
	      (display-buffer-mark-dedicated 'soft))
    (display-buffer diff-buffer)))

(defun undo-tree-visualizer-hide-diff ()
  (setq undo-tree-visualizer-diff nil)
  (when-let ((diff-buffer-window (get-buffer-window undo-tree-diff-buffer-name)))
    (with-selected-window diff-buffer-window
      (kill-buffer-and-window))))

(defun undo-tree-visualizer-update-diff (&optional node)
  (with-current-buffer undo-tree-visualizer-parent-buffer
    (undo-tree-diff node)))

move-text

(use-package move-text)

unfill

(use-package unfill)

string-inflection

(use-package string-inflection)

string-edit-at-point

(use-package string-edit-at-point)

format-all

Keep an eye on LSP support.

(use-package format-all)

blacken

(use-package blacken
  :config
  ;; Use a `blacken-buffer' that doesn't randomly move the point to the
  ;; beginning of the buffer.
  ;; https://github.com/pythonic-emacs/blacken/pull/19/files
  (defun mpereira/blacken-buffer (&optional display)
    "Try to blacken the current buffer.
Show black output, if black exit abnormally and DISPLAY is t."
    (interactive (list t))
    (let* ((original-buffer (current-buffer))
           (tmpbuf (get-buffer-create "*blacken*"))
           (errbuf (get-buffer-create "*blacken-error*")))
      ;; This buffer can be left after previous black invocation.  It
      ;; can contain error message of the previous run.
      (dolist (buf (list tmpbuf errbuf))
        (with-current-buffer buf
          (erase-buffer)))
      (condition-case err
          (if (not (zerop (blacken-call-bin original-buffer tmpbuf errbuf)))
              (error "Black failed, see %s buffer for details" (buffer-name errbuf))
            (unless (eq (compare-buffer-substrings tmpbuf nil nil original-buffer nil nil) 0)
              (with-current-buffer original-buffer (replace-buffer-contents tmpbuf)))
            (mapc 'kill-buffer (list tmpbuf errbuf)))
        (error (message "%s" (error-message-string err))
               (when display
                 (pop-to-buffer errbuf))))))

  (general-define-key
   :keymaps '(python-mode-map)
   :states '(normal visual)
   :prefix mpereira/leader
   "F" 'mpereira/blacken-buffer)

  (setq blacken-line-length 'fill))

prettier

(use-package prettier-js)

git

Git operations via SSH require SSH keys to be added to the ssh-agent. At least in my macOS system, SSH keys with default names are added automatically added to the agent.

magit

(use-package magit
  :config
  (setq magit-diff-refine-hunk 'all)
  (setq magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1)
  (setq magit-prefer-remote-upstream t)
  (setq magit-refresh-verbose t)
  (setq magit-bury-buffer-function 'magit-restore-window-configuration)

  (add-to-list 'magit-no-confirm 'stage-all-changes)

  ;; https://github.com/magit/magit/issues/2872#issuecomment-291011191
  (setq magit-list-refs-sortby "-creatordate")

  ;; ediff starts by default in a new frame. Don't do that.
  (setq ediff-window-setup-function 'ediff-setup-windows-plain)

  (defun mpereira/magit-center-buffer-contents ()
    (interactive)
    (setq-local olivetti-body-width mpereira/magit-status-width)
    (olivetti-mode))

  (add-hook 'magit-status-mode-hook 'mpereira/magit-center-buffer-contents)
  (add-hook 'magit-log-mode-hook 'mpereira/magit-center-buffer-contents)

  (setq magit-blame-styles '((headings
                              (heading-format . "%-20a %C %s\n"))
                             (margin
                              (margin-format " %s%f" " %C %a" " %H")
                              (margin-width . 60)
                              (margin-face . magit-blame-margin)
                              (margin-body-face magit-blame-dimmed))))

  (transient-bind-q-to-quit)

  ;; "q" is being bound to `quit-window' on the magit status buffer, for some
  ;; reason. It doesn't seem to be coming from transient.
  (general-define-key
   :keymaps '(magit-status-mode-map)
   :states '(normal visual)
   "q" #'magit-mode-bury-buffer)

  ;; Originally `magit-log-refresh'. Disabling it so that `evil-window-bottom'
  ;; gets called instead. Narrowing to normal and visual states doesn't work.
  (general-define-key
   :keymaps '(magit-status-mode-map)
   "L" nil)

  (general-define-key
   :keymaps '(magit-mode-map)
   :states '(normal visual)
   "<" #'magit-go-backward
   ">" #'magit-go-forward
   "zb" #'evil-scroll-line-to-bottom
   "zt" #'evil-scroll-line-to-top
   "zz" #'evil-scroll-line-to-center))

libegit2 and magit-libjit

I was using my fork until the PR making it work on macOS got merged, but decided to disable it since the project doesn’t appear to be in active development.

(use-package libgit
  :disabled
  :ensure nil
  :quelpa (libgit
           :fetcher github
           :repo "mpereira/libegit2"))

(use-package magit-libgit
  :disabled
  :after (magit libgit))

forge

(use-package forge
  :after (magit)
  :init
  (setq ghub-use-workaround-for-emacs-bug nil)
  :config
  (general-define-key
   :keymaps '(forge-post-mode-map
              forge-topic-mode-map
              forge-post-section-map
              forge-issue-section-map
              forge-issues-section-map
              forge-pullreq-section-map
              forge-topic-list-mode-map
              forge-issue-list-mode-map
              forge-pullreqs-section-map
              forge-pullreq-list-mode-map
              forge-forge-repo-section-map
              forge-notifications-mode-map
              forge-topic-state-section-map
              forge-topic-marks-section-map
              forge-topic-title-section-map
              forge-repository-list-mode-map
              forge-topic-labels-section-map
              forge-topic-assignees-section-map
              forge-topic-review-requests-section-map)
   :states '(normal visual)
   "yb" 'forge-copy-url-at-point-as-kill)

  (general-define-key
   :keymaps '(forge-post-mode-map
              forge-topic-mode-map
              forge-post-section-map
              forge-issue-section-map
              forge-issues-section-map
              forge-pullreq-section-map
              forge-topic-list-mode-map
              forge-issue-list-mode-map
              forge-pullreqs-section-map
              forge-pullreq-list-mode-map
              forge-forge-repo-section-map
              forge-notifications-mode-map
              forge-topic-state-section-map
              forge-topic-marks-section-map
              forge-topic-title-section-map
              forge-repository-list-mode-map
              forge-topic-labels-section-map
              forge-topic-assignees-section-map
              forge-topic-review-requests-section-map)
   :states '(normal visual)
   :prefix mpereira/leader
   "go" 'forge-browse-dwim)

  (general-define-key
   :keymaps '(forge-topic-mode-map
              forge-topic-list-mode-map
              forge-topic-state-section-map
              forge-topic-marks-section-map
              forge-topic-title-section-map)
   :states '(normal visual)
   :prefix mpereira/leader
   "go" 'forge-browse-topic)

  (general-define-key
   :keymaps '(forge-post-mode-map
              forge-post-section-map
              forge-topic-list-mode-map
              forge-topic-state-section-map
              forge-topic-marks-section-map
              forge-topic-title-section-map)
   :states '(normal visual)
   :prefix mpereira/leader
   "go" 'forge-browse-post))

magit-todos

(use-package magit-todos
  ;; Disabled on 2020-05-05.
  :disabled
  :ensure nil
  :quelpa (magit-todos
           :fetcher github
           :repo "alphapapa/magit-todos")
  :after (magit ivy)
  :config
  ;; Only show magit-todos on non-TRAMP magit status buffers.
  (add-hook 'magit-status-mode-hook
            (lambda ()
              (when (not (ivy--remote-buffer-p (current-buffer)))
                (let ((magit-todos-mode t))
                  (magit-todos-update))))))

A VSCode git lens type of thing

(use-package hydra)

(use-package hydra-posframe
  :ensure nil
  :custom
  (hydra-posframe-border-width 10)

  (hydra-posframe-poshandler 'posframe-poshandler-point-bottom-left-corner-upward)
  :quelpa (hydra-posframe
           :fetcher github
           :repo "Ladicle/hydra-posframe")
  :config
  (hydra-posframe-mode)

  (custom-set-faces
   '(hydra-posframe-face ((t (:inherit ivy-posframe))))
   '(hydra-posframe-border-face ((t (:inherit ivy-posframe-border))))))

(use-package git-messenger
  :custom
  (git-messenger:show-detail t)
  (git-messenger:use-magit-popup t)

  :config
  (general-define-key
   :keymaps '(git-messenger-map)
   "(" 'git-messenger:show-parent)

  (defhydra git-messenger-hydra (:color blue)
    ("d" git-messenger:popup-show "Show diff")
    ("y" git-messenger:copy-commit-id "Yank SHA")
    ("m" git-messenger:copy-message "Yank message")
    ("(" (catch 'git-messenger-loop (git-messenger:show-parent)) "Go to parent")
    ("q" git-messenger:popup-close "Quit"))

  (defun mpereira/git-messenger-format-message (vcs commit-id commit-author message)
    (if (eq vcs 'git)
        (let ((date (git-messenger:commit-date commit-id))
              (colon (propertize ":" 'face 'font-lock-comment-face)))
          (concat
           (format "%s%s %s \n%s%s %s\n%s  %s %s \n"
                   (propertize "Commit" 'face 'font-lock-keyword-face) colon
                   (propertize (substring commit-id 0 8) 'face 'font-lock-comment-face)
                   (propertize "Author" 'face 'font-lock-keyword-face) colon
                   (propertize commit-author 'face 'font-lock-string-face)
                   (propertize "Date" 'face 'font-lock-keyword-face) colon
                   (propertize date 'face 'font-lock-string-face))
           (propertize (make-string 38 ?─) 'face 'font-lock-comment-face)
           message))
      (git-messenger:format-detail vcs commit-id commit-author message)))

  ;; FIXME: going to parent isn't working.
  (defun mpereira/git-messenger-show ()
    "TODO: docstring."
    (interactive)
    (let* ((vcs (git-messenger:find-vcs))
           (file (buffer-file-name (buffer-base-buffer)))
           (line (line-number-at-pos))
           (commit-info (git-messenger:commit-info-at-line vcs file line))
           (commit-id (car commit-info))
           (commit-author (cdr commit-info))
           (commit-message (git-messenger:commit-message vcs commit-id))
           (detailed-message (if (git-messenger:show-detail-p commit-id)
                                 (mpereira/git-messenger-format-message
                                  vcs commit-id commit-author commit-message)
                               commit-message)))
      (setq git-messenger:vcs vcs
            git-messenger:last-message commit-message
            git-messenger:last-commit-id commit-id)
      (run-hook-with-args 'git-messenger:before-popup-hook detailed-message)
      (git-messenger-hydra/body)
      (cond ((and (fboundp 'posframe-workable-p) (posframe-workable-p))
             (let ((buffer-name "*git-messenger*"))
               ;; TODO: reuse frame.
               (posframe-show buffer-name
                              :string detailed-message
                              :left-fringe 8
                              :right-fringe 8
                              :background-color (face-attribute 'ivy-posframe :background nil t)
                              :foreground-color (face-attribute 'ivy-posframe :foreground nil t)
                              :internal-border-color (face-attribute 'ivy-posframe-border
                                                                     :background
                                                                     nil
                                                                     t)
                              :internal-border-width ivy-posframe-border-width)
               (unwind-protect
                   (push (read-event) unread-command-events)
                 (posframe-delete buffer-name))))
            (t (message "%s" detailed-message)))
      (run-hook-with-args 'git-messenger:after-popup-hook detailed-message))
    (advice-add #'git-messenger:popup-close :override #'ignore)
    (advice-add #'git-messenger:popup-message :override #'mpereira/git-messenger-show)))

gist

The “gist list” buffer is unfortunately based on tabulated-mode instead of tablist-mode. The keybindings below make it behave similar to what one would expect from an evil mode buffer.

gist.el depends on a GitHub personal access token set in ~/.gitconfig.

(use-package gist
  :after evil
  :init
  (evil-set-initial-state 'gist-list-mode 'normal)

  (setq gist-list-format '((id "ID" 8 nil identity)
                           (files "Files" 40 t car)
                           (created "Created" 15 t "%D %R")
                           (visibility "Visibility" 10 nil
                                       (lambda (public)
                                         (or (and public "public")
                                             "private")))
                           (description "Description" 0 t identity)))
  :config
  (defun mpereira/gist-fetch-current ()
    "TODO: docstring."
    (interactive)
    (cl-letf (((symbol-function 'switch-to-buffer-other-window) #'switch-to-buffer)
              (original-ibuffer (symbol-function 'ibuffer))
              ((symbol-function 'ibuffer) #'(lambda (&optional other-window-p
			                                                         name
                                                               qualifiers
			                                                         noselect
                                                               shrink
			                                                         filter-groups
			                                                         formats)
                                              (funcall original-ibuffer
                                                       nil ; other-window-p
                                                       name
                                                       qualifiers
                                                       noselect
                                                       shrink
                                                       filter-groups
                                                       formats))))
      (gist-fetch-current)))

  (general-define-key
   :keymaps '(gist-list-menu-mode-map)
   "<tab>" nil                          ; originally `gist-fetch-current-noselect'.
   "<backtab>" nil)                     ; originally `backward-button'.

  (general-define-key
   :keymaps '(gist-list-menu-mode-map)
   :states '(normal visual)
   "<return>" 'mpereira/gist-fetch-current
   "D" 'gist-kill-current
   "go" 'gist-browse-current-url
   "gr" 'gist-list-reload))

git-modes

(use-package git-modes)

git-timemachine

(use-package git-timemachine)

browse-at-remote

(use-package browse-at-remote
  :config
  ;; Permanent SHA link.
  (setq browse-at-remote-prefer-symbolic nil)

  ;; Browse file with no lines if no lines are selected.
  (setq browse-at-remote-add-line-number-if-no-region-selected nil)

  (defun mpereira/browse-at-remote ()
    "TODO: docstring."
    (interactive)
    (let ((url (browse-at-remote-get-url)))
      (kill-new url)
      (browse-url url))))

Software development

flycheck

(use-package flycheck
  :config
  (general-define-key
   :keymaps '(flycheck-mode-map)
   :states '(normal visual)
   :prefix mpereira/leader
   :infix "1"
   "c" 'flycheck-buffer
   "e" 'flycheck-explain-error-at-point
   "h" 'flycheck-display-error-at-point
   "j" 'flycheck-next-error
   "k" 'flycheck-previous-error
   "l" 'flycheck-list-errors
   "n" 'flycheck-next-error
   "p" 'flycheck-previous-error
   "y" 'flycheck-copy-errors-as-kill)

  (setq flycheck-display-errors-delay 0.3)
  (setq-default flycheck-emacs-lisp-load-path 'inherit)

  (setq flycheck-error-list-format
        `[("File" 12)
          ("Line" 5 flycheck-error-list-entry-< :right-align t)
          ("Col" 3 nil :right-align t)
          ("Level" 25 flycheck-error-list-entry-level-<)
          ("ID" 5 t)
          (,(flycheck-error-list-make-last-column "Message" 'Checker) 0 t)])
  (setq flycheck--error-list-msg-offset (+ (-sum (mapcar (lambda (x) (nth 1 x))
                                                         flycheck-error-list-format))
                                           (- (length flycheck-error-list-format)
                                              2)))

  (add-hook 'prog-mode-hook #'flycheck-mode)

  (defun mpereira/disable-emacs-lisp-checkdoc-checker ()
    "TODO: docstring."
    (interactive)
    (setq-local flycheck-disabled-checkers '(emacs-lisp-checkdoc)))

  (add-hook 'org-src-mode-hook #'mpereira/disable-emacs-lisp-checkdoc-checker))

LSP

Install some dependencies:

  • Rust Analyzer
    curl -L https://github.com/rust-analyzer/rust-analyzer/releases/latest/download/rust-analyzer-mac \
         -o /usr/local/bin/rust-analyzer \
      && chmod +x /usr/local/bin/rust-analyzer
        
    • Some crates for Rust
    rustup component add rustfmt rust-src rust-docs clippy
        
  • Clojure Language Server
    wget https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-macos-amd64.zip
    unzip clojure-lsp-native-macos-amd64.zip
    mv clojure-lsp /usr/local/bin
    rm -rf clojure-lsp-native-macos-amd64.zip
        

- theia-ide/typescript-language-server for JavaScript.

npm i -g typescript-language-server \
        typescript \
  • pyright for Python.
    npm i -g pyright
        
  • LLVM for C/C++. You might need to configure PATH, LDFLAGS, and CPPFLAGS.
    brew install llvm
        

lsp-mode

(use-package lsp-mode
  :config
  (setq mpereira/lsp-disabled-modes '(emacs-lisp-mode
                                      lisp-data-mode))

  (defun mpereira/maybe-enable-lsp ()
    "TODO: docstring."
    (interactive)
    (when (not (-contains? mpereira/lsp-disabled-modes major-mode))
      (lsp)))

  (add-hook 'prog-mode-hook #'mpereira/maybe-enable-lsp)

  ;; Only enable this for debugging. It makes LSP buffers really slow.
  ;; (setq lsp-log-io t)

  (setq lsp-enable-symbol-highlighting nil)

  (setq lsp-headerline-breadcrumb-enable nil)

  (setq lsp-lens-enable nil)

  (setq lsp-eldoc-enable-hover nil)

  (general-define-key
   :keymaps '(lsp-mode-map)
   :states '(normal visual)
   "C-9" 'lsp-ui-doc-glance
   "K" 'lsp-describe-thing-at-point)

  (general-define-key
   :keymaps '(lsp-ui-doc-frame-mode-map)
   :states '(normal visual)
   "q" 'lsp-ui-doc-unfocus-frame
   "C-9" 'lsp-ui-doc-unfocus-frame)

  (general-define-key
   :keymaps '(lsp-mode-map)
   :states '(normal visual)
   :prefix "g"
   "a" 'lsp-execute-code-action
   "A" 'lsp-ui-sideline-apply-code-actions
   "d" 'lsp-find-definition
   "E" 'lsp-find-references
   "F" 'lsp-format-buffer
   "e" 'lsp-ui-peek-find-references
   "r" 'lsp-rename
   "S" 'lsp-ivy-global-workspace-symbol
   "s" 'lsp-ivy-workspace-symbol
   "n" 'lsp-organize-imports)

  (general-define-key
   :keymaps '(rust-mode-map)
   :states '(normal visual)
   :prefix "g"
   "r" 'lsp-rust-analyzer-rerun
   "R" 'lsp-rust-analyzer-run)

  (general-define-key
   :keymaps '(lsp-mode-map)
   :states '(normal visual)
   :prefix mpereira/leader
   "F" 'lsp-format-buffer)

  ;; Binding this on visual/normal state causes the peek view to be aborted on
  ;; keypress.
  (general-define-key
   :keymaps '(lsp-ui-peek-mode-map)
   "C-j" 'lsp-ui-peek--select-next
   "C-k" 'lsp-ui-peek--select-prev))

lsp-ui

(use-package lsp-ui
  :after lsp-mode
  :init
  (setq lsp-ui-doc-delay 0)
  (setq lsp-ui-doc-enable t)
  (setq lsp-ui-doc-use-webkit t)
  (setq lsp-ui-doc-position 'at-point)

  (setq lsp-ui-peek-always-show t)
  (setq lsp-ui-peek-list-width 40)
  (setq lsp-ui-peek-show-directory nil)
  ;; Expand all peek folds.
  (setq lsp-ui-peek-expand-function (lambda (xs) (mapcar #'car xs)))

  (setq lsp-ui-sideline-enable t)
  (setq lsp-ui-sideline-show-diagnostics t)
  (setq lsp-ui-sideline-show-hover nil)
  (setq lsp-ui-sideline-update-mode 'line)
  :config
  (add-hook 'lsp-mode-hook 'lsp-ui-mode))

dap-mode

(use-package dap-mode)

lsp-pyright

(use-package lsp-pyright
  :after lsp-mode
  :init
  (require 'lsp-pyright)
  :config
  (setq lsp-pyright-diagnostic-mode "workspace")

  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-tramp-connection (lambda ()
                                            (cons "pyright-langserver"
                                                  lsp-pyright-langserver-command-args)))
    :major-modes '(python-mode)
    :remote? t
    :server-id 'pyright-remote
    :multi-root t
    :priority 3
    :initialization-options (lambda ()
                              (ht-merge (lsp-configuration-section "python")
                                        (lsp-configuration-section "pyright")))
    :initialized-fn (lambda (workspace)
                      (with-lsp-workspace workspace
                        (lsp--set-configuration
                         (ht-merge (lsp-configuration-section "pyright")
                                   (lsp-configuration-section "python")))))
    :download-server-fn (lambda (_client callback error-callback _update?)
                          (lsp-package-ensure 'pyright callback error-callback))
    :notification-handlers (lsp-ht
                            ("pyright/beginProgress" 'lsp-pyright--begin-progress-callback)
                            ("pyright/reportProgress" 'lsp-pyright--report-progress-callback)
                            ("pyright/endProgress" 'lsp-pyright--end-progress-callback)))))

lsp-pyls

lsp-pyls is included with lsp-mode.

(use-package lsp-python-ms
  :after lsp-mode
  :init
  (require 'lsp-pyls)
  (lsp-register-client
   (make-lsp-client :new-connection (lsp-tramp-connection
                                     (executable-find
                                      (car lsp-pyls-server-command)))
                    :major-modes '(python-mode)
                    :priority -1
                    :remote? t
                    :server-id 'pyls-remote)))

lsp-python-ms

(use-package lsp-python-ms
  :after lsp-mode
  :init
  (setq lsp-python-ms-auto-install-server t)
  (setq lsp-python-ms-python-executable-cmd "python3")
  :config
  (lsp-register-client
   (make-lsp-client :new-connection (lsp-tramp-connection lsp-python-ms-executable)
                    :major-modes '(python-mode)
                    :remote? t
                    :server-id 'mypyls-remote)))

lsp-ivy

(use-package lsp-ivy
  :after (ivy lsp-mode)
  :commands (lsp-ivy-workspace-symbol
             lsp-ivy-global-workspace-symbol))

company

(use-package company-tabnine
  :disabled
  :config
  (require 'company-tabnine))

(use-package company
  :config
  (setq company-global-modes '(not comint-mode
                                   eshell-mode
                                   help-mode
                                   message-mode
                                   org-mode))

  (add-hook 'after-init-hook 'global-company-mode)

  (setq company-backends '(company-capf
                           company-cmake
                           company-clang
                           company-files
                           (company-dabbrev-code company-gtags company-etags company-keywords)
                           company-dabbrev))

  (setq company-idle-delay nil)
  (setq company-require-match 'never)
  (setq company-tooltip-align-annotations t)
  ;; Prescient will prepend `company-prescient-transformer' to this.
  (setq company-transformers '(company-sort-by-backend-importance))
  ;; company-box will set this to a single item list containing
  ;; `company-box-frontend'.
  (setq company-frontends '(company-pseudo-tooltip-frontend))

  (general-define-key
   :keymaps '(company-mode-map)
   :states '(insert)
   "<tab>" 'company-complete
   "<backtab>" 'counsel-company)

  (general-define-key
   :keymaps '(company-search-map)
   "<tab>" 'company-search-abort)

  ;; NOTE: these are an increment over the mappings already set by
  ;; evil-collection.
  (general-define-key
   :keymaps '(company-active-map)
   "C-/" 'company-filter-candidates
   "C-b" 'company-previous-page
   "C-f" 'company-next-page
   "RET" 'company-complete-selection
   "TAB" 'company-abort))

aggressive-indent

(use-package aggressive-indent
  :config
  (setq aggressive-indent-excluded-buffers nil)
  (add-to-list 'aggressive-indent-excluded-buffers "*outorg-edit-buffer*")
  (add-to-list 'aggressive-indent-excluded-modes 'sql-mode)
  (add-to-list 'aggressive-indent-excluded-modes 'makefile-bsdmake-mode)
  (add-to-list 'aggressive-indent-excluded-modes 'org-mode)
  (add-hook 'prog-mode-hook #'mpereira/maybe-enable-aggressive-indent-mode))

LISP

lispy

(use-package lispy
  :config
  (add-hook 'emacs-lisp-mode-hook 'lispy-mode)
  (add-hook 'clojure-mode-hook 'lispy-mode)

  ;; REVIEW: can this be removed?
  ;;
  ;; semantic-mode is disabled by lispy for $reasons. Sometimes it fails to also
  ;; disable this timer, which keeps printing the following error message in the
  ;; echo area:
  ;;
  ;; Error running timer `semantic-idle-scheduler-function'
  ;; (error "Unmatched Text during Lexical Analysis")
  ;;
  ;; So here we disable the timer manually. Check
  ;; https://github.com/abo-abo/lispy/issues/473 for more context.
  (advice-add 'semantic-idle-scheduler-function :around #'ignore)

  ;; Disable all non-evil lispy mappings.
  ;; NOTE: setting `lispy-key-theme' to nil during :init or :custom doesn't work
  ;; to achieve this.
  (lispy-set-key-theme nil)

  ;; `lispy-fill' doesn't handle filling comments really well. Override it.
  (general-define-key
   :keymaps 'lispy-mode-map
   "M-q" 'unfill-toggle)

  (general-define-key
   :keymaps 'lispy-mode-map
   :states '(insert)
   "<backspace>" 'lispy-delete-backward
   "<deletechar>" 'lispy-delete
   ")" 'lispy-right-nostring
   "\"" 'lispy-doublequote
   "[" 'lispy-brackets
   "]" 'lispy-close-square
   "{" 'lispy-braces
   "}" 'lispy-close-curly)

  (general-define-key
   :keymaps 'lispy-mode-map
   :states '(normal)
   :prefix mpereira/leader
   "fo" 'counsel-imenu
   "r" 'lispy-raise-sexp
   "R" 'lispy-raise-some
   "(" 'lispy-wrap-round
   "[" 'lispy-wrap-brackets
   "{" 'lispy-wrap-braces
   "c" 'lispy-clone))

lispyville

(use-package lispyville
  :after (evil lispy)
  :config
  (add-hook 'lispy-mode-hook 'lispyville-mode)

  (lispyville-set-key-theme '(operators))

  (general-define-key
   :keymaps '(lispyville-mode-map)
   :states '(insert)
   "ESC" 'lispyville-normal-state)

  (general-define-key
   :keymaps '(lispyville-mode-map)
   :states '(normal visual)
   "(" 'lispyville-backward-up-list
   ")" 'lispyville-up-list
   "B" 'mpereira/backward-sexp-begin
   "C-(" 'lispyville-beginning-of-defun
   "C-)" 'lispyville-end-of-defun
   "C-j" 'mpereira/forward-sexp-begin
   "C-k" 'mpereira/backward-sexp-begin
   "E" 'mpereira/forward-sexp-end
   "W" 'mpereira/forward-sexp-begin
   "{" 'lispy-knight-up
   "}" 'lispy-knight-down)

  (general-define-key
   :keymaps '(lispyville-mode-map)
   :states '(normal)
   "S" 'lispyville-change-whole-line
   "gA" 'mpereira/append-to-end-of-list
   "gI" 'mpereira/insert-to-beginning-of-list
   "go" 'lispy-oneline
   "gm" 'lispy-multiline
   "gs" 'lispy-stringify
   "gS" 'lispy-unstringify
   "gt" 'lispy-teleport
   ">)" 'lispy-forward-slurp-sexp
   "<)" 'lispy-forward-barf-sexp
   "<(" 'lispy-backward-slurp-sexp
   ">(" 'lispy-backward-barf-sexp
   "|" 'lispy-split
   "_" 'lispy-join
   "<f" 'lispyville-drag-backward
   ">f" 'lispyville-drag-forward
   "C-9" 'lispy-describe-inline
   "C-0" 'lispy-arglist-inline))

Emacs Lisp

(use-package emacs
  :config
  (add-hook 'emacs-lisp-mode-hook #'mpereira/turn-off-mpereira-combobulate-mode))

(use-package eros
  :config
  (eros-mode 1))

(defun mpereira/eval-buffer ()
  "Evaluate buffer and show confirmation message."
  (interactive)
  (eval-buffer)
  (message "%s" (buffer-name)))

(general-define-key
 :keymaps '(emacs-lisp-mode-map)
 :states '(normal)
 :prefix mpereira/leader
 :infix "e"
 "d" #'edebug-defun
 "e" #'mpereira/eval-thing-at-or-around-point
 "(" #'eval-defun
 "E" #'mpereira/eval-buffer)

(general-define-key
 :keymaps '(emacs-lisp-mode-map)
 :states '(visual)
 :prefix mpereira/leader
 :infix "e"
 "e" 'eval-region)

(general-define-key
 :keymaps '(emacs-lisp-mode-map)
 :states '(normal)
 "C-]" 'xref-find-definitions-other-window
 "K" 'helpful-at-point)

Java

(add-hook 'java-mode-hook
          (lambda ()
            (setq-local whitespace-line-column mpereira/fill-column-wide)
            (setq-local fill-column mpereira/fill-column-wide)
            (setq-local comment-column mpereira/fill-column-wide)))

Clojure

clojure-mode

(use-package clojure-mode
  :config
  ;; aggressive-indent-mode is too slow for editing Clojure.
  (add-to-list 'aggressive-indent-excluded-modes 'clojure-mode))

clj-refactor

(use-package clj-refactor
  :config
  (setq cljr-hotload-dependencies t))

inf-clojure

(use-package inf-clojure)

zprint-mode

(use-package zprint-mode)

cider

(use-package cider
  :config
  (setq cider-prompt-for-symbol nil)
  (setq cider-repl-display-help-banner nil)
  (setq cider-repl-pop-to-buffer-on-connect nil)

  (general-define-key
   :keymaps 'cider-mode-map
   :states '(normal visual)
   "K" 'cider-doc
   "gf" 'cider-find-var)

  (defun mpereira/cider-eval-sexp-at-point (&optional output-to-current-buffer)
    "Evaluate the expression around point.
If invoked with OUTPUT-TO-CURRENT-BUFFER, output the result to current buffer."
    (interactive "P")
    (save-excursion
      (goto-char (- (cadr (cider-sexp-at-point 'bounds))
                    1))
      (cider-eval-last-sexp output-to-current-buffer)))

  (general-define-key
   :keymaps 'cider-mode-map
   :states '(normal)
   :prefix mpereira/leader
   "ee" #'mpereira/cider-eval-sexp-at-point
   "e(" #'cider-eval-defun-at-point
   "F" #'cider-format-buffer
   "eE" #'cider-eval-buffer
   "en" #'cider-eval-ns-form
   "dd" #'cider-debug-defun-at-point
   "tt" #'cider-test-run-test
   "tr" #'cider-test-rerun-test
   "tT" #'cider-test-run-ns-tests
   "tR" #'cider-test-rerun-failed-tests
   "tp" #'cider-test-run-project-tests)

  (general-define-key
   :keymaps 'cider-mode-map
   :states '(visual)
   :prefix mpereira/leader
   "ee" 'cider-eval-region)

  (general-define-key
   :keymaps 'cider-repl-mode-map
   :states '(insert)
   "C-l" 'cider-repl-clear-buffer))

Go

go-mode

(use-package go-mode
  :config
  (general-define-key
   :keymaps 'go-mode-map
   :states '(normal)
   "K" #'godoc-at-point
   "C-]" #'godef-jump)

  (general-define-key
   :keymaps 'go-mode-map
   :states '(normal)
   :prefix mpereira/leader
   "tt" #'go-test-current-test
   "tT" #'go-test-current-file
   "pt" #'go-test-current-project))

Rust

rust-mode

(use-package rust-mode
  :config
  (add-to-list 'aggressive-indent-excluded-modes 'rust-mode))

flycheck-rust

(use-package flycheck-rust
  :after rust-mode
  :config
  (add-hook 'flycheck-mode-hook #'flycheck-rust-setup))

ob-rust

(use-package ob-rust)

Kotlin

kotlin-mode

(use-package kotlin-mode)

flycheck-kotlin

(use-package flycheck-kotlin
  :after kotlin-mode
  :config
  (add-hook 'flycheck-mode-hook #'flycheck-kotlin-setup))

JavaScript

(setq js-indent-level 2)

TypeScript

(use-package typescript-mode
  :custom (typescript-indent-level 2)
  :hook (typescript-mode-hook . lsp))

GraphQL

(use-package graphql-mode)

Shell script

(add-hook 'sh-mode-hook
          (lambda ()
            (setq-local sh-basic-offset 2)
            (setq-local sh-indentation 2)))

Python

Install the dependencies:

python3 -m pip install mypy flake8 pylint black
(with-eval-after-load "elpy"
  (general-define-key
   :keymaps '(inferior-python-mode-map)
   :states '(insert)
   "C-l" 'mpereira/elpy-shell-clear-shell)

  (setq flycheck-python-mypy-executable "mypy")
  (setq flycheck-python-flake8-executable "python3")
  (setq flycheck-python-pylint-executable "python3")
  (setq flycheck-python-pycompile-executable "python3")
  (setq python-shell-interpreter "python3")

  (general-define-key
   :keymaps '(python-mode-map)
   :states '(normal visual)
   "C-j" 'elpy-nav-forward-block
   "C-k" 'elpy-nav-backward-block
   "S-C-j" 'elpy-nav-move-line-or-region-down
   "S-C-k" 'elpy-nav-move-line-or-region-up)

  (general-define-key
   :keymaps '(python-mode-map)
   :states '(normal visual)
   :prefix mpereira/leader
   :infix "e"
   "e" 'elpy-shell-send-statement
   "E" 'elpy-shell-send-buffer
   "p" 'elpy-shell-send-group))

(setq python-indent-offset 4)

(add-hook 'python-mode-hook
          (lambda ()
            (setq evil-shift-width python-indent-offset)))

elpy

(use-package elpy
  :after company
  :init
  (setq elpy-rpc-python-command "python3")

  ;; https://github.com/jorgenschaefer/elpy/issues/1550#issuecomment-468763310
  (setq elpy-shell-echo-output nil)

  (remove-hook 'elpy-modules 'elpy-module-eldoc)
  (remove-hook 'elpy-modules 'elpy-module-django)

  (setq company-idle-delay-before-elpy-fucks-it-up company-idle-delay)
  (add-hook 'elpy-mode-hook
            (lambda ()
              (setq company-idle-delay company-idle-delay-before-elpy-fucks-it-up))))

JSON

json-mode

(use-package json-mode)

json-navigator

Seems to be broken under latest Emacs 28: Lisp error: (void-variable hierarchy--make).

(use-package json-navigator
  :disabled)

json-snatcher

(use-package json-snatcher
  :ensure nil
  :quelpa (json-snatcher
           :fetcher github
           :repo "Sterlingg/json-snatcher")
  :config
  (setq jsons-path-printer #'jsons-print-path-jq))

Scala

scala-mode

(use-package scala-mode)

SQL

(require 'sql)

(add-hook 'sql-interactive-mode-hook (lambda () (toggle-truncate-lines t)))

terraform-mode

(use-package terraform-mode)

docker

(use-package docker)

bazel-mode

(use-package bazel)

groovy-mode

(use-package groovy-mode)

dockerfile-mode

(use-package dockerfile-mode
  :mode "Dockerfile.*\\'")

jinja2-mode

(use-package jinja2-mode)

literate-calc-mode

(use-package literate-calc-mode
  :mode "\\.calc\\'")

Web development

web-mode

(use-package web-mode
  :config
  (add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.mustache\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))

  (setq web-mode-engines-alist '())

  (setq web-mode-markup-indent-offset 2))

auto-rename-tag

(use-package auto-rename-tag
  :config
  (add-hook 'html-mode-hook #'auto-rename-tag-mode))

css

(setq css-indent-offset 2)

lsp-tailwindcss

(use-package lsp-tailwindcss
  :init
  (setq lsp-tailwindcss-add-on-mode t)
  (add-to-list 'lsp-tailwindcss-major-modes 'tsx-ts-mode))

tsx-ts-mode

Still need something like [rjsx-mode](https://github.com/felipeochoa/rjsx-mode)’s [rjsx-comment-dwim](https://github.com/felipeochoa/rjsx-mode/blob/b697fe4d92cc84fa99a7bcb476f815935ea0d919/rjsx-mode.el#L1161).

(use-package emacs
  :mode ("\\.jsx?$" . tsx-ts-mode)
  :config
  ;; NOTE: disable aggressive indent for tsx-ts-mode because I've seen some
  ;; weirdness. Also I wanna give priority to LSP and Prettier automatic
  ;; formatting.
  (add-to-list 'aggressive-indent-excluded-modes 'tsx-ts-mode)
  (add-to-list 'auto-mode-alist '("components\\/.*\\.js\\'" . tsx-ts-mode)))

Infrastructure

kubel

(use-package kubel)

(use-package kubel-evil)

nginx-mode

(use-package nginx-mode)

Multimedia

pdf-tools

(use-package pdf-tools
  :disabled
  :config
  (pdf-tools-install))

(use-package pdf-continuous-scroll-mode
  :disabled
  :ensure nil
  :hook ((pdf-view-mode-hook . pdf-continuous-scroll-mode))
  :general (:states '(normal)
            :keymaps '(pdf-continuous-scroll-mode-map)
            "j" 'pdf-continuous-scroll-forward
            "k" 'pdf-continuous-scroll-backward
            "C-f" 'pdf-continuous-next-page
            "C-b" 'pdf-continuous-previous-page)
  :quelpa (pdf-continuous-scroll-mode
           :fetcher github
           :repo "dalanicolai/pdf-continuous-scroll-mode.el"))

screenshot

This doesn’t work on macOS because there’s no x-export-frames.

(use-package screenshot
  :if (display-graphic-p)
  :ensure nil
  :quelpa (screenshot
           :fetcher github
           :repo "tecosaur/screenshot"))

Writing prose

Grammarly

This currently makes buffers slow and doesn’t work.

(use-package grammarly
  :disabled)

(use-package flycheck-grammarly
  :disabled)

(use-package emacs-grammarly
  :disabled
  :ensure nil
  :quelpa (emacs-grammarly
           :fetcher github
           :repo "mmagnus/emacs-grammarly"))

flyspell

(use-package flyspell
  :defer 1
  :custom
  ;; TODO: Do I want this?
  ;; (flyspell-abbrev-p t)
  (flyspell-issue-message-flag nil)
  (flyspell-issue-welcome-flag nil)
  (flyspell-mode 1))

flyspell-correct-ivy

(use-package flyspell-correct-ivy
  :after flyspell
  :bind (:map flyspell-mode-map
         ;; TODO: This mapping is too good... should I use it for something
         ;; else?
         ("C-;" . flyspell-correct-word-generic))
  :custom
  (flyspell-correct-interface 'flyspell-correct-ivy))

mw-thesaurus

(use-package request)

(defun mpereira/thesaurus-lookup-at-point ()
  (interactive)
  (let* ((response (mw-thesaurus-lookup-at-point))
         (process (get-buffer-process (request-response--buffer response)))
         (current-sentinel (process-sentinel process)))
    ;; FIXME: how to compose process sentinels?
    (set-process-sentinel process
                          (lambda (&rest args)
                            (funcall 'current-sentinel args)
                            (when olivetti-mode
                              (message "dere")
                              (with-current-buffer mw-thesaurus-buffer-name
                                (message "guise")
                                (olivetti-mode)))))
    ))

(use-package mw-thesaurus
  :general
  (:keymaps '(text-mode-map org-mode-map)
   :states '(normal visual)
   "K" #'mw-thesaurus-lookup-at-point)
  (:keymaps '(mw-thesaurus-mode-map)
   :states '(normal visual)
   "q" #'mw-thesaurus--quit)
  :config
  (mw-thesaurus-mode))

atomic-chrome

(use-package atomic-chrome
  :config
  (setq atomic-chrome-url-major-mode-alist '(("github\\.com" . gfm-mode)))

  (atomic-chrome-start-server)

  (add-hook 'atomic-chrome-edit-mode-hook #'mpereira/prevent-buffer-kill))

Distraction-free editing

hide-mode-line

(use-package hide-mode-line)

olivetti

(use-package olivetti
  :config
  (setq-default olivetti-body-width 100))

writeroom-mode

(use-package writeroom-mode)

Buffer management

transpose-frame

(use-package transpose-frame)

buffer-expose

(use-package buffer-expose
  :config
  (general-define-key
   :keymaps '(buffer-expose-grid-map)
   "h" 'buffer-expose-left-window
   "l" 'buffer-expose-right-window
   "k" 'buffer-expose-up-window
   "j" 'buffer-expose-down-window
   "0" 'buffer-expose-first-window-in-row
   "$" 'buffer-expose-last-window-in-row
   "g" 'buffer-expose-first-window
   "G" 'buffer-expose-last-window
   "SPC" 'buffer-expose-ace-window
   "]" 'buffer-expose-next-page
   "[" 'buffer-expose-prev-page
   "d" 'buffer-expose-kill-buffer))

buffer-move

(use-package buffer-move)

rotate

(use-package rotate)

persistent-scratch

(use-package persistent-scratch
  :config
  ;; Enables auto save and automatic backup restore.
  (persistent-scratch-setup-default)

  (setq persistent-scratch-autosave-interval 60)

  (defun mpereira/persistent-scratch-scratch-buffer-p ()
    "Return non-nil iff the current buffer's name starts with *scratch*."
    (string-prefix-p "*scratch*" (buffer-name)))

  (setq persistent-scratch-scratch-buffer-p-function
        'mpereira/persistent-scratch-scratch-buffer-p))

Prevent scratch buffers from being killed

Also start them in Org mode.

(setq initial-major-mode (lambda ()
                           (persistent-scratch-mode)
                           (org-mode)))

display-buffer-alist configuration

;; TODO: configure `display-buffer-fallback-action'.
(use-package emacs
  :init
  ;; Inspiration, resources:
  ;; - http://juanjose.garciaripoll.com/blog/arranging-emacs-windows
  ;; - https://github.com/hlissner/doom-emacs/blob/master/modules/ui/popup/config.el
  ;; - https://protesilaos.com/dotemacs/#h:3d8ebbb1-f749-412e-9c72-5d65f48d5957
  ;; - https://web.archive.org/web/20160409014815/https://www.lunaryorn.com/2015/04/29/the-power-of-display-buffer-alist.html
  (setq display-buffer-alist
        '(;; ("\\*emacs-audit: package-list\\*"
          ;;  (display-buffer-in-side-window)
          ;;  (window-height . 1.0)
          ;;  (window-width . 1.0)
          ;;  (slot . 0)
          ;;  (mode-line-format . (" " "%b")))

          ("\\*Org Agenda\\*"
           (display-buffer-same-window))

          ("\\*docker-\\(containers\\|images\\|networks\\|volumes\\)\\*"
           (display-buffer-below-selected))

          ("\\*kubel (.+\\*"
           (display-buffer-same-window))

          ("\\*kubel -.+\\*"
           (display-buffer-below-selected))

          ("\\* docker container \\(logs\\|inspect\\) .+ \\*"
           (display-buffer-same-window))

          ("\\* Merriam-Webster Thesaurus \\*"
           (display-buffer-below-selected))

          ("\\*cider-\\(doc\\|error\\|inspect\\|test-report\\)\\*"
           (display-buffer-below-selected))

          ;; File-specific magit-log buffer.
          ;; magit-log(branch-name -- path/to/file.ext): project-name
          ("magit-log(.+): .+"
           (display-buffer-below-selected))

          ;; Clicking on a revision in a file-specific magit-log buffer.
          ;; magit-revision: project-name
          ("magit-revision: .+"
           (display-buffer-same-window))

          ("\\*compilation\\*"
           (display-buffer-below-selected))

          ("\\*mpereira/pp-macroexpand-all\\*"
           (display-buffer-in-side-window)
           (window-height . 0.5)
           (window-width . 0.5)
           (slot . 0)
           (mode-line-format . (" " "%b")))

          ("\\*hackernews top stories\\*"
           (display-buffer-in-side-window)
           (window-width . 120)
           (side . left)
           (slot . 0)
           (mode-line-format . (" " "%b")))

          ("^\\(\\*help.*\\)$"
           (display-buffer-below-selected))

          ("^\\(\\*lsp-help\\*\\)$"
           (display-buffer-below-selected))

          ("\\(\\Wo\\)?*Man.*"
           (display-buffer-below-selected))

          ("\\*\\(compilation\\|run .*\\|cargo test.*\\|cargo check.*\\|Async Shell Command\\)\\*"
           (display-buffer-in-side-window)
           (window-width . 100)
           (side . right)
           (slot . 0)
           (mode-line-format . (" " "%b")))

          ("\\*\\(rg\\)\\*"
           (display-buffer-in-side-window)
           (window-height . 0.5)
           (window-width . 0.5)
           (slot . 0)
           (mode-line-format . (" " "%b")))

          ("\\*Flycheck.*\\*"
           (display-buffer-below-selected))

          ("\\*last-eshell-output\\*"
           (display-buffer-same-window))

          ("\\*Diff\\*"
           (display-buffer-same-window))

          ("\\(\\*undo-tree\\*\\)"
           (display-buffer-in-side-window)
           (window-width . 80)
           (window-height . 0.5)
           (side . left)
           (slot . 0)
           (mode-line-format . (" " "%b")))

          ("\\(\\*undo-tree Diff\\*\\)"
           (display-buffer-in-side-window)
           (window-width . 80)
           (window-height . 0.5)
           (side . left)
           (slot . 1)
           (mode-line-format . (" " "%b"))))))

Display compilation result buffers to a single window to the right

(defvar-local mpereira/display-buffer-compilation-result-window nil
  "TODO: docstring.")

;; FIXME: when
;; 1. opening a compilation buffer
;; 2. pressing RET on an error
;; 3. navigating to a different project/perpective
;; 4. quitting the compilation buffer window
;; the error buffer window doesn't get deleted.
(defun mpereira/display-buffer-compilation-result (buffer alist)
  "TODO: docstring."
  (interactive)
  (if (with-current-buffer compilation-last-buffer
        mpereira/display-buffer-compilation-result-window)
      (window--display-buffer buffer
                              (with-current-buffer compilation-last-buffer
                                mpereira/display-buffer-compilation-result-window)
                              'reuse
                              alist)
    (let ((window (display-buffer-in-atom-window buffer
                                                 alist)))
      (with-current-buffer compilation-last-buffer
        (setq mpereira/display-buffer-compilation-result-window window))
      window)))

(defun mpereira/compile-goto-error-window-to-the-right ()
  "TODO: docstring."
  (interactive)
  (let ((display-buffer-overriding-action
         `((mpereira/display-buffer-compilation-result)
           (window-width . 0.5)
           (side . right))))
    (call-interactively #'compile-goto-error)
    (recenter nil)))

(general-define-key
 :keymaps '(compilation-button-map)
 "RET" #'mpereira/compile-goto-error-window-to-the-right)

Project management

A fast non-Projectile-based project file finder

Existing functions in projectile, counsel, ffip, etc. were too slow for one reason or another.

;;; fast-project-find-file.el --- TODO: description  -*- coding: utf-8 -*-

;; Copyright (C) 2020 Murilo Pereira

;; Author: Murilo Pereira <murilo@murilopereira.com>
;; Maintainer: Murilo Pereira <murilo@murilopereira.com>
;; URL: https://github.com/mpereira/fast-project-find-file
;; Package-Requires: ((emacs "25.2") (ivy) (dash))
;; Keywords: maint
;; SPDX-License-Identifier: GPL-3+

;; This file is not part of GNU Emacs.

;;; Commentary:

;; TODO.

;; TODO: Communicate when the rg process finishes somehow. Show something in the
;; ivy prompt?

;; TODO: Make it work with file-local lexical-binding.

;; TODO: Make it work without ivy.

;; TODO: Make it work without dash.

;; TODO: Make it work with `find`.

;; TODO: Make ivy filtering styles work.

;; TODO: Add unit tests.

;; TODO: Make it possible to clear caches.

;; TODO: Make it work with other VC systems than git.

;;; Code:

(defvar ivy--all-candidates)
(defvar ivy-regex)

(declare-function -filter "ext:dash.el" (pred list))
(declare-function ivy--format "ext:ivy.el" (cands))
(declare-function ivy--insert-minibuffer "ext:ivy.el" (text))
(declare-function ivy--set-candidates "ext:ivy.el" (x))
(declare-function ivy-re-to-str "ext:ivy.el" (re))

(defvar fast-project-find-file-debug-mode nil
  "TODO: docstring.")

(defvar fast-project-find-file-executable-find-cache (make-hash-table :test 'equal)
  "TODO: docstring.")

(defvar fast-project-find-file-project-root-cache (make-hash-table :test 'equal)
  "TODO: docstring.")

(defun fast-project-find-file-get-cached-or-compute (cache key-fn compute-fn)
  "TODO: CACHE KEY-FN COMPUTE-FN docstring."
  (let* ((key (funcall key-fn))
         (cached-value (gethash key cache)))
    (if cached-value
        (progn
          (when fast-project-find-file-debug-mode
            (message "cache hit: %s -> %s" key cached-value))
          cached-value)
      (let ((value (funcall compute-fn)))
        (when fast-project-find-file-debug-mode
          (message "cache miss: %s" key))
        (puthash key value cache)))))

(defun fast-project-find-file-executable-find (executable)
  "TODO: EXECUTABLE docstring."
  (interactive)
  (fast-project-find-file-get-cached-or-compute
   fast-project-find-file-executable-find-cache
   (lambda () (let ((host (file-remote-p default-directory 'host)))
                (list host executable)))
   (lambda () (executable-find executable t))))

(defun fast-project-find-file-project-root (&optional file-name)
  "TODO: FILE-NAME docstring."
  (interactive)
  (let ((file-name* (or (and (boundp 'file-name) file-name)
                        default-directory)))
    (fast-project-find-file-get-cached-or-compute
     fast-project-find-file-project-root-cache
     (lambda () file-name*)
     (lambda () (locate-dominating-file file-name* ".git")))))

(defun fast-project-find-file (&optional initial-input)
  "TODO: INITIAL-INPUT docstring."
  (interactive)
  (if-let ((fd-executable (fast-project-find-file-executable-find "fd")))
      (let* ((default-directory (fast-project-find-file-project-root))
             (make-process-fn (if (file-remote-p default-directory)
                                  'tramp-handle-make-process
                                'make-process))
             (filter-fn (lambda (candidate)
                          (string-match-p (ivy-re-to-str ivy-regex) candidate)))
             (process-name "fast-project-find-file")
             (candidates))
        (funcall
         make-process-fn
         :name process-name
         :filter (lambda (_process-buffer output-batch)
                   (when (boundp 'candidates)
                     (let ((candidate-batch (split-string output-batch "\n" t)))
                       (setq candidates (append candidates candidate-batch))
                       (ivy--set-candidates (-filter filter-fn candidates))
                       (ivy--insert-minibuffer (ivy--format ivy--all-candidates)))))
         :command (list "fd" "." "--color=never")
         :sentinel #'ignore
         :connection-type 'pipe)
        (ivy-read "File: "
                  (lambda (_input)
                    (if candidates
                        (-filter filter-fn candidates)
                      '("Searching...")))
                  :initial-input initial-input
                  :dynamic-collection t
                  :unwind (lambda ()
                            (when-let ((process (get-process process-name)))
                              (delete-process process)))
                  :action (lambda (candidate)
                            (find-file candidate))
                  :caller 'fast-project-find-file-project-find-file))
    (message "Coudn't find `fd` in PATH")))

(provide 'fast-project-find-file)

;;; fast-project-find-file.el ends here
(let ((package-name "fast-project-find-file"))
  (byte-compile-file (expand-file-name (concat package-name ".el") user-emacs-directory))
  (load-file (expand-file-name (concat package-name ".elc") user-emacs-directory)))

projectile

(use-package projectile
  :config
  (projectile-mode t)

  (setq projectile-enable-caching t)
  (setq projectile-require-project-root t)
  (setq projectile-dynamic-mode-line nil)

  ;; Prevent projectile from automatically creating projects when visiting
  ;; files. For example, navigating to the definition of a function from a
  ;; dependency will add the dependency directory as a project.
  (setq projectile-track-known-projects-automatically nil)

  ;; With this, do I even need counsel-projectile?
  (setq projectile-completion-system 'ivy))

term-projectile

(use-package term-projectile
  :after projectile)

ibuffer

(use-package ibuffer
  :general (:keymaps '(ibuffer-mode-map)
            :states '(normal)
            "o" 'ibuffer-toggle-sorting-mode
            "," nil)
  :config
  (setq ibuffer-formats '((mark modified read-only locked " "
                                (name 40 40 :left :elide)
                                " "
                                (size 9 -1 :right)
                                " "
                                (mode 16 16 :left :elide)
                                " " filename-and-process)
                          (mark " "
                                (name 16 -1)
                                " " filename))))

ibuffer-projectile

(use-package ibuffer-projectile
  :after projectile)

perspective

(use-package perspective
  :ensure nil
  :quelpa (perspective
           :fetcher github
           :repo "nex3/perspective-el")
  :init
  (setq persp-show-modestring nil)
  (setq persp-suppress-no-prefix-key-warning t)
  :config
  (persp-mode t))

counsel

(use-package counsel
  :after (ivy swiper)
  :custom
  (counsel-yank-pop-preselect-last t)
  (counsel-yank-pop-separator "\n----\n")
  :config
  (counsel-mode 1)

  (setq counsel-find-file-ignore-regexp "/vendor/")

  ;; Counsel sets the ivy initial input as a caret for some of its functions. I
  ;; don't want that.
  (setq ivy-initial-inputs-alist nil)

  (setq counsel-describe-function-function #'helpful-callable)
  (setq counsel-describe-variable-function #'helpful-variable))

persp-projectile

(use-package persp-projectile
  :after perspective projectile)

counsel-projectile

(use-package counsel-projectile
  :after (counsel projectile)
  :config
  (setq projectile-project-name-function 'mpereira/projectile-default-project-name)
  (setq projectile-switch-project-action 'counsel-projectile-switch-project-action-dired))

find-file-in-project

(use-package find-file-in-project
  :config
  ;; ffip adds `ffap-guess-file-name-at-point' automatically and it is crazy
  ;; slow on TRAMP buffers.
  (remove-hook 'file-name-at-point-functions 'ffap-guess-file-name-at-point))

Commands

amx

Only installing this package for its functions. I get the actual “candidate recency” functionality for M-x from ivy-prescient.

counsel also depends on this.

(use-package amx)

ivy

(use-package ivy
  :general
  (:keymaps 'ivy-switch-buffer-map
   "C-k" 'ivy-previous-line) ;; this is bound to `ivy-switch-buffer-kill' by
                             ;; something.
  (:keymaps 'ivy-minibuffer-map
   "C-j" 'ivy-next-line
   "C-k" 'ivy-previous-line
   "C-f" 'ivy-scroll-up-command
   "C-b" 'ivy-scroll-down-command
   "C-o" 'ivy-occur
   "C-h" 'ivy-beginning-of-buffer
   "C-l" 'ivy-end-of-buffer
   "C-/" 'ivy-restrict-to-matches
   "<escape>" 'minibuffer-keyboard-quit)

  :custom
  (ivy-use-selectable-prompt t)
  (ivy-height 20)
  (ivy-wrap t)
  (ivy-height-alist '((counsel-evil-registers . 10)
                      (counsel-yank-pop . 10)
                      (counsel-git-log . 8)))

  :config
  (ivy-mode t))

prescient

(use-package prescient
  :config
  (prescient-persist-mode))

ivy-prescient

This seems to affect performance when there are lots of candidates, mostly via ivy-prescient-sort-function.

Disabled for now because it seems to be broken and unmaintained (as of [2020-09-06 Sun]).

(use-package ivy-prescient
  :disabled
  :after (ivy counsel amx prescient)
  :config
  (ivy-prescient-mode))

company-prescient

(use-package company-prescient
  :after (company prescient)
  :config
  (company-prescient-mode))

Help

helpful

(use-package helpful)

discover-my-major

(use-package discover-my-major)

which-key

(use-package which-key
  :config
  (which-key-mode))

dash-at-point

(use-package dash-at-point)

command-log-mode

(use-package command-log-mode
  :config
  (setq command-log-mode-auto-show t)
  (setq command-log-mode-window-size 60))

Markup

markdown-mode

markdown-mode also provides gfm-mode.

(use-package markdown-mode
  :config
  (setq markdown-fontify-code-blocks-natively t)
  (setq markdown-command "pandoc --from markdown --to html")

  (general-define-key
   :keymaps 'markdown-mode-map
   :states '(normal visual)
   :prefix mpereira/leader
   "oi" 'markdown-insert-link)

  (general-define-key
   :keymaps 'markdown-mode-map
   :states '(normal visual)
   "TAB" 'markdown-cycle
   "(" 'markdown-up-heading
   "k" 'evil-previous-visual-line
   "j" 'evil-next-visual-line
   "C-k" 'markdown-outline-previous-same-level
   "C-j" 'markdown-outline-next-same-level))

toml-mode

(use-package toml-mode)

yaml-mode

(use-package yaml-mode
  :config
  (add-to-list 'auto-mode-alist '("\\.yml(?:\\.j2)?\\'" . yaml-mode))

  (general-define-key
   :keymaps '(yaml-mode-map)
   :states '(insert)
   "RET" 'newline-and-indent))

htmlize

(use-package htmlize)

grip-mode

(use-package grip-mode)

Interactions with websites

counsel-web

(use-package counsel-web
  :after counsel
  :ensure nil
  :quelpa (counsel-web
           :fetcher github
           :repo "mnewt/counsel-web"))

emacs-webkit

Doesn’t work with macOS yet.

(use-package webkit
  :ensure nil
  :quelpa (webkit
           :fetcher github
           :repo "akirakyle/emacs-webkit"))

xwwp-follow-link

(use-package xwwp
  :custom
  (xwwp-follow-link-completion-system 'ivy)
  :general (:states '(normal)
            :keymaps '(xwidget-webkit-mode-map)
            "f" 'xwwp-follow-link))

(use-package xwwp-follow-link-ivy)

elfeed

(use-package elfeed
  :custom
  (elfeed-feeds '("https://adamwiggins.com/feed.rss"
                  "https://aphyr.com/posts.atom"
                  "https://feeds.transistor.fm/on-the-metal-0294649e-ec23-4eab-975a-9eb13fd94e06"
                  "https://stopa.io/feed.rss")))

stackoverflow

REVIEW: do I use or need this?

(require 'json)

(defun mpereira/get-stackoverflow-answers (query)
  "TODO: docstring QUERY."
  (interactive "sQuestion: ")
  (let* ((question_ids
          (with-current-buffer
              (url-retrieve-synchronously
               (concat "https://google.com/search?ie=utf-8&oe=utf-8&hl=en&as_qdr=all&q="
                       (url-hexify-string (concat query " site:stackoverflow.com"))))
            (let (ids)
              (while (re-search-forward "https://stackoverflow.com/questions/\\([0-9]+\\)" nil t)
                (push (match-string-no-properties 1) ids))
              (setq ids (reverse ids))
              (if (> (length ids) 5)
                  (subseq ids 0 5)
                ids))))

         (url_template (format "https://api.stackexchange.com/2.2/questions/%s%%s?site=stackoverflow.com"
                               (string-join question_ids ";")))

         (questions (with-current-buffer
                        (url-retrieve-synchronously
                         (format url_template ""))
                      (goto-char (point-min))
                      (search-forward "\n\n")
                      (append (assoc-default 'items (json-read)) nil)))

         (answers (with-current-buffer
                      (url-retrieve-synchronously
                       (concat (format url_template "/answers")
                               "&order=desc&sort=activity&filter=withbody"))
                    (goto-char (point-min))
                    (search-forward "\n\n")
                    (sort (append (assoc-default 'items (json-read)) nil)
                          (lambda (x y)
                            (> (assoc-default 'score x)
                               (assoc-default 'score y)))))))

    (switch-to-buffer "*stackexchange*")
    (erase-buffer)

    (dolist (question_id (mapcar 'string-to-number question_ids))
      (let ((question (some (lambda (question)
                              (if (equal (assoc-default 'question_id question)
                                         question_id)
                                  question))
                            questions)))
        (insert "<hr><h2 style='background-color:paleturquoise'>Question: "
                (format "<a href='%s'>%s</a>"
                        (assoc-default 'link question)
                        (assoc-default 'title question))
                "</h2>"
                "\n"
                (mapconcat
                 'identity
                 (let ((rendered
                        (remove-if
                         'null
                         (mapcar (lambda (answer)
                                   (if (and (equal question_id
                                                   (assoc-default 'question_id answer))
                                            (>= (assoc-default 'score answer) 0))
                                       (concat "<hr><h2 style='background-color:"
                                               "#c1ffc1'>Answer - score: "
                                               (number-to-string (assoc-default 'score answer))
                                               "</h2>"
                                               (assoc-default 'body answer))))
                                 answers))))
                   (if (> (length rendered) 5)
                       (append (subseq rendered 0 5)
                               (list (format "<br><br><a href='%s'>%s</a>"
                                             (assoc-default 'link question)
                                             "More answers...")))
                     rendered))
                 "\n")
                )))
    (shr-render-region (point-min) (point-max))
    (goto-char (point-min))
    (save-excursion
      (while (search-forward "^M" nil t)
        (replace-match "")))))

google-this

(use-package google-this
  :config
  (google-this-mode 1))

Wolfram Alpha

(use-package wolfram
  :config
  (load-file (expand-file-name "wolfram-secrets.el" user-emacs-directory))
  (setq wolfram-alpha-app-id mpereira/secret-wolfram-alpha-app-id))

hackernews

(use-package hackernews)

Miscellaneous

suggest

(use-package suggest)

open-junk-file

(use-package open-junk-file
  :config
  (setq open-junk-file-directory (concat user-emacs-directory
                                         "junk/%Y/%m/%d/%H%M%S.")))

gif-screencast

(use-package gif-screencast
  :config
  (setq gif-screencast-args '("-x"))
  (setq gif-screencast-cropping-program "mogrify")
  (setq gif-screencast-capture-format "ppm"))

disk-usage

(use-package disk-usage)

circe

(use-package circe
  :general
  (:keymaps '(circe-mode-map)
   "K" 'helpful-at-point)
  :config
  (enable-circe-color-nicks)

  (load-file (expand-file-name "circe-secrets.el" user-emacs-directory))

  ;; Disable name listing when joining channels.
  (circe-set-display-handler "353" 'circe-display-ignore)
  (circe-set-display-handler "366" 'circe-display-ignore)

  ;; Logging.
  (setq lui-logging-directory
        (expand-file-name "irc" mpereira/cloud-synced-directory))
  (load "lui-logging" nil t)
  (enable-lui-logging-globally)

  ;; Automatic reconnect.
  (setq circe-lagmon-timer-tick 60)
  (load "circe-lagmon" nil t)
  (circe-lagmon-mode)

  ;; Timestamps in margins.
  (setq lui-time-stamp-position 'right-margin)
  (setq lui-time-stamp-format " %H:%M ")
  (defun mpereira/circe-set-margin ()
    (setq right-margin-width 7))
  (add-hook 'lui-mode-hook #'mpereira/circe-set-margin)

  (setq circe-default-nick "mpereira"
        circe-default-user "mpereira"
        circe-default-realname "mpereira")
  (setq circe-default-part-message "Bye.")
  (setq circe-default-quit-message "Bye.")
  (setq circe-reduce-lurker-spam t)
  (setq circe-network-options
        `(("Freenode"
           :host "chat.freenode.net"
           :nickserv-password ,mpereira/secret-circe-nickserv-password
           :tls t
           :channels (:after-auth
                      "#emacs"
                      "#clojure"
                      "##rust"
                      "#haskell")))))

mingus

(use-package mingus
  :config
  (evil-set-initial-state 'mingus-help-mode 'emacs)
  (evil-set-initial-state 'mingus-playlist-mode 'emacs)
  (evil-set-initial-state 'mingus-browse-mode 'emacs)

  (dolist (hook '(mingus-browse-hook
                  mingus-playlist-hooks))
    (add-hook hook 'mpereira/hide-trailing-whitespace)))

osascripts

(use-package osascripts
  :ensure nil
  :quelpa (osascripts
           :fetcher github
           :repo "leoliu/osascripts"))

emacs-audit

Not tangled because it breaks on make test and running on different hosts.

(condition-case err
    (use-package emacs-audit
      :ensure nil
      :quelpa (emacs-audit
               :fetcher file
               :path "~/git/emacs-audit/elisp/emacs-audit.el"))
  (error (message "Error: %S" err)))

(defun mpereira/org-ascii--box-string (s info)
  "Return string S with a partial box to its left.
INFO is a plist used as a communication channel."
  s)

;; FIXME: causes "Lisp nesting exceeds ‘max-lisp-eval-depth’" for some reason.
(defun mpereira/org-ascii-export-as-ascii (&rest args)
  "TODO: docstring."
  (interactive)
  (cl-letf (((symbol-function #'org-ascii--box-string) #'mpereira/org-ascii--box-string))
    (apply 'org-ascii-export-as-ascii args)))

(comment
 (emacs-audit--with-command
  command
  command)
 (quelpa-upgrade
  '(emacs-audit
    :fetcher file
    :path "/Users/murilo/git/emacs-audit/elisp/emacs-audit.el")))

Mappings

;; Resources:
;; - https://github.com/emacs-evil/evil/issues/897
;; - `evil-end-of-line-or-visual-line'
;; - `evil-end-of-line'
;; - `end-of-visual-line'
(defun mpereira/evil-make-$-not-include-newline (keymap)
  "Make `$' not include the newline character."
  (general-define-key
   :keymaps `(,keymap)
   :states '(motion)
   "$" '(lambda ()
          (interactive)
          (evil-end-of-line))))

(use-package emacs
  :after (evil-org)
  :config
  (add-hook 'evil-org-mode-hook
            (lambda ()
              (mpereira/evil-make-$-not-include-newline 'evil-org-mode-map))))

(use-package emacs
  :after (evil)
  :config
  (mpereira/evil-make-$-not-include-newline 'global)

  (general-define-key
   :keymaps '(evil-ex-search-keymap minibuffer-local-map)
   "C-k" #'previous-line-or-history-element
   "C-j" #'next-line-or-history-element)

  (evil-ex-define-cmd "bdelete" #'kill-this-buffer))

;; TUI Emacs.
(use-package emacs
  :if (not (display-graphic-p))
  :config
  (define-key input-decode-map "\e[1;5A" [C-up])
  (define-key input-decode-map "\e[1;5B" [C-down])
  (define-key input-decode-map "\e[1;5C" [C-right])
  (define-key input-decode-map "\e[1;5D" [C-left])

  ;; NOTE: for some reason TAB is mapped to `evil-jump-forward' in terminal
  ;; mode?
  (general-define-key
   :keymaps '(org-mode-map)
   :states '(normal)
   "TAB" 'org-cycle)

  (general-define-key
   :keymaps '(eshell-mode-map)
   :states '(insert)
   "C-_" 'mpereira/eshell-history))

(use-package emacs
  :after (evil-collection)
  :config
  (general-define-key
   :keymaps '(image-mode-map)
   :states '(normal)
   ;; Originally `image-previous-frame'.
   "," nil))

(use-package emacs
  :config
  (general-define-key
   :keymaps '(magit-mode-map)
   :states '(normal visual)
   "j" 'evil-next-visual-line           ; originally `evil-next-line'.
   "k" 'evil-previous-visual-line       ; originally `evil-previous-line'.
   "(" 'magit-section-up
   "TAB" 'magit-section-cycle
   "C-j" 'magit-section-forward-sibling
   "C-k" 'magit-section-backward-sibling)

  (general-define-key
   :keymaps '(git-rebase-mode-map)
   :states '(normal)
   "C-S-j" 'git-rebase-move-line-down
   "C-S-k" 'git-rebase-move-line-up))
(general-define-key
 :keymaps '(override)                   ; check out `general-override-mode-map'.
 ;; Adding `nil' to the states makes these keybindings work on buffers where
 ;; they would usually not work, e.g. the *Messages* buffer or the
 ;; `undo-tree-visualize' buffer.
 :states '(normal visual insert nil)
 "M-+" #'mpereira/font-size-increase
 "M--" #'mpereira/font-size-decrease
 "M-=" #'default-text-scale-reset
 "M-F" #'toggle-frame-fullscreen
 "M-H" 'buf-move-left
 "M-J" 'buf-move-down
 "M-K" 'buf-move-up
 "M-L" 'buf-move-right
 "M-M" #'mpereira/toggle-buffer-maximize
 "M-N" (lambda () (interactive) (mpereira/toggle-buffer-maximize t))
 "M-O" #'transpose-frame
 "M-h" #'evil-window-left
 "M-j" #'evil-window-down
 "M-k" #'evil-window-up
 "M-l" #'evil-window-right
 "C-w =" #'balance-windows              ; this is the default keybinding.
 "C-0" #'mpereira/git-messenger-show
 "<C-left>" #'winner-undo
 "<C-right>" #'winner-redo)

(general-define-key
 "<escape>" #'keyboard-quit)

(general-define-key
 :keymaps '(minibuffer-local-map
            minibuffer-local-ns-map
            minibuffer-local-completion-map
            minibuffer-local-must-match-map
            minibuffer-local-isearch-map)
 "<escape>" #'minibuffer-keyboard-quit
 "<C-escape>" #'(lambda ()
                  (interactive)
                  (let ((evil-want-minibuffer t))
                    (evil-initialize))))

(general-define-key
 :keymaps '(swiper-map
            swiper-all-map
            ivy-minibuffer-map)
 "<escape>" 'minibuffer-keyboard-quit   ; REVIEW: is this still needed?
 "C-r" 'evil-paste-from-register)

;; REVIEW: why doesn't this binding work in some modes like
;; `org-src-mode-map', even when specifically adding it in the keymaps?
(general-define-key
 :keymaps '(global-map)
 :states '(insert)
 "M-x" #'execute-extended-command)

;; FIXME: this doesn't work.
;; REVIEW: do I still need this?
;; Movement bindings for evil-ex-search. Why doesn't `evil-ex-search-keymap'
;; work for this?
(general-define-key
 :keymaps '(minibuffer-inactive-mode-map)
 "C-k" #'previous-line-or-history-element
 "C-j" #'next-line-or-history-element
 "C-?" #'previous-matching-history-element
 "C-/" #'next-matching-history-element)

;; REVIEW: is this still needed?
;; Make it possible for other modes to use these bindings (e.g. company mode
;; uses it for navigating completions).
(general-define-key
 :keymaps '(evil-insert-state-map)
 "C-j" nil
 "C-k" nil)

;; TODO: make this not override org mode?
;; (general-define-key
;;  :keymaps '(global-map)
;;  :states '(normal visual)
;;  :prefix "C-c"
;;  "C-o" 'browse-url)

(eval-after-load 'evil-ex
  '(evil-ex-define-cmd "bD" #'mpereira/delete-file-and-buffer))

(eval-after-load 'evil-ex
  '(evil-ex-define-cmd "pwd" #'mpereira/pwd))

;; Non-leader "g" ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix "g"
 "q" #'fill-paragraph)

(general-define-key
 :keymaps '(global-map)
 :states '(motion)
 :prefix "g"
 "-" #'evil-operator-string-inflection
 "c" #'evilnc-comment-operator)

;; Non-leader "master" bindings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 "M-X" 'amx-major-mode-commands
 "M-q" 'unfill-toggle
 "C-]" 'dumb-jump-go
 "C-}" 'dumb-jump-quick-look
 "C-l" #'evil-ex-nohighlight)

;; NOTE: replacing the stock `undo' with `undo-tree-undo' due to the following
;; error:
;;
;;   "primitive-undo: Unrecognized entry in undo list undo-tree-canary"
;;
;; More details in https://www.dr-qubit.org/Lost_undo-tree_history.html.
(general-define-key
 :keymaps '(global-map)
 :states '(normal)
 "u" 'undo-tree-undo)

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 "[c" #'diff-hl-previous-hunk
 "]c" #'diff-hl-next-hunk
 ;; NOTE(2023-01-03): not using "s" for this due to conflicts with other modes
 ;; (e.g., `magit-stage' in diff hunks).
 ;;
 ;; Using 'M-s' instead.
 "s" nil)

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 "M-s" #'avy-goto-char-timer)

;; "Master" bindings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; REVIEW: This seems to be working for now. `general-override-mode-map' might
;; be of use in the future.
(general-define-key
 :states '(normal visual)
 :prefix mpereira/leader
 "," #'evil-switch-to-windows-last-buffer
 "." #'ivy-resume
 "/" #'swiper
 "|" #'olivetti-mode
 "<backtab>" #'discover-my-major
 "<tab>" #'counsel-descbinds
 "=" #'quick-calc
 "B" #'ibuffer
 "b" #'switch-to-buffer
 "C" #'link-hint-copy-link
 "D" #'mpereira/bm-counsel-find-bookmark       ; REVIEW: rethink.
 "F" #'link-hint-open-link
 "hs" #'mpereira/split-window-below-and-switch
 "hv" #'mpereira/toggle-window-split           ; REVIEW: rethink.
 "n" #'mpereira/narrow-or-widen-dwim
 "q" #'evil-quit
 "T" #'bm-toggle                               ; REVIEW: rethink.
 "u" #'undo-tree-visualize
 "vh" #'mpereira/toggle-window-split           ; REVIEW: rethink.
 "vs" #'mpereira/split-window-right-and-switch ; REVIEW: rethink.
 "w" #'save-buffer)

;; d -> describe ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Mnemonic transgressions:
;; - ",dd" for `dired-jump'
;; - ",do" for `docker'

;; REVIEW: "d" is too good of a key for these bindinds that I never use.
(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "d"
 "b" #'describe-buffer
 "d" #'dired-jump
 "f" #'find-function-on-key
 "k" #'describe-key
 "m" #'describe-mode
 "o" #'docker)

;; e -> evaluate ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "e"
 ":" #'eval-expression
 "v" #'counsel-set-variable
 "w" 'wolfram-alpha)

;; f -> find ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "f"
 ";" #'counsel-minibuffer-history
 ":" #'counsel-command-history
 "b" #'ivy-switch-buffer
 "c" #'counsel-org-clock-history
 ;; Didn't like `counsel-explorer' from ivy-explorer too much...
 ;; "f" #'counsel-explorer
 "f" #'counsel-find-file
 "g" #'google-this
 "G" #'counsel-web-suggest
 "k" #'counsel-descbinds
 "l" #'counsel-find-library
 "m" #'describe-keymap
 "n" #'counsel-describe-function
 "o" #'counsel-imenu
 "p" #'package-install
 "v" #'counsel-describe-variable
 "y" #'counsel-yank-pop)

;; g -> git ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal)
 :prefix mpereira/leader
 :infix "g"
 "/" #'counsel-git-log
 "<" #'smerge-keep-mine
 ">" #'smerge-keep-other
 "[" #'git-timemachine-show-previous-revision
 "]" #'git-timemachine-show-next-revision
 "b" #'magit-blame
 "c" #'magit-commit-popup
 "d" #'(lambda ()
         (interactive)
         (let ((display-buffer-alist (append display-buffer-alist
                                             '(("magit-diff.*"
                                                (display-buffer-same-window))))))
           (magit-diff-buffer-file)))
 "D" #'magit-diff-unstaged
 "f" #'magit-find-file
 "g" #'magit-dispatch
 "ip" #'gist-region-or-buffer-private
 "ii" #'gist-region-or-buffer
 "il" #'gist-list
 "L" #'magit-log-all
 "l" #'magit-log-buffer-file
 "o" #'mpereira/browse-at-remote
 "p" #'magit-push
 "r" #'diff-hl-revert-hunk
 "s" #'magit-status
 "t" #'git-timemachine-toggle
 "w" #'magit-stage-file
 "W" #'magit-stage-modified)

;; p -> project ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "p"
 "B" 'mpereira/maybe-projectile-ibuffer
 "b" 'mpereira/maybe-projectile-switch-buffer
 "d" 'mpereira/maybe-projectile-dired
 "D" 'mpereira/maybe-projectile-find-directory
 "f" 'mpereira/maybe-projectile-find-file
 "G" 'counsel-rg
 "g" 'rg
 "p" 'dired-sidebar-toggle-sidebar
 "s" 'mpereira/counsel-projectile-perspective-switch-project
 "t" 'persp-switch-last)

;; s -> shell ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :states '(normal)
 :prefix mpereira/leader
 :infix "s"
 ">" 'mpereira/eshell-complete-redirect-to-buffer
 "H" 'projectile-run-term
 "c" 'projectile-run-async-shell-command-in-root
 "d" 'mpereira/eshell-complete-recent-directory
 "h" 'mpereira/maybe-projectile-eshell
 "n" (lambda ()
       (interactive)
       (mpereira/call-interactively-with-prefix-arg
        '(4)
        'eshell-show-output)))

;; t -> toggle ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(global-map)
 :states '(normal visual)
 :prefix mpereira/leader
 :infix "t"
 "d" #'toggle-debug-on-error
 "e" #'toggle-debug-on-error
 "l" #'toggle-truncate-lines
 "n" #'mpereira/narrow-or-widen-dwim
 "q" #'toggle-debug-on-quit
 "r" #'toggle-read-only
 "t" #'mpereira/narrow-or-widen-dwim
 "w" #'delete-trailing-whitespace)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(general-define-key
 :keymaps '(origami-mode-map)
 :states '(normal visual)
 "TAB" #'origami-toggle-node
 "<S-tab>" #'origami-toggle-all-nodes)

;; Return to original cursor position when cancelling search.
(general-define-key
 :keymaps '(isearch-mode-map)
 "<escape>" #'isearch-cancel)

(general-define-key
 :keymaps '(evil-ex-search-keymap)
 "<escape>" #'minibuffer-keyboard-quit)

(general-define-key
 :keymaps '(help-mode-map)
 "<" #'help-go-back
 ">" #'help-go-forward)

(general-define-key
 :states '(normal visual)
 :keymaps '(helpful-mode-map deadgrep-mode-map)
 "q" #'mpereira/kill-buffer-and-maybe-window)

(general-define-key
 :keymaps '(tabulated-list-mode-map)
 :states '(normal visual)
 "<" 'tabulated-list-narrow-current-column
 ">" 'tabulated-list-widen-current-column
 "gr" 'tabulated-list-revert
 "S" 'tabulated-list-sort)

(general-define-key
 :keymaps '(tablist-mode-map)
 :states '(normal visual)
 "W" 'tablist-forward-column
 "B" 'tablist-backward-column
 "S" 'tablist-sort)

(with-eval-after-load "evil-collection"
  (add-hook 'evil-collection-setup
            (lambda ()
              (general-define-key
               :states '(normal visual)
               :keymaps '(dired-mode-map)
               "H" #'evil-window-top
               "L" #'evil-window-bottom))))

Fun

fireplace

(use-package fireplace)

let-it-snow

FIXME: Doesn’t work. Disabled for now.

(use-package snow
  :ensure nil
  :disabled
  :quelpa (snow
           :fetcher github
           :repo "alphapapa/snow.el"))

Tips and tricks

org mode file links to search patterns can’t start with open parens

https://www.mail-archive.com/emacs-orgmode@gnu.org/msg112359.html

EXPRESSION can be used only once per org-agenda-prefix-format

Emulate C-u (universal-argument)

For raw prefix arg (interactive “P”)

(let ((current-prefix-arg '(4)))
  (call-interactively 'some-func))

Otherwise

(let ((current-prefix-arg 4))
  (call-interactively 'some-func))

After modifying PATH

Run mpereira/exec-path-from-shell-initialize on eshell buffers.

Terminate init.el loading early

(with-current-buffer " *load*"
  (goto-char (point-max)))

Change font: M-x x-select-font

Overriding a function

https://endlessparentheses.com/understanding-letf-and-how-it-replaces-flet.html

(cl-letf (((symbol-function 'function-be-overridden) #'overriding-function))
  ;; expressions to be evaluated with the function override.
  )

License

MIT License

Copyright (c) 2023 Murilo Pereira

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File-local variables

These need to be at the end of the file.