Skip to content

Latest commit

 

History

History
1126 lines (995 loc) · 48.1 KB

ha-general.org

File metadata and controls

1126 lines (995 loc) · 48.1 KB

Leader Key Sequences

A literate programming file for defining leaders with general

Introduction

The one thing that both Spacemacs and Doom taught me, is how much I like the key sequences that begin with a leader key. In both of those systems, the key sequences begin in the normal state with a space key. This means, while typing in insert state, I have to escape to normal state and then hit the space.

I’m not trying an experiment where specially-placed function keys on my fancy ergodox keyboard can kick these off using General Leader project. Essentially, I want a set of leader keys for Evil’s normal state as well as a global leader in all modes.

(use-package general
  :config
  (setq general-use-package-emit-autoloads t)

  (general-evil-setup t)

  (general-create-definer ha-leader
    :states '(normal visual motion)
    :keymaps 'override
    :prefix "SPC"
    :non-normal-prefix "s-SPC"
    :global-prefix "<f13>")

  (general-create-definer ha-local-leader
    :states '(normal visual motion)
    :prefix "<f17>")

  (general-nmap "SPC m" (general-simulate-key "," :which-key "major mode")))

Relabel the G Keys

Can’t remember all the shortcuts on the g key, and which-key displays the entire function, so let’s re-add those keybindings, but with labels. The g is extemely convenient, yet I realize that I will never use some of the default keybindings (like g m to go to the middle of the line? Too imprecise). So I am also going to delete some of them.

(use-package evil
  :general
  (:states '(normal visual motion operator)
           ;; These go into operator mode, so the key sequence, g U i o
           ;; upper cases the symbol at point:
           "g u" '("downcase" . evil-downcase)
           "g U" '("upcase" . evil-upcase)
           "g ~" '("invert case" . evil-invert-case)

           ;; Use this ALL the time:
           "g ;" '("last change →" . evil-goto-last-change)
           "g :" '("last change ←" . evil-goto-last-change-reverse)
           "g d" '("goto def" . evil-goto-definition)
           "g i" '("resume insert" . evil-insert-resume)
           "g v" '("resume visual" . evil-visual-restore)

           "g g" '("goto first line" . evil-goto-first-line)
           "g f" '("find file" . find-file-at-point)

           "g e" '("← WORD end" . evil-backward-WORD-end) ; like b
           "g E" '("← word end" . evil-backward-word-end) ; like B
           "g w" '("→ WORD end" . evil-forward-WORD-end)
           "g W" '("→ word end" . evil-forward-word-end)

           ;; Not sure how to use these two as they need text objs
           "g n" '("next match" , evil-next-match)
           "g N" '("prev match" , evil-previous-match)

           "g P" '("paste after" . evil-paste-before-cursor-after)

           ;; Let's clean out keybindings already in normal mode
           ;; without the initial g:
           "g #" nil   ; evil-search-unbounded-word-backward
           "g *" nil   ; evil-search-unbounded-word-forward
           "g ^" nil   ; evil-first-non-blank
           "g $" nil   ; evil-end-of-line
           "g _" nil   ; evil-last-non-blank ... eh
           "g 0" nil   ; evil-beginning-of-line
           "g &" nil   ; evil-ex-repeat-global-substitute
           "g 8" nil   ; what-cursor-position
           "g F" nil   ; evil-find-file-at-point-with-line
           "g J" nil   ; evil-join-whitespace
           "g I" nil   ; evil-insert-0-line ... just use I
           "g m" nil   ; evil-middle-of-visual-line
           "g M" nil   ; evil-percentage-of-line ... middle?
           "g T" nil   ; tab-bar-switch-to-prev-tab
           "g t" nil   ; tab-bar-switch-to-next-tab

           "g j" nil   ; This will be a major-mode-specific keybinding
           "g k" nil

           (kbd "g C-]") nil
           (kbd "g <up>") nil
           (kbd "g <down>") nil
           (kbd "g <left>") nil
           (kbd "g <right>") nil
           (kbd "g <home>") nil
           (kbd "g <end>") nil))

While we are at it, let’s readd, and relabel the z command functions:

(use-package evil
  :general
  (:states '(normal visual motion operator)
           "z q" '("fill para" . fill-paragraph)
           "z Q" '("unfill para" . unfill-paragraph)
           "z p" '("unfill para" . unfill-paragraph)

           "z m" '("scroll to center" . evil-scroll-line-to-center)
           "z t" '("scroll to top" . evil-scroll-line-to-top)
           "z b" '("scroll to bottom" . evil-scroll-line-to-bottom)
           (kbd "z <left>") '("scroll left" . evil-scroll-column-left)
           (kbd "z <right>") '("scroll right" . evil-scroll-column-right)

           "z a" '("toggle fold" . evil-toggle-fold)
           "z f" '("close fold" . evil-close-fold)
           "z o" '("open fold" . evil-open-fold)
           "z F" '("close all folds" . evil-close-folds)
           "z O" '("open all folds" . evil-open-folds)
           ;; Open a fold at point recursively? Never see a need:

           ;; Since I have overridden z-l and whatnot, why have z-h?
           "z e" nil   ; evil-scroll-end-column
           "z h" nil   ; evil-scroll-column-left
           "z l" nil   ; evil-scroll-column-right
           "z r" nil
           "z s" nil   ; evil-scroll-start-column
           "z ^" nil   ; evil-scroll-top-line-to-bottom
           "z +" nil   ; evil-scroll-bottom-line-to-top
           "z -" nil   ; evil-scroll-line-to-bottom-first-non-blank
           "z ." nil   ; evil-scroll-line-to-center-first-non-blank
           (kbd "z RET") nil ; evil-scroll-line-to-top
           (kbd "z <return>") nil)) ; evil-scroll-line-to-top

Top-Level Operations

Let’s try this general “space” prefix by defining some top-level operations, including hitting space twice to bring up the M-x collection of functions:

(ha-leader
  "SPC" '("M-x" . execute-extended-command)
  "."   '("repeat" . repeat)
  "!"   '("shell command" . shell-command)
  "|"   'piper
  "X"   '("org capture" . org-capture)
  "L"   '("store org link" . org-store-link)
  "RET" 'bookmark-jump
  "a"   '(:ignore t :which-key "apps")
  "m"   '(:ignore t :which-key "mode")
  "o"   '(:ignore t :which-key "org/open")
  "o i" 'imenu

  "<escape>" '(keyboard-escape-quit :which-key t)
  "a <escape>" '(keyboard-escape-quit :which-key t)
  "m <escape>" '(keyboard-escape-quit :which-key t)
  "o <escape>" '(keyboard-escape-quit :which-key t)
  "C-g" '(keyboard-escape-quit :which-key t)
  "a C-g" '(keyboard-escape-quit :which-key t)
  "m C-g" '(keyboard-escape-quit :which-key t)
  "o C-g" '(keyboard-escape-quit :which-key t)

  "u"   'universal-argument)

And ways to stop the system:

(ha-leader
  "q"  '(:ignore t :which-key "quit/session")
  "q b" '("bury buffer" . bury-buffer)
  "q w" '("close window" . delete-window)
  "q K" '("kill emacs (and dæmon)" . save-buffers-kill-emacs)
  "q q" '("quit emacs" . save-buffers-kill-terminal)
  "q Q" '("quit without saving" . evil-quit-all-with-error-code)

  "q <escape>" '(keyboard-escape-quit :which-key t)
  "q C-g" '(keyboard-escape-quit :which-key t))

And ways to load my tangled org-files:

(ha-leader
    "h h"   '(:ignore t :which-key "hamacs")
    "h h f" '("features"     . ha-hamacs-features)
    "h h e" '("edit"         . ha-hamacs-find-file)
    "h h j" `("heading jump" . ,(lambda () (interactive) (ha-hamacs-edit-file-heading "~/src/hamacs")))
    "h h h" '("reload"       . ha-hamacs-load)
    "h h a" '("reload all"   . ha-hamacs-reload-all)

    "h h <escape>" '(keyboard-escape-quit :which-key t)
    "h h C-g" '(keyboard-escape-quit :which-key t)
)

File Operations

While find-file is still my bread and butter, I like getting information about the file associated with the buffer. For instance, the file path:

(defun ha-relative-filepath (filepath)
  "Return the FILEPATH without the HOME directory and typical filing locations.
The expectation is that this will return a filepath with the proejct name."
  (let* ((home-re (rx (literal (getenv "HOME")) "/"))
         (work-re (rx (regexp home-re)
                      (or "work" "other" "projects") ; Typical organization locations
                      "/"
                      (optional (or "4" "5" "xway") "/") ; Sub-organization locations
                      )))
    (cond
     ((string-match work-re filepath) (substring filepath (match-end 0)))
     ((string-match home-re filepath) (substring filepath (match-end 0)))
     (t filepath))))

(defun ha-yank-buffer-path (&optional root)
  "Copy the file path of the buffer relative to my 'work' directory, ROOT."
  (interactive)
  (if-let (filename (buffer-file-name (buffer-base-buffer)))
      (message "Copied path to clipboard: %s"
               (kill-new (abbreviate-file-name
                          (if root
                              (file-relative-name filename root)
                            (ha-relative-filepath filename)))))
    (error "Couldn't find filename in current buffer")))

(defun ha-yank-project-buffer-path (&optional root)
  "Copy the file path of the buffer relative to the file's project.
When given ROOT, this copies the filepath relative to that."
  (interactive)
  (if-let* ((filename (buffer-file-name (buffer-base-buffer)))
            (relative (f-relative filename (or nil (project-root (project-current))))))
      (progn
        (kill-new relative)
        (message "Copied path to clipboard: %s" relative))
    (message "Couldn't find filename in current buffer")))

This simple function allows me to load a project-specific file in a numbered window, based on winum:

(defun find-file-in-window (win)
  "Change the buffer in a particular window number."
  (interactive)
  (if (windowp win)
      (aw-switch-to-window win)
    (winum-select-window-by-number win))
  (project-find-file))

With these helper functions in place, I can create a leader collection for file-related functions:

(ha-leader
  "f"  '(:ignore t :which-key "files")
  "f a" '("load any" . find-file)
  "f f" '("load" . project-find-file)
  "f F" '("load new window" . find-file-other-window)
  "f l" '("locate" . locate)
  "f s" '("save" . save-buffer)
  "f S" '("save as" . write-buffer)
  "f r" '("recent" . recentf-open-files)
  "f c" '("copy" . copy-file)
  "f R" '("rename" . rename-file)
  "f x" '("delete" . delete-file)
  "f y" '("yank path" . ha-yank-buffer-path)
  "f Y" '("yank path from project" . ha-yank-project-buffer-path)
  "f d" '("dired" . dired)
  "f D" '("find dired" . find-dired)

  "f 1" '("load win-1" . ha-find-file-window-1)
  "f 2" '("load win-2" . ha-find-file-window-2)
  "f 3" '("load win-3" . ha-find-file-window-3)
  "f 4" '("load win-4" . ha-find-file-window-4)
  "f 5" '("load win-5" . ha-find-file-window-5)
  "f 6" '("load win-6" . ha-find-file-window-6)
  "f 7" '("load win-7" . ha-find-file-window-7)
  "f 8" '("load win-8" . ha-find-file-window-8)
  "f 9" '("load win-9" . ha-find-file-window-9)

  "f <escape>" '(keyboard-escape-quit :which-key t)
  "f C-g" '(keyboard-escape-quit :which-key t))

The d brings up Dired, and D pulls up a dired, not on a single directory, but based on a pattern given to find (see this discussion on Mastering Emacs).

On Unix systems, the locate command is faster than find when searching the whole system, since it uses a pre-computed database, and find is faster if you need to search a specific directory instead of the whole system. On the Mac, we need to change the locate command:

(when (ha-running-on-macos?)
  (setq locate-command "mdfind"))

The advantage of mdfind is that is searches for filename and its contents of your search string.

Trying the spotlight project, as it has a slick interface for selecting files:

(use-package spotlight
  :config (ha-leader "f /" '("search files" . spotlight)))

Buffer Operations

This section groups buffer-related operations under the “SPC b” sequence.

Putting the entire visible contents of the buffer on the clipboard is often useful:

(defun ha-yank-buffer-contents ()
  "Copy narrowed contents of the buffer to the clipboard."
  (interactive)
  (kill-new (buffer-substring-no-properties
             (point-min) (point-max))))

This simple function allows me to switch to a buffer in a numbered window, based on winum:

(defun switch-buffer-in-window (win)
  "Change the buffer in a particular window number."
  (interactive)
  (if (windowp win)
      (aw-switch-to-window win)
    (winum-select-window-by-number win))
  (consult-project-buffer))

And the collection of useful operations:

(ha-leader
  "b"  '(:ignore t :which-key "buffers")
  "b O" '("other" . project-switch-buffer-to-other-window)
  "b i" '("ibuffer" . ibuffer)
  "b I" '("ibuffer" . ibuffer-other-window)
  "b k" '("persp remove" . persp-remove-buffer)
  "b N" '("new" . evil-buffer-new)
  "b d" '("delete" . persp-kill-buffer*)
  "b r" '("revert" . revert-buffer)
  "b s" '("save" . save-buffer)
  "b S" '("save all" . evil-write-all)
  "b n" '("next" . next-buffer)
  "b p" '("previous" . previous-buffer)
  "b y" '("copy contents" . ha-yank-buffer-contents)
  "b z" '("bury" . bury-buffer)
  "b Z" '("unbury" . unbury-buffer)

  "b 1" '("load win-1" . (lambda () (interactive) (switch-buffer-in-window 1)))
  "b 2" '("load win-2" . (lambda () (interactive) (switch-buffer-in-window 2)))
  "b 3" '("load win-3" . (lambda () (interactive) (switch-buffer-in-window 3)))
  "b 4" '("load win-4" . (lambda () (interactive) (switch-buffer-in-window 4)))
  "b 5" '("load win-5" . (lambda () (interactive) (switch-buffer-in-window 5)))
  "b 6" '("load win-6" . (lambda () (interactive) (switch-buffer-in-window 6)))
  "b 7" '("load win-7" . (lambda () (interactive) (switch-buffer-in-window 7)))
  "b 8" '("load win-8" . (lambda () (interactive) (switch-buffer-in-window 8)))
  "b 9" '("load win-9" . (lambda () (interactive) (switch-buffer-in-window 9)))

  "b <escape>" '(keyboard-escape-quit :which-key t)
  "b C-g" '(keyboard-escape-quit :which-key t))

Bookmarks

I like the idea of dropping returnable bookmarks, however, the built-in behavior doesn’t honor either projects or perspectives, but I use bookmark-in-project package to make a project-specific bookmarks and use that to jump to only bookmarks in the current project.

(use-package bookmark-in-project
  :config
  (ha-leader
    ;; Set or delete a bookmark associated with project:
    "b m" '("set proj mark" . bookmark-in-project-toggle)
    "b M" '("set global mark" . bookmark-set)
    "b X" '("delete mark" . bookmark-delete)
    "b g" '("goto proj mark" . bookmark-in-project-jump)
    "b <down>" '("next mark" . bookmark-in-project-jump-next)
    "b <up>" '("next mark" . bookmark-in-project-jump-previous)))

Centering

After reading this essay, I got to thinking that it would be nice to position the text in a buffer near the top, but show context based on some specific, textual things. My thought is to have a function that prompts for the thing (like the current paragraph, function, etc), but also create thing-specific functions.

(defun ha-center-to-top (thing &optional count)
  "Place THING nearest the point at the top of window.
COUNT is the number of things from point that should be
display at the top of the window.

THING can be any of the following:
  - 'heading (an org-mode headline)
  - 'block (an org-mode block)
  - 'paragraph
  - 'sentence
  - 'line (similar to `recenter-top-bottom')
  - 'comment
  - 'defun"
  (interactive
   (list
    (completing-read "Recenter to: " '("heading" "block"
                                       "paragraph" "sentence" "line"
                                       "comment" "defun")
                     nil t)))
  (unless count (setq count 1))
  ;; Move to the start of the `thing', and then call `recenter-top-bottom':
  (save-excursion
    (cond
     ((equal~ thing 'heading) (org-previous-visible-heading count))
     ((equal~ thing 'block) (org-previous-block count))
     ((equal~ thing 'paragraph) (backward-paragraph count))
     ((equal~ thing 'sentence) (backward-sentence count))
     ((equal~ thing 'comment) (beginning-of-defun-comments count))
     ((equal~ thing 'defun) (beginning-of-defun count)))
    (recenter-top-bottom 0)))

One thing I have always wished is a simple string-or-symbol-or-keyword comparison function. This is helpful since completing-read works best with strings, but calling a Lisp function should take symbols or keywords. It would be easy enough to write after converting everything to a string:

(defun equal~ (obj1 obj2)
  "Tries to coerce OBJ1 and OBJ2 to strings for comparison."
  (let ((str1 (cond
               ((keywordp obj1) (substring (symbol-name obj1) 1))
               ((symbolp obj1) (symbol-name obj1))
               (t obj1)))
        (str2 (cond
               ((keywordp obj2) (substring (symbol-name obj2) 1))
               ((symbolp obj2) (symbol-name obj2))
               (t obj2))))
    (equal str1 str2)))

Let’s write a quick test to make sure this works:

(ert-deftest equal~-test ()
  (should (equal~ "foobar" "foobar"))
  (should (equal~ 'foobar "foobar"))
  (should (equal~ :foobar "foobar"))
  (should (equal~ "foobar"'foobar))
  (should (equal~ 'foobar 'foobar))
  (should (equal~ :foobar 'foobar))
  (should (equal~ "foobar":foobar))
  (should (equal~ 'foobar :foobar))
  (should (equal~ :foobar :foobar)))

Create a number of interactive functions for each thing to recenter to the top:

(defun ha-center-to-top-heading (prefix)
  "Recenter the current org-mode headline to top of window.
PREFIX is a numeric value to specify how many previous headings
should be shown."
  (interactive "P")
  (ha-center-to-top 'heading prefix))

(defun ha-center-to-top-block (prefix)
  "Recenter the current org-mode block to top of window.
PREFIX is a numeric value to specify how many previous blocks
should be shown."
  (interactive "P")
  (ha-center-to-top 'block prefix))

(defun ha-center-to-top-paragraph (prefix)
  "Recenter the current paragraph to the top of window.
PREFIX is a numeric value to specify how many previous paragraphs
should be shown."
  (interactive "P")
  (ha-center-to-top 'paragraph prefix))

(defun ha-center-to-top-sentence (prefix)
  "Recenter the current org-mode headline to top of window.
PREFIX is a numeric value to specify how many previous sentences
should be shown."
  (interactive "P")
  (ha-center-to-top 'sentence prefix))

(defun ha-center-to-top-comment (prefix)
  "Recenter the current org-mode headline to top of window.
PREFIX is a numeric value to specify how many previous comments
should be shown."
  (interactive "P")
  (ha-center-to-top 'comment prefix))

(defun ha-center-to-top-defun (prefix)
  "Recenter the current org-mode headline to top of window.
PREFIX is a numeric value to specify how many previous defuns
should be shown."
  (interactive "P")
  (ha-center-to-top 'defun prefix))

Let’s bind them all to a leader prefix:

(ha-leader
  "c"  '(:ignore t :which-key "center display")
  "c p" '("paragraph" . ha-center-to-top-paragraph)
  "c s" '("sentence" . ha-center-to-top-sentence)
  "c c" '("comment" . ha-center-to-top-comment)
  "c f" '("defun" . ha-center-to-top-defun)
  "c h" '("org-headline" . ha-center-to-top-heading)
  "c o" '("only headline" . org-narrow-to-subtree)
  "c b" '("org-block" . ha-center-to-top-block)
  "c a" '("only block" . org-edit-special))

Toggle Switches

The goal here is toggle switches and other miscellaneous settings.

(ha-leader
  "t"   '(:ignore t :which-key "toggles")
  "t a" '("abbrev"         . abbrev-mode)
  "t d" '("debug"          . toggle-debug-on-error)
  "t F" '("show functions" . which-function-mode)
  "t f" '("auto-fill"      . auto-fill-mode)
  "t l" '("line numbers"   . ha-toggle-relative-line-numbers)
  "t o" '("overwrite"      . overwrite-mode)
  "t m" '("menu bar"       . menu-bar-mode)
  "t R" '("read only"      . read-only-mode)
  "t r" '("recentf mode"   . recentf-mode)
  "t t" '("truncate"       . toggle-truncate-lines)
  "t T" '("tramp mode"     . tramp-mode)
  "t v" '("visual"         . visual-line-mode)
  "t w" '("whitespace"     . whitespace-mode)

  "t <escape>" '(keyboard-escape-quit :which-key t)
  "t C-g" '(keyboard-escape-quit :which-key t))

Narrowing

I like the focus the Narrowing features offer, but what a dwim aspect:

(defun ha-narrow-dwim ()
  "Narrow to region or org-tree or widen if already narrowed."
  (interactive)
  (cond
   ((buffer-narrowed-p) (widen))
   ((region-active-p)  (narrow-to-region (region-beginning) (region-end)))
   ((and (fboundp 'logos-focus-mode)
         (seq-contains local-minor-modes 'logos-focus-mode 'eq))
    (logos-narrow-dwim))
   ((eq major-mode 'org-mode) (org-narrow-to-subtree))
   (t  (narrow-to-defun))))

And put it on the toggle menu:

(ha-leader "t n" '("narrow" . ha-narrow-dwim))

Window Operations

While it comes with Emacs, I use winner-mode to undo window-related changes:

(use-package winner
  :custom
  (winner-dont-bind-my-keys t)
  :config
  (winner-mode +1))

Ace Window

Use the ace-window project to jump to any window you see.

Often transient buffers show in other windows, obscuring my carefully crafted display. Instead of jumping into a window, typing q (to either call quit-buffer) if available, or bury-buffer otherwise. This function hooks to ace-window

(defun ha-quit-buffer (window)
  "Quit or bury buffer in a given WINDOW."
  (interactive)
  (aw-switch-to-window window)
  (unwind-protect
      (condition-case nil
          (quit-buffer)
        (error
         (bury-buffer))))
  (aw-flip-window))

Since I use numbers for the window, I can make the commands more mnemonic, and add my own:

(use-package ace-window
  :init
  (setq aw-dispatch-alist
        '((?d aw-delete-window "Delete Window")
          (?m aw-swap-window "Swap Windows")
          (?M aw-move-window "Move Window")
          (?c aw-copy-window "Copy Window")
          (?b switch-buffer-in-window "Select Buffer")
          (?f find-file-in-window "Find File")
          (?n aw-flip-window)
          (?c aw-split-window-fair "Split Fair Window")
          (?s aw-split-window-vert "Split Vert Window")
          (?v aw-split-window-horz "Split Horz Window")
          (?o delete-other-windows "Delete Other Windows")
          (?q ha-quit-buffer "Quit Buffer")
          (?w aw-execute-command-other-window "Execute Command")
          (?? aw-show-dispatch-help)))

  :bind ("s-w" . ace-window))

Keep in mind, these shortcuts work with more than two windows open. For instance, SPC w w d 3 closes the “3” window.

Transpose Windows

My office at work has a monitor oriented vertically, and to move an Emacs with “three columned format” to a “stacked format” I use the transpose-frame package:

(use-package transpose-frame)

Winum

To jump to a window even quicker, use the winum package:

(use-package winum
  :bind (("s-1" . winum-select-window-1)
         ("s-2" . winum-select-window-2)
         ("s-3" . winum-select-window-3)
         ("s-4" . winum-select-window-4)
         ("s-5" . winum-select-window-5)
         ("s-6" . winum-select-window-6)
         ("s-7" . winum-select-window-7)
         ("s-8" . winum-select-window-8)
         ("s-9" . winum-select-window-9)))

This is nice since the window numbers are always present on a Doom modeline, but they sometime order the window numbers differently than ace-window.

(use-package winum
  :config (winum-mode +1))

Let’s try this out with a Hydra since some I can repeat some commands (e.g. enlarge window). It also allows me to organize the helper text.

(use-package hydra
  :config
  (defhydra hydra-window-resize (:color blue :hint nil) "
_w_: select _m_: move/swap _u_: undo  _^_: taller (t)  _+_: text larger
_j_: go up  _d_: delete    _U_: undo+ _v_: shorter (T) _-_: text smaller
_k_: down   _e_: balance   _r_: redo  _>_: wider       _F_: font larger
_h_: left   _n_: v-split   _R_: redo+ _<_: narrower    _f_: font smaller
_l_: right  _s_: split   _o_: only this window     _c_: choose (also 1-9)"
    ("w" ace-window)
    ("c" other-window                 :color pink) ; change window
    ("o" delete-other-windows)          ; “Only” this window
    ("d" delete-window)     ("x" delete-window)

    ;; Ace Windows ... select the window to affect:
    ("m" ace-swap-window)
    ("D" ace-delete-window)
    ("O" ace-delete-other-windows)

    ("u" winner-undo)
    ("U" winner-undo                 :color pink)
    ("C-r" winner-redo)
    ("r" winner-redo)
    ("R" winner-redo                 :color pink)

    ("J" evil-window-down            :color pink)
    ("K" evil-window-up              :color pink)
    ("H" evil-window-left            :color pink)
    ("L" evil-window-right           :color pink)

    ("j" evil-window-down)
    ("k" evil-window-up)
    ("h" evil-window-left)
    ("l" evil-window-right)

    ("x" transpose-frame)
    ("s" hydra-window-split/body)
    ("n" hydra-window-split/body)

    ("F" font-size-increase          :color pink)
    ("f" font-size-decrease          :color pink)
    ("+" text-scale-increase         :color pink)
    ("=" text-scale-increase         :color pink)
    ("-" text-scale-decrease         :color pink)
    ("^" evil-window-increase-height :color pink)
    ("v" evil-window-decrease-height :color pink)
    ("t" evil-window-increase-height :color pink)
    ("T" evil-window-decrease-height :color pink)
    (">" evil-window-increase-width  :color pink)
    ("<" evil-window-decrease-width  :color pink)
    ("." evil-window-increase-width  :color pink)
    ("," evil-window-decrease-width  :color pink)
    ("e" balance-windows)

    ("1" winum-select-window-1)
    ("2" winum-select-window-2)
    ("3" winum-select-window-3)
    ("4" winum-select-window-4)
    ("5" winum-select-window-5)
    ("6" winum-select-window-6)
    ("7" winum-select-window-7)
    ("8" winum-select-window-8)
    ("9" winum-select-window-9)

    ;; Extra bindings:
    ("q" nil :color blue)))

(ha-leader "w" '("windows" . hydra-window-resize/body))

Window Splitting

When I split a window, I have a following intentions:

  • Split and open a file from the prespective/project in the new window
  • Split and change to a buffer from the prespective in the new window
  • Split and move focus to the new window … you know, to await a new command

And when creating new windows, why isn’t the new window selected? Also, when I create a new window, I typically want a different buffer or file shown.

(defun ha-new-window (side file-or-buffer)
  (pcase side
    (:left  (split-window-horizontally))
    (:right (split-window-horizontally)
            (other-window 1))
    (:above (split-window-vertically))
    (:below (split-window-vertically)
            (other-window 1)))
  (pcase file-or-buffer
    (:file   (call-interactively 'project-find-file))
    (:buffer (call-interactively 'project-switch-to-buffer))
    (:term   (ha-shell (project-root (project-current))))))

Shame that hydra doesn’t have an ignore-case feature.

(use-package hydra
  :config
  (defhydra hydra-window-split (:color blue :hint nil)
    ("s" hydra-window-split-below/body "below")
    ("j" hydra-window-split-below/body "below")
    ("k" hydra-window-split-above/body "above")
    ("h" hydra-window-split-left/body "left")
    ("l" hydra-window-split-right/body "right")
    ("n" hydra-window-split-right/body "right"))

  (defhydra hydra-window-split-above (:color blue :hint nil)
    ("b" (lambda () (interactive) (ha-new-window :above :buffer)) "switch buffer")
    ("f" (lambda () (interactive) (ha-new-window :above :file))   "load file")
    ("t" (lambda () (interactive) (ha-new-window :above :term))   "terminal")
    ("k" split-window-below                                  "split window"))

  (defhydra hydra-window-split-below (:color blue :hint nil)
    ("b" (lambda () (interactive) (ha-new-window :below :buffer))        "switch buffer")
    ("f" (lambda () (interactive) (ha-new-window :below :file))          "load file    ")
    ("t" (lambda () (interactive) (ha-new-window :below :term))          "terminal")
    ("j" (lambda () (interactive) (split-window-below) (other-window 1)) "split window ")
    ("s" (lambda () (interactive) (split-window-below) (other-window 1)) "split window "))

  (defhydra hydra-window-split-right (:color blue :hint nil)
    ("b" (lambda () (interactive) (ha-new-window :right :buffer))        "switch buffer")
    ("f" (lambda () (interactive) (ha-new-window :right :file))          "load file")
    ("t" (lambda () (interactive) (ha-new-window :right :term))          "terminal")
    ("l" (lambda () (interactive) (split-window-right) (other-window 1)) "split window ")
    ("n" (lambda () (interactive) (split-window-right) (other-window 1)) "split window "))

  (defhydra hydra-window-split-left (:color blue :hint nil)
    ("b" (lambda () (interactive) (ha-new-window :left :buffer))         "switch buffer")
    ("f" (lambda () (interactive) (ha-new-window :left :file))           "load file    ")
    ("t" (lambda () (interactive) (ha-new-window :left :term))           "terminal")
    ("h" split-window-right                                         "split window")))

This means that, without thinking, the following just works:

SPC w s s s
creates a window directly below this.
SPC w n n n
creates a window directly to the right.

But, more importantly, the prefix w s gives me more precision to view what I need.

Search Operations

Ways to search for information goes under the s key. The venerable sage has always been grep, but we now have new-comers, like ripgrep, which are really fast.

ripgrep

Install the rg package, which builds on the internal grep system, and creates a *rg* window with compilation mode, so C-j and C-k will move and show the results by loading those files.

(use-package rg
  :config
  ;; Make an interesting Magit-like menu of options, which I don't use much:
  (rg-enable-default-bindings (kbd "M-R"))

  (ha-leader
    "s"  '(:ignore t :which-key "search")
    "s q" '("close" . ha-rg-close-results-buffer)
    "s r" '("dwim" . rg-dwim)
    "s s" '("search" . rg)
    "s S" '("literal" . rg-literal)
    "s p" '("project" . rg-project)
    "s d" '("directory" . rg-dwim-project-dir)
    "s f" '("file only" . rg-dwim-current-file)
    "s j" '("next results" . ha-rg-go-next-results)
    "s k" '("prev results" . ha-rg-go-previous-results)
    "s b" '("results buffer" . ha-rg-go-results-buffer)

    "s <escape>" '(keyboard-escape-quit :which-key t)
    "s C-g" '(keyboard-escape-quit :which-key t))

  (defun ha-rg-close-results-buffer ()
    "Close to the `*rg*' buffer that `rg' creates."
    (interactive)
    (kill-buffer "*rg*"))

  (defun ha-rg-go-results-buffer ()
    "Pop to the `*rg*' buffer that `rg' creates."
    (interactive)
    (pop-to-buffer "*rg*"))

  (defun ha-rg-go-next-results ()
    "Bring the next file results into view."
    (interactive)
    (ha-rg-go-results-buffer)
    (next-error-no-select)
    (compile-goto-error))

  (defun ha-rg-go-previous-results ()
    "Bring the previous file results into view."
    (interactive)
    (ha-rg-go-results-buffer)
    (previous-error-no-select)
    (compile-goto-error)))

Note we bind the key M-R to the rg-menu, which is a Magit-like interface to ripgrep.

I don’t understand the bug associated with the :general extension to use-package, but it works, but stops everything else from working, so pulling it out into its own use-package section addresses that issue:

(use-package rg
  :general (:states 'normal "gS" 'rg-dwim))

wgrep

The wgrep package integrates with ripgrep. Typically, you hit i to automatically go into wgrep-mode and edit away, but since I typically want to edit everything at the same time, I have a toggle that should work as well:

(use-package wgrep
  :after rg
  :commands wgrep-rg-setup
  :hook (rg-mode-hook . wgrep-rg-setup)
  :config
  (ha-leader
    :keymaps 'rg-mode-map  ; Actually, `i' works!
    "s w" '("wgrep-mode" . wgrep-change-to-wgrep-mode)
    "t w" '("wgrep-mode" . wgrep-change-to-wgrep-mode)))

Text Operations

Stealing much of this from Spacemacs.

(ha-leader
  "x"  '(:ignore t :which-key "text")
  "x a" '("align"            . align-regexp)
  "x q" '("fill paragraph"   . fill-paragraph)
  "x p" '("unfill paragraph" . unfill-paragraph)

  "x <escape>" '(keyboard-escape-quit :which-key t)
  "x C-g" '(keyboard-escape-quit :which-key t))

Unfilling a paragraph joins all the lines in a paragraph into a single line. Taken from here … I use this all the time:

(defun unfill-paragraph ()
  "Convert a multi-line paragraph into a single line of text."
  (interactive)
  (let ((fill-column (point-max)))
    (fill-paragraph nil)))

Help Operations

While the C-h is easy enough, I am now in the habit of typing SPC h instead. Since I tweaked the help menu, I craft my own menu:

(ha-leader
  "h"  '(:ignore t :which-key "help")
  "h ." '("cursor position"  . what-cursor-position)
  "h a" '("apropos"          . apropos-command)
  "h c" '("elisp cheatsheet" . shortdoc-display-group)
  "h e" '("errors"           . view-echo-area-messages)
  "h f" '("function"         . helpful-callable)
  "h F" '("font"             . describe-font)
  "h =" '("face"             . describe-face)
  "h k" '("key binding"      . helpful-key)
  "h K" '("key map"          . describe-keymap)
  "h m" '("mode"             . describe-mode)
  "h o" '("symbol"           . describe-symbol)
  "h p" '("package"          . describe-package)
  "h s" '("info symbol"      . info-lookup-symbol)
  "h v" '("variable"         . helpful-variable)
  "h i" '("info"             . info)
  "h j" '("info jump"        . info-apropos)

  "h E" '("emacs info"       . (lambda () (interactive) (info "emacs")))
  "h L" '("emacs-lisp"       . (lambda () (interactive) (info "elisp")))
  "h O" '("org info"         . (lambda () (interactive) (info "org")))
  ;; Since I do a lot of literate programming, I appreciate a quick
  ;; jump directly into the Info manual...
  "h B" '("org babel"        . (lambda () (interactive)
                                 (org-info-open "org#Working with Source Code" nil)))

  "h <escape>" '(keyboard-escape-quit :which-key t)
  "h C-g" '(keyboard-escape-quit :which-key t))

Some of these call the Helpful package:

(use-package helpful)

Remember these keys in the Help buffer:

s
view source of the function
i
view info manual of the function

Let’s make Info behave a little more VI-like:

(use-package info
  :straight (:type built-in)
  :general
  (:states 'normal :keymaps 'Info-mode-map
           "B" 'Info-bookmark-jump
           "Y" 'org-store-link
           "H" 'Info-history-back
           "L" 'Info-history-forward
           "u" 'Info-up
           "U" 'Info-directory
           "T" 'Info-top-node
           "p" 'Info-backward-node
           "n" 'Info-forward-node))

Consult

The consult project aims to use libraries like Vertico to enhance specific, built-in, Emacs functions. I appreciate this project that when selecting an element in the minibuffer, it displays what you are looking at… for instance, it previews a buffer before choosing it. Unlike Vertico and Orderless, you need to bind keys to its special functions (or rebind existing keys that do something similar).

(use-package consult
  :after general
  ;; Enable automatic preview at point in the *Completions* buffer. This is
  ;; relevant when you use the default completion UI.
  :hook (completion-list-mode . consult-preview-at-point-mode)

  :bind (("s-v" . consult-yank-pop)
         ("M-X" . consult-mode-command)) ; Hrm...

  :general
  (:states 'normal
           "gp" '("preview paste" . 'consult-yank-pop)
           "gs" '("go to line" . 'consult-line)))

I found the consult-mark as part of this essay about the mark.

Let’s show consult-xref for two functions:

(use-package consult
  :config
  ;; Use Consult to select xref locations with preview
  (setq xref-show-xrefs-function #'consult-xref
        xref-show-definitions-function #'consult-xref))

We sprinkle Consult features throughout the leader menu system:

(use-package consult
  :config
  (ha-leader
    "RET" '("bookmark"            . consult-bookmark)
    "k"   '("marks"               . consult-mark)
    "K"   '("global marks"        . consult-global-mark)
    "b b" '("switch"              . consult-buffer)
    "b B" '("proj switch"         . consult-project-buffer)
    "b o" '("switch win"          . consult-buffer-other-window)
    "f g" '("find grep"           . consult-ripgrep)
    "h I" '("info manual"         . consult-info)
    "h O" '("org info"            . (lambda () (interactive) (consult-info "org")))
    "h M" '("man pages"           . consult-man)
    "t m" '("minor mods"          . consult-minor-mode-menu)
    "x i" '("choose from imenu"   . consult-imenu)
    "x I" '("choose from outline" . consult-outline)
    "x r" '("registers"           . consult-register)
    "x y" '("preview yank"        . consult-yank-pop)))

An under-appreciated version of Consult is the changing your mind aspect. Type SPC b b to switch to a different buffer, and change your mind, “oh, I really need a file!” Type f SPC and it switches to a file browser. Nope, I did need the buffer, type b SPC and your back to buffer switching. Other narrowing keys:

b
Buffers
SPC
Hidden buffers
*
Modified buffers
f
Files (Requires recentf-mode)
r
File registers
m
Bookmarks
p
Project

Embark

The embark project offers actions on targets. I’m primarily thinking of acting on selected items in the minibuffer, but these commands act anywhere. I need an easy-to-use keybinding that doesn’t conflict. Hey, that is what the Super key is for, right?

(use-package embark
  :bind
  (("s-'" . embark-act)               ; Work in minibuffer and elsewhere
   ("s-/" . embark-dwim))

  :init
  ;; Optionally replace the key help with a completing-read interface
  (setq prefix-help-command #'embark-prefix-help-command)

  :config
  (ha-leader "h K" '("keybindings" . embark-bindings)))

In 15 Ways to Use Embark, Karthik Chikmagalur suggests a nifty macro for integrating Embark with Ace Window:

(use-package embark
  :after ace-window
  :config
  (defmacro my/embark-ace-action (fn)
    `(defun ,(intern (concat "my/embark-ace-" (symbol-name fn))) ()
       (interactive)
       (with-demoted-errors "%s"
         (require 'ace-window)
         (let ((aw-dispatch-always t))
           (aw-switch-to-window (aw-select nil))
           (call-interactively (symbol-function ',fn))))))

  (defmacro my/embark-split-action (fn split-type)
    `(defun ,(intern (concat "my/embark-"
                             (symbol-name fn)
                             "-"
                             (car (last  (split-string
                                          (symbol-name split-type) "-"))))) ()
       (interactive)
       (funcall #',split-type)
       (call-interactively #',fn)))

  ;; Use the macros to define some helper functions:
  (my/embark-ace-action find-file)                             ; --> my/embark-ace-find-file
  (my/embark-ace-action switch-to-buffer)                      ; --> my/embark-ace-switch-to-buffer
  (my/embark-ace-action bookmark-jump)                         ; --> my/embark-ace-bookmark-jump
  (my/embark-split-action find-file split-window-below)        ; --> my/embark-find-file-below
  (my/embark-split-action find-file split-window-right)        ; --> my/embark-find-file-right
  (my/embark-split-action switch-to-buffer split-window-below) ; --> my/embark-switch-to-buffer-below
  (my/embark-split-action switch-to-buffer split-window-right) ; --> my/embark-switch-to-buffer-right
  (my/embark-split-action bookmark-jump split-window-below)    ; --> my/embark-bookmark-jump-below
  (my/embark-split-action bookmark-jump split-window-right))   ; --> my/embark-bookmark-jump-right

We can rebind the various embark-xyz-map with calls to our macroized functions:

(use-package embark
  :bind
  (:map embark-file-map
   ("y" . embark-copy-as-kill)
   ("Y" . embark-save-relative-path)
   ("W" . nil)
   ("w" . my/embark-ace-find-file)
   ("2" . my/embark-find-file-below)
   ("3" . my/embark-find-file-right)
   :map embark-buffer-map
   ("y" . embark-copy-as-kill)
   ("w" . my/embark-ace-switch-to-buffer)
   ("2" . my/embark-switch-to-buffer-below)
   ("3" . my/embark-switch-to-buffer-right)
   :map embark-file-map
   ("y" . embark-copy-as-kill)
   ("w" . my/embark-ace-bookmark-jump)
   ("2" . my/embark-bookmark-jump-below)
   ("3" . my/embark-bookmark-jump-right)))

According to this essay, Embark cooperates well with the Marginalia and Consult packages. Neither of those packages is a dependency of Embark, but Embark supplies a hook for Consult where Consult previews can be done from Embark Collect buffers:

(use-package embark-consult
  :after (embark consult)
  :demand t ; only necessary if you have the hook below
  ;; if you want to have consult previews as you move around an
  ;; auto-updating embark collect buffer
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))

According to the Embark-Consult page:

Users of the popular which-key package may prefer to use the embark-which-key-indicator from the Embark wiki. Just copy its definition from the wiki into your configuration and customize the embark-indicators user option to exclude the mixed and verbose indicators and to include embark-which-key-indicator.

In other words, typing s-. to call Embark, specifies the options in a buffer, but the following code puts them in a smaller configuration directly above the selections.

(defun embark-which-key-indicator ()
  "An embark indicator that displays keymaps using which-key.
The which-key help message will show the type and value of the
current target followed by an ellipsis if there are further
targets."
  (lambda (&optional keymap targets prefix)
    (if (null keymap)
        (which-key--hide-popup-ignore-command)
      (which-key--show-keymap
       (if (eq (plist-get (car targets) :type) 'embark-become)
           "Become"
         (format "Act on %s '%s'%s"
                 (plist-get (car targets) :type)
                 (embark--truncate-target (plist-get (car targets) :target))
                 (if (cdr targets) "" "")))
       (if prefix
           (pcase (lookup-key keymap prefix 'accept-default)
             ((and (pred keymapp) km) km)
             (_ (key-binding prefix 'accept-default)))
         keymap)
       nil nil t (lambda (binding)
                   (not (string-suffix-p "-argument" (cdr binding))))))))

(setq embark-indicators
      '(embark-which-key-indicator
        embark-highlight-indicator
        embark-isearch-highlight-indicator))

(defun embark-hide-which-key-indicator (fn &rest args)
  "Hide the which-key indicator immediately when using the completing-read prompter."
  (which-key--hide-popup-ignore-command)
  (let ((embark-indicators
         (remq #'embark-which-key-indicator embark-indicators)))
    (apply fn args)))

(advice-add #'embark-completing-read-prompter
            :around #'embark-hide-which-key-indicator)

Technical Artifacts

Let’s provide a name so we can require this file: