4db75eb591
Gives the same feature, but has offers more.
2065 lines
93 KiB
Org Mode
2065 lines
93 KiB
Org Mode
#+TITLE: General Emacs Configuration
|
||
#+AUTHOR: Howard X. Abrams
|
||
#+DATE: 2020-09-10
|
||
|
||
A literate programming file for configuring Emacs.
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
;;; ha-config --- Emacs configuration. -*- lexical-binding: t; -*-
|
||
;;
|
||
;; © 2020-2022 Howard X. Abrams
|
||
;; Licensed under a Creative Commons Attribution 4.0 International License.
|
||
;; See http://creativecommons.org/licenses/by/4.0/
|
||
;;
|
||
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
||
;; Maintainer: Howard X. Abrams
|
||
;; Created: September 10, 2020
|
||
;;
|
||
;; This file is not part of GNU Emacs.
|
||
;;
|
||
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
|
||
;; ~/other/hamacs/ha-config.org
|
||
;; Using `find-file-at-point', and tangle the file to recreate this one .
|
||
;;
|
||
;;; Code:
|
||
#+end_src
|
||
* Basic Configuration
|
||
I begin with configuration of Emacs that isn’t /package-specific/. For instance, I hate a fat-finger that stop Emacs:
|
||
#+begin_src emacs-lisp
|
||
(setq confirm-kill-emacs 'yes-or-no-p)
|
||
#+end_src
|
||
|
||
New way to display line-numbers. I set mine to =relative= so that I can jump up and down by that value. Set this to =nil= to turn off, or =t= to be absolute.
|
||
#+begin_src emacs-lisp
|
||
(setq display-line-numbers t
|
||
display-line-numbers-type 'relative)
|
||
#+end_src
|
||
|
||
I like the rendering to curved quotes using [[help:text-quoting-style][text-quoting-style]], because it improves the readability of documentation strings in the =∗Help∗= buffer and whatnot.
|
||
#+begin_src emacs-lisp
|
||
(setq text-quoting-style 'curve)
|
||
#+end_src
|
||
|
||
Changes and settings I like introduced in Emacs 28:
|
||
#+begin_src emacs-lisp
|
||
(setq use-short-answers t
|
||
describe-bindings-outline t
|
||
completions-detailed t)
|
||
#+end_src
|
||
|
||
As [[https://tecosaur.github.io/emacs-config/config.html][tec wrote]], I want to use =~/.authsource.gpg= as I don’t want to accidentaly purge this file cleaning =~/.emacs.d=, and let's cache as much as possible, as my home machine is pretty safe, and my laptop is shutdown a lot. Also, as [[https://www.bytedude.com/gpg-in-emacs/][bytedude]] mentions, I need to se the =epa-pineentry-mode= to =loopback= to actually get a prompt for the password, instead of an error.
|
||
#+begin_src emacs-lisp
|
||
(use-package epa-file
|
||
:config
|
||
(defvar epa-pinentry-mode)
|
||
(setq epa-file-select-keys nil
|
||
epa-pinentry-mode 'loopback
|
||
auth-sources '("~/.authinfo.gpg")
|
||
auth-source-cache-expiry nil))
|
||
#+end_src
|
||
|
||
Unicode ellispis are nicer than three dots:
|
||
#+begin_src emacs-lisp
|
||
(setq truncate-string-ellipsis "…")
|
||
#+end_src
|
||
More settings:
|
||
|
||
When I get an error, I need a stack trace to figure out the problem. Yeah, when I stop fiddling with Emacs, this should go off:
|
||
#+begin_src emacs-lisp
|
||
(setq debug-on-error t)
|
||
#+end_src
|
||
|
||
The venerable [[help:hippie-expand][hippie-expand]] function does a better job than the default, [[help:dabbrev-expand][dabbrev-expand]], so let’s swap it out (see this [[https://www.masteringemacs.org/article/text-expansion-hippie-expand][essay]] by Mickey Petersen):
|
||
#+begin_src emacs-lisp
|
||
(global-set-key [remap dabbrev-expand] 'hippie-expand)
|
||
#+end_src
|
||
Details? Check out its [[help:hippie-expand-try-functions-list][list of expanders]].
|
||
|
||
Let’s bind ~TAB~ instead of the default ~M-/~. By default, ~TAB~ re-indents the line, but I find that I want that feature when I’m in Evil’s =normal state= and hit the ~=~ key, so changing this sounds good. But why not /have both/?
|
||
#+begin_src emacs-lisp
|
||
(advice-add #'indent-for-tab-command :after #'hippie-expand)
|
||
#+end_src
|
||
Now while we’re typing along, we can hit the ~TAB~ key after partially typing a word to have it completed.
|
||
|
||
And some Mac-specific settings:
|
||
#+begin_src emacs-lisp
|
||
(when (ha-running-on-macos?)
|
||
(setq mac-option-modifier 'meta
|
||
mac-command-modifier 'super)
|
||
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
|
||
(add-to-list 'default-frame-alist '(ns-appearance . dark)))
|
||
#+end_src
|
||
* Support Packages
|
||
** Piper and DWIM Shell Commands
|
||
Rewriting my shell scripts in Emacs Lisp uses my [[https://gitlab.com/howardabrams/emacs-piper][emacs-piper project]], and this code spills into my configuration code, so let's load it now:
|
||
#+begin_src emacs-lisp
|
||
(use-package piper
|
||
:straight (:host gitlab :repo "howardabrams/emacs-piper")
|
||
:commands piper shell-command-to-list ; I use this function often
|
||
:bind (:map evil-normal-state-map
|
||
("C-M-|" . piper)
|
||
("C-|" . piper-user-interface)))
|
||
#+end_src
|
||
** Yet Another Snippet System (YASnippets)
|
||
Using [[https://github.com/joaotavora/yasnippet][yasnippet]] to convert templates into text:
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package yasnippet
|
||
:config
|
||
(add-to-list 'yas-snippet-dirs (expand-file-name "snippets" user-emacs-directory))
|
||
(yas-global-mode +1))
|
||
#+end_src
|
||
Check out [[http://joaotavora.github.io/yasnippet/][the documentation]] for writing them.
|
||
|
||
Seems the best [[https://github.com/hlissner/doom-snippets][collection of snippets]] is what Henrik Lissner has made for Doom (otherwise, we should use [[http://github.com/AndreaCrotti/yasnippet-snippets][yasnippet-snippets]] package):
|
||
#+begin_src emacs-lisp
|
||
(use-package doom-snippets
|
||
:after yasnippet
|
||
:straight (:host github :repo "doomemacs/snippets"))
|
||
#+end_src
|
||
*Note:* Including his snippets also includes some [[https://github.com/hlissner/doom-snippets#snippets-api][helper functions]] and other features.
|
||
** Auto Insert Templates
|
||
The [[https://www.emacswiki.org/emacs/AutoInsertMode][auto-insert]] feature is a wee bit complicated. All I want is to associate a filename regular expression with a YASnippet template. I'm stealing some ideas from Henrik Lissner's [[https://github.com/hlissner/doom-emacs/blob/develop/modules/editor/file-templates/autoload.el][set-file-template!]] macro, but simpler?
|
||
#+begin_src emacs-lisp
|
||
(use-package autoinsert
|
||
:init
|
||
(setq auto-insert-directory (expand-file-name "templates" user-emacs-directory))
|
||
;; Don't prompt before insertion:
|
||
(setq auto-insert-query nil)
|
||
|
||
(add-hook 'find-file-hook 'auto-insert)
|
||
(auto-insert-mode t))
|
||
#+end_src
|
||
Since auto insertion requires entering data for particular fields, and for that Yasnippet is better, so in this case, we combine them:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-autoinsert-yas-expand()
|
||
"Replace text in yasnippet template."
|
||
(yas-expand-snippet (buffer-string) (point-min) (point-max)))
|
||
#+end_src
|
||
|
||
And since I'll be associating snippets with new files all over my configuration, let's make a helper function:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-auto-insert-file (filename-re snippet-name)
|
||
"Autofill file buffer matching FILENAME-RE regular expression.
|
||
The contents inserted from the YAS SNIPPET-NAME."
|
||
;; The define-auto-insert takes a regular expression and an ACTION:
|
||
;; ACTION may also be a vector containing successive single actions.
|
||
(define-auto-insert filename-re
|
||
(vector snippet-name 'ha-autoinsert-yas-expand)))
|
||
#+end_src
|
||
|
||
As an example of its use, any Org files loaded in /this project/ should insert my config file:
|
||
#+begin_src emacs-lisp
|
||
(ha-auto-insert-file (rx "hamacs/" (one-or-more any) ".org" eol) "hamacs-config")
|
||
#+end_src
|
||
** Request System
|
||
The above code needs the [[https://github.com/tkf/emacs-request][request]] package:
|
||
#+begin_src emacs-lisp
|
||
(use-package request
|
||
:init
|
||
(defvar ha-dad-joke nil "Holds the latest dad joke.")
|
||
|
||
:config
|
||
(defun ha-dad-joke ()
|
||
"Display a random dad joke."
|
||
(interactive)
|
||
(message (ha--dad-joke)))
|
||
|
||
(defun ha--dad-joke ()
|
||
"Return string containing a dad joke from www.icanhazdadjoke.com."
|
||
(setq ha-dad-joke nil) ; Clear out old joke
|
||
(ha--dad-joke-request)
|
||
(ha--dad-joke-wait))
|
||
|
||
(defun ha--dad-joke-wait ()
|
||
(while (not ha-dad-joke)
|
||
(sit-for 1))
|
||
(unless ha-dad-joke
|
||
(ha--dad-joke-wait))
|
||
ha-dad-joke)
|
||
|
||
(defun ha--dad-joke-request ()
|
||
(request "https://icanhazdadjoke.com"
|
||
:sync t
|
||
:complete (cl-function
|
||
(lambda (&key data &allow-other-keys)
|
||
(setq ha-dad-joke data))))))
|
||
#+end_src
|
||
*** Dad Jokes!
|
||
The /critical part/ here, is the [[https://icanhazdadjoke.com/][Dad Joke]] function, a =curl= call to a web service:
|
||
#+begin_src sh
|
||
curl -sH "Accept: text/plain" https://icanhazdadjoke.com/
|
||
#+end_src
|
||
For this, I use the =request= package, which is /asynchronous/
|
||
#+begin_src emacs-lisp
|
||
#+end_src
|
||
|
||
* Configuration Changes
|
||
** Initial Settings and UI
|
||
Let's turn off the menu and other settings:
|
||
#+begin_src emacs-lisp
|
||
(when (display-graphic-p)
|
||
(tool-bar-mode -1)
|
||
(scroll-bar-mode -1)
|
||
(horizontal-scroll-bar-mode -1)
|
||
(setq visible-bell 1))
|
||
#+end_src
|
||
|
||
I like being able to enable local variables in =.dir-local.el= files:
|
||
#+begin_src emacs-lisp
|
||
(setq enable-local-variables t)
|
||
#+end_src
|
||
** File Access
|
||
*** Changes on Save
|
||
Always spaces and never tabs. Note that we use =setq-default= since [[elisp:(describe-variable 'indent-tabs-mode)][indent-tabs-mode]] is a /buffer-local/ variable, meaning using =setq=, sets it for /that buffer file/. We want this globally the default:
|
||
#+begin_src emacs-lisp
|
||
(setq-default indent-tabs-mode nil)
|
||
#+end_src
|
||
|
||
When I push changes to my files to Gerrit and other code review, I don’t want trailing spaces or any tabs to appear, so let’s fix all files when I [[elisp:(describe-variable 'before-save-hook)][save them]]:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-cleanup-buffer-file ()
|
||
"Cleanup a file, often done before a file save."
|
||
(interactive)
|
||
(ignore-errors
|
||
(unless (equal major-mode 'makefile-bsdmake-mode)
|
||
(untabify (point-min) (point-max)))
|
||
(delete-trailing-whitespace)))
|
||
|
||
(add-hook 'before-save-hook #'ha-cleanup-buffer-file)
|
||
#+end_src
|
||
*** Recent Files
|
||
The [[https://www.emacswiki.org/emacs/RecentFiles][recentf]] feature has been in Emacs for a long time, but it has a problem with Tramp, as we need to turn off the cleanup feature that attempts to =stat= all the files and remove them from the =recent= accessed list if they are readable. The requires recentf to open up a remote files which blocks Emacs at the most inopportune times… like when trying to reboot the machine.
|
||
#+begin_src emacs-lisp
|
||
(use-package recentf
|
||
:straight (:type built-in)
|
||
:config
|
||
(setq recentf-auto-cleanup 'never) ;; disable before we start recentf!
|
||
(recentf-mode 1))
|
||
#+end_src
|
||
*** File Backups
|
||
While I use git as much as I can, sometimes Emacs’ built-in file backup and versioning feature has saved me for files that aren’t.
|
||
|
||
As [[https://philjackson.github.io//emacs/backups/2022/01/31/keeping-backups-of-every-edited-file/][Phil Jackson]] mentioned, Emacs has a lot of variations to its file backup strategy, and either change the [[help:backup-directory-alist][backup-directory-alist]] to put individual file backups elsewhere, e.g.
|
||
#+begin_src emacs-lisp
|
||
(setq backup-directory-alist `(("." . ,(concat user-emacs-directory "backups"))))
|
||
#+end_src
|
||
|
||
Or leave them in the current directory, but create an alias so =ls= doesn’t display them, e.g.
|
||
#+begin_src sh
|
||
alias ls="ls --color=auto --hide='*~'"
|
||
#+end_src
|
||
|
||
I'm leaving them side-by-side, but I am keeping some extra copies:
|
||
#+begin_src emacs-lisp
|
||
(setq create-lockfiles nil ; Having .# files around ain't helpful
|
||
auto-save-default t
|
||
delete-old-versions t
|
||
kept-new-versions 6
|
||
kept-old-versions 2
|
||
version-control t)
|
||
#+end_src
|
||
The [[help:version-control][version-control]] variable affect backups (not some sort of global VC setting), this makes numeric backups.
|
||
*** Auto Save of Files
|
||
Save the file whenever I move away from Emacs (see [[https://irreal.org/blog/?p=10314][this essay]]):
|
||
#+begin_src emacs-lisp
|
||
(defun save-all-buffers ()
|
||
"Saves all buffers, because, why not?"
|
||
(interactive)
|
||
(save-some-buffers t))
|
||
|
||
(add-hook 'focus-out-hook 'save-all-buffers)
|
||
#+end_src
|
||
** Completing Read User Interface
|
||
After using Ivy, I am going the route of a =completing-read= interface that extends the original Emacs API, as opposed to implementing backend-engines or complete replacements.
|
||
*** Vertico
|
||
The [[https://github.com/minad/vertico][vertico]] package puts the completing read in a vertical format, and like [[https://github.com/raxod502/selectrum#vertico][Selectrum]], it extends Emacs’ built-in functionality, instead of adding a new process. This means all these projects work together.
|
||
#+begin_src emacs-lisp
|
||
(use-package vertico
|
||
:config (vertico-mode))
|
||
#+end_src
|
||
My issue with Vertico is when calling =find-file=, the Return key opens =dired=, instead of inserting the directory at point. This package addresses this:
|
||
#+begin_src emacs-lisp
|
||
(use-package vertico-directory
|
||
:straight (el-patch :files ("~/.emacs.d/straight/repos/vertico/extensions/vertico-directory.el"))
|
||
;; More convenient directory navigation commands
|
||
:bind (:map vertico-map
|
||
("RET" . vertico-directory-enter)
|
||
; ("DEL" . vertico-directory-delete-word)
|
||
("M-RET" . minibuffer-force-complete-and-exit)
|
||
("M-TAB" . minibuffer-complete))
|
||
;; Tidy shadowed file names
|
||
:hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
|
||
#+end_src
|
||
*** Hotfuzz
|
||
This fuzzy completion style is like the built-in =flex= style, but has a better scoring algorithm, non-greedy and ranks completions that match at word; path component; or camelCase boundaries higher.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package hotfuzz)
|
||
#+end_src
|
||
While flexible at matching, you have to get the /order/ correct. For instance, ~alireg~ matches with [[help:align-regexp][align-regexp]], but ~regali~ does not, so we will use =hotfuzz= for scoring, and not use this as a completion-project (see the =fussy= project below).
|
||
*** Orderless
|
||
While the space can be use to separate words (acting a bit like a =.*= regular expression), the [[https://github.com/oantolin/orderless][orderless]] project allows those words to be in any order.
|
||
#+begin_src emacs-lisp
|
||
(use-package orderless
|
||
:commands (orderless-filter)
|
||
:custom
|
||
(completion-styles '(orderless basic))
|
||
(completion-ignore-case t)
|
||
(completion-category-defaults nil)
|
||
(completion-category-overrides '((file (styles partial-completion)))))
|
||
#+end_src
|
||
*Note:* Open more than one file at once with =find-file= with a wildcard. We may also give the =initials= completion style a try.
|
||
*** Fussy Filtering and Matching
|
||
The [[https://github.com/jojojames/fussy][fussy]] project is a fuzzy pattern matching extension for the normal [[help:completing-read][completing-read]] interface. By default, it uses [[https://github.com/lewang/flx][flx]], but we can specify other sorting and filtering algorithms.
|
||
|
||
How does it compare? Once upon a time, I enjoyed typing ~plp~ for =package-list-packages=, and when I switched to [[https://github.com/oantolin/orderless][orderless]], I would need to put a space between the words. While I will continue to play with the different mechanism, I’ll combine =hotfuzz= and =orderless=.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package fussy
|
||
:straight (:host github :repo "jojojames/fussy")
|
||
:config
|
||
(push 'fussy completion-styles)
|
||
(setq completion-category-defaults nil
|
||
completion-category-overrides nil
|
||
fussy-filter-fn 'fussy-filter-orderless-flex
|
||
fussy-score-fn 'fussy-hotfuzz-score))
|
||
#+end_src
|
||
*** Savehist
|
||
Persist history over Emacs restarts using the built-in [[https://www.emacswiki.org/emacs/SaveHist][savehist]] project. Since both Vertico and Selectrum sorts by history position, this should make the choice /smarter/ with time.
|
||
#+begin_src emacs-lisp
|
||
(use-package savehist
|
||
:init
|
||
(savehist-mode))
|
||
#+end_src
|
||
*** Marginalia
|
||
The [[https://github.com/minad/marginalia][marginalia]] package gives a preview of =M-x= functions with a one line description, extra information when selecting files, etc. Nice enhancement without learning any new keybindings.
|
||
|
||
#+begin_src emacs-lisp
|
||
;; Enable richer annotations using the Marginalia package
|
||
(use-package marginalia
|
||
:init
|
||
(setq marginalia-annotators-heavy t)
|
||
:config
|
||
(marginalia-mode))
|
||
#+end_src
|
||
* Key Bindings
|
||
To begin my binding changes, let's turn on [[https://github.com/justbur/emacs-which-key][which-key]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package which-key
|
||
:init (setq which-key-popup-type 'minibuffer)
|
||
:config (which-key-mode))
|
||
#+end_src
|
||
Why would I ever quit Emacs with a simple keybinding? Let’s override it:
|
||
#+begin_src emacs-lisp
|
||
(global-set-key (kbd "s-q") 'bury-buffer)
|
||
#+end_src
|
||
Oh, and let’s not close the frame, but instead, the window:
|
||
#+begin_src emacs-lisp
|
||
(global-set-key (kbd "s-w") 'delete-window)
|
||
#+end_src
|
||
** Undo
|
||
I mean, I /always/ use ~C-/~ for [[help:undo][undo]] (and ~C-?~ for [[help:undo-redo][redo]]), but when I’m on the Mac, I need to cover my bases.
|
||
|
||
Why use [[https://gitlab.com/ideasman42/emacs-undo-fu][undo-fu]] instead of the built-in undo functionality? Well, there isn’t much to the project (that’s a good thing), but It basically doesn’t /cycle/ around the redo, which annoying.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package undo-fu
|
||
:config
|
||
(global-set-key [remap undo] 'undo-fu-only-undo)
|
||
(global-set-key [remap undo-redo] 'undo-fu-only-redo)
|
||
(global-unset-key (kbd "s-z"))
|
||
(global-set-key (kbd "s-z") 'undo-fu-only-undo)
|
||
(global-set-key (kbd "s-S-z") 'undo-fu-only-redo))
|
||
#+end_src
|
||
** Evil-Specific Keybindings
|
||
Can we change Evil at this point? Some tips:
|
||
- [[https://github.com/noctuid/evil-guide]]
|
||
- [[https://nathantypanski.com/blog/2014-08-03-a-vim-like-emacs-config.html]]
|
||
- [[https://stackoverflow.com/questions/25542097/emacs-evil-mode-how-to-change-insert-state-to-emacs-state-automatically][Evil insert state is really Emacs?]] Real answer to that is to set [[help:evil-disable-insert-state-bindings][evil-disable-insert-state-bindings]]
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil
|
||
:init
|
||
(setq evil-undo-system 'undo-fu
|
||
evil-want-fine-undo t ; Be more like Emacs
|
||
evil-disable-insert-state-bindings t
|
||
evil-want-keybinding nil
|
||
evil-want-integration t
|
||
evil-escape-key-sequence "jk"
|
||
evil-escape-unordered-key-sequence t)
|
||
|
||
:config
|
||
;; Underscores are part of a symbolic word in programming languages:
|
||
;; Sure, I could use capital `W' and `B', but I often forget that.
|
||
(modify-syntax-entry ?_ "w")
|
||
|
||
;; Now that `evil-disable-insert-state-bindings' works to use Emacs
|
||
;; keybindings in Evil's insert mode, we no longer need this code:
|
||
;; (setq evil-insert-state-map (make-sparse-keymap))
|
||
;; (define-key evil-insert-state-map (kbd "<escape>") 'evil-normal-state)
|
||
|
||
;; In insert mode, type C-o to execute a single Evil command:
|
||
(define-key evil-insert-state-map (kbd "C-o") 'evil-execute-in-normal-state)
|
||
|
||
(evil-mode))
|
||
#+end_src
|
||
|
||
While I’m pretty good with the VIM keybindings, I would like to play around with the [[https://evil.readthedocs.io/en/latest/extension.html#text-objects][text objects]] and how it compares to others (including the surround), for instance:
|
||
- ~diw~ :: deletes a word, but can be anywhere in it, while ~de~ deletes to the end of the word.
|
||
- ~daw~ :: deletes a word, plus the surrounding space, but not punctuation.
|
||
- ~xis~ :: changes a /sentence,/ and if ~i~ is ~a~, it gets rid of the surrounding whitespace as well. Probably ~das~ and ~cis~.
|
||
- ~xip~ :: changes a /paragraph/.
|
||
- ~xio~ :: changes a /symbol/, which can change for each mode, but works with =snake_case= and other larger-than-word variables.
|
||
- Surrounding punctuation, like quotes, parenthesis, brackets, etc. also work, so ~ci)~ changes all the parameters to a function call, for instance
|
||
- ~a”~ :: a double quoted string
|
||
- ~i”~ :: inner double quoted string
|
||
- ~a'~ :: a single quoted string
|
||
- ~i'~ :: inner single quoted string
|
||
- ~a`~ :: a back quoted string
|
||
- ~i`~ :: inner back quoted string
|
||
|
||
*Note:* The ~x~ in the above examples are ~d~ for delete, ~v~ for select, ~y~ for copying and ~c~ for changing.
|
||
|
||
What text objects are known?
|
||
- ~w~ :: word
|
||
- ~s~ :: sentence
|
||
- ~p~ :: paragraph
|
||
- ~l~ :: lines, with the [[Evil Text Object Line][Text Object Line]] package
|
||
- ~o~ :: symbol, like a variable
|
||
- ~’~ :: a string, surround by quotes, also ~`~ for backticks
|
||
- ~)~ :: parenthesis, also ~}~ and ~]~, see ~g~
|
||
- ~g~ :: within a brace, paren, etc., with the [[Better Parenthesis with Text Object][my extensions below]], see ~b~ and ~f~ for similar functionality.
|
||
- ~d~ :: a /defun/, or code block, similar to ~p~.
|
||
- ~i~ :: indention area, for YAML and Python, with the [[Text Objects based on Indentation][evil-indent-plus]] package
|
||
- ~t~ :: an HTML tag
|
||
- ~c~ :: for comments
|
||
- ~u~ :: for URLs
|
||
- ~a~ :: function arguments (probably a lot like symbol, ~o~) with the [[https://github.com/wcsmith/evil-args][evil-args]] extension (that I’m not bothering with)
|
||
*** Evil Text Object Line
|
||
Delete a line, ~d d~ is in basic VI, but many commands are based on text objects, and the basic text object doesn’t include lines. The [[https://github.com/emacsorphanage/evil-textobj-line][evil-textobj-line]] project adds that:
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-textobj-line)
|
||
#+end_src
|
||
So, ~v i l~ and ~v a l~ works as you’d expect.
|
||
*** Text Objects based on Indentation
|
||
The [[https://github.com/TheBB/evil-indent-plus][evil-indent-plus]] project creates text objects based on the indentation level, similar to how the ~b~ works with “blocks” of code.
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-indent-plus)
|
||
#+end_src
|
||
This can be handy for Python, YAML, and lists in org files. Note that ~i~ works for the current indent, but ~k~ includes one line above and ~j~ includes one line above and below.
|
||
*** Arguments as Text Objects
|
||
The [[https://github.com/wcsmith/evil-args][evil-args]] projects creates text objects for symbols, but with trailing ~,~ or other syntax.
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-args
|
||
:config
|
||
;; bind evil-args text objects
|
||
(define-key evil-inner-text-objects-map "a" 'evil-inner-arg)
|
||
(define-key evil-outer-text-objects-map "a" 'evil-outer-arg)
|
||
|
||
;; bind evil-forward/backward-args
|
||
(define-key evil-normal-state-map "L" 'evil-forward-arg)
|
||
(define-key evil-normal-state-map "H" 'evil-backward-arg)
|
||
(define-key evil-motion-state-map "L" 'evil-forward-arg)
|
||
(define-key evil-motion-state-map "H" 'evil-backward-arg)
|
||
|
||
;; bind evil-jump-out-args
|
||
(define-key evil-normal-state-map "K" 'evil-jump-out-args))
|
||
#+end_src
|
||
For a function, like this Python example, with the cursor on =b=:
|
||
#+begin_src python :tangle no
|
||
def foobar(a, b, c):
|
||
return a + b + c
|
||
#+end_src
|
||
Typing ~d a a~ will delete the argument leaving:
|
||
#+begin_src python :tangle no
|
||
def foobar(a, c):
|
||
return a + b + c
|
||
#+end_src
|
||
*** Better Parenthesis with Text Object
|
||
I took the following clever idea and code from [[http://blog.binchen.org/posts/code-faster-by-extending-emacs-evil-text-object/][this essay]] from Chen Bin for creating a ~xig~ to grab code within any grouping characters, like parens, braces and brackets. For instance, ~dig~ cuts the content inside brackets, etc. First, we need a function to do the work (I changed the original from =my-= to =ha-= so that it is easier for me to distinguish functions from my configuration):
|
||
#+begin_src emacs-lisp
|
||
(defun ha-evil-paren-range (count beg end type inclusive)
|
||
"Get minimum range of paren text object.
|
||
COUNT, BEG, END, TYPE is used. If INCLUSIVE is t, the text object is inclusive."
|
||
(let* ((parens '("()" "[]" "{}" "<>"))
|
||
range
|
||
found-range)
|
||
(dolist (p parens)
|
||
(condition-case nil
|
||
(setq range (evil-select-paren (aref p 0) (aref p 1) beg end type count inclusive))
|
||
(error nil))
|
||
(when range
|
||
(cond
|
||
(found-range
|
||
(when (< (- (nth 1 range) (nth 0 range))
|
||
(- (nth 1 found-range) (nth 0 found-range)))
|
||
(setf (nth 0 found-range) (nth 0 range))
|
||
(setf (nth 1 found-range) (nth 1 range))))
|
||
(t
|
||
(setq found-range range)))))
|
||
found-range))
|
||
#+end_src
|
||
Extend the text object to call this function for both /inner/ and /outer/:
|
||
#+begin_src emacs-lisp
|
||
(evil-define-text-object ha-evil-a-paren (count &optional beg end type)
|
||
"Select a paren."
|
||
:extend-selection t
|
||
(ha-evil-paren-range count beg end type t))
|
||
|
||
(evil-define-text-object ha-evil-inner-paren (count &optional beg end type)
|
||
"Select 'inner' paren."
|
||
:extend-selection nil
|
||
(ha-evil-paren-range count beg end type nil))
|
||
#+end_src
|
||
And the keybindings:
|
||
#+begin_src emacs-lisp
|
||
(define-key evil-inner-text-objects-map "g" #'ha-evil-inner-paren)
|
||
(define-key evil-outer-text-objects-map "g" #'ha-evil-a-paren)
|
||
#+end_src
|
||
|
||
*** Key Chord
|
||
Using the key-chord project allows me to make Escape be on two key combo presses on both sides of my keyboard:
|
||
#+begin_src emacs-lisp
|
||
(use-package key-chord
|
||
:config
|
||
(key-chord-mode t)
|
||
(key-chord-define-global "fd" 'evil-normal-state)
|
||
(key-chord-define-global "jk" 'evil-normal-state)
|
||
(key-chord-define-global "JK" 'evil-normal-state))
|
||
#+end_src
|
||
*** Evil Easy Motion
|
||
The [[https://github.com/PythonNut/evil-easymotion][evil-easymotion]] project combines [[Jump with Avy][avy]] and evil keybindings, where ~SPC j~ shows labels for all the lines below the cursor, so that you can jump right there. This doesn’t work well with a leader, but what about using Key Chords?
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-easymotion
|
||
:config (evilem-default-keybindings "<f19>"))
|
||
#+end_src
|
||
My ~F19~ key is within easy reach of my [[https://configure.zsa.io/moonlander/layouts/L4laD/latest/0][Moonlander configuration]], so this might be a good, if somewhat distracting, feature. Perhaps a better solution is to use [[Jump with Avy][avy]] (see below).
|
||
*** Evil Lion
|
||
The [[https://github.com/edkolev/evil-lion][evil-lion]] package is a wrapper around Emacs’ [[help:align][align]] function. Just a little easier to use. Primary sequence is ~g a i p =~ to align along all the equal characters in the paragraph (block), or ~g a i b RET~ to use a built in rule to align (see below), or ~g a i b /~ to specify a regular expression, similar to [[help:align-regexp][align-regexp]].
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-lion
|
||
:after evil
|
||
:bind (:map evil-normal-state-map
|
||
("g a" . evil-lion-left)
|
||
("g A" . evil-lion-right)
|
||
:map evil-visual-state-map
|
||
("g a" . evil-lion-left)
|
||
("g A" . evil-lion-right)))
|
||
#+end_src
|
||
Lion sounds like /align/ … get it?
|
||
|
||
Where I like to align, is on variable assignments, e.g.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(let ((foobar "Something something")
|
||
(a 42)
|
||
(very-long-var "odd string"))
|
||
;;
|
||
)
|
||
#+end_src
|
||
|
||
If you press ~RETURN~ for the /character/ to align, =evil-lion= package simply calls the built-in [[help:align][align]] function. This function chooses a regular expression based on a list of /rules/, and aligning Lisp variables requires a complicated regular expression. Extend [[elisp:(describe-variable 'align-rules-list)][align-rules-list]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package align
|
||
:straight (:type built-in)
|
||
:config
|
||
(add-to-list 'align-rules-list
|
||
`("lisp-assignments"
|
||
(regexp . ,(rx (group (one-or-more space))
|
||
(or
|
||
(seq "\"" (zero-or-more any) "\"")
|
||
(one-or-more (not space)))
|
||
(one-or-more ")") (zero-or-more space) eol))
|
||
(group . 1)
|
||
(modes . align-lisp-modes))))
|
||
#+end_src
|
||
** General Leader Key Sequences
|
||
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 [[https://github.com/noctuid/general.el][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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package general
|
||
:custom
|
||
(general-use-package-emit-autoloads t)
|
||
|
||
:config
|
||
(general-evil-setup t)
|
||
|
||
(general-create-definer ha-leader
|
||
:states '(normal visual motion)
|
||
:keymaps 'override
|
||
:prefix "SPC"
|
||
:non-normal-prefix "M-SPC"
|
||
:global-prefix "<f13>")
|
||
|
||
(general-create-definer ha-local-leader
|
||
:states '(normal visual motion)
|
||
:prefix "SPC m"
|
||
:global-prefix "<f17>"
|
||
:non-normal-prefix "S-SPC"))
|
||
#+end_src
|
||
*** 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:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"SPC" '("M-x" . execute-extended-command)
|
||
"." '("repeat" . repeat)
|
||
"!" '("shell command" . dwim-shell-command)
|
||
"|" 'piper
|
||
"X" '("org capture" . org-capture)
|
||
"L" '("store org link" . org-store-link)
|
||
"RET" 'bookmark-jump
|
||
"a" '(:ignore t :which-key "apps")
|
||
"o" '(:ignore t :which-key "org/open")
|
||
"o i" 'imenu
|
||
"m" '(:ignore t :which-key "mode")
|
||
"u" 'universal-argument)
|
||
#+end_src
|
||
And ways to stop the system:
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
*** 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:
|
||
#+begin_src emacs-lisp
|
||
(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)))
|
||
(message "Copied path to clipboard: %s"
|
||
(kill-new
|
||
(f-relative filename (or root (projectile-project-root filename)))))
|
||
(error "Couldn't find filename in current buffer")))
|
||
#+end_src
|
||
|
||
Perhaps my OCD is out-of-control, but I want to load a file in another window, but want to control which window.
|
||
#+begin_src emacs-lisp
|
||
(defmacro ha-create-find-file-window (winum)
|
||
(let ((func-name (intern (format "ha-find-file-window-%s" winum)))
|
||
(call-func (intern (format "winum-select-window-%s" winum))))
|
||
`(defun ,func-name ()
|
||
"Call `find-file' in the particular `winum' window."
|
||
(interactive)
|
||
(,call-func)
|
||
(call-interactively 'find-file))))
|
||
|
||
(dolist (winum (number-sequence 1 9))
|
||
(ha-create-find-file-window winum))
|
||
#+end_src
|
||
|
||
With these helper functions in place, I can create a leader collection for file-related functions:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"f" '(:ignore t :which-key "files")
|
||
"f f" '("load" . find-file)
|
||
"f F" '("load new window" . find-file-other-window)
|
||
"f s" '("save" . save-buffer)
|
||
"f S" '("save as" . write-buffer)
|
||
"f SPC" '("project" . projectile-find-file)
|
||
"f r" '("recent" . recentf-open-files)
|
||
"f c" '("copy" . copy-file)
|
||
"f R" '("rename" . rename-file)
|
||
"f D" '("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 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))
|
||
#+end_src
|
||
*** 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:
|
||
#+begin_src emacs-lisp
|
||
(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))))
|
||
#+end_src
|
||
And the collection of useful operations:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"b" '(:ignore t :which-key "buffers")
|
||
"b B" '("switch" . persp-switch-to-buffer)
|
||
"b o" '("switch" . switch-to-buffer-other-window)
|
||
"b O" '("other" . projectile-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)
|
||
|
||
;; And double up on the bookmarks:
|
||
"b m" '("set bookmark" . bookmark-set)
|
||
"b M" '("delete mark" . bookmark-delete))
|
||
#+end_src
|
||
*** Toggle Switches
|
||
The goal here is toggle switches and other miscellaneous settings.
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"t" '(:ignore t :which-key "toggles")
|
||
"t a" '("abbrev" . abbrev-mode)
|
||
"t d" '("debug" . toggle-debug-on-error)
|
||
"t f" '("auto-fill" . auto-fill-mode)
|
||
"t l" '("line numbers" . display-line-numbers-mode)
|
||
"t t" '("truncate" . toggle-truncate-lines)
|
||
"t v" '("visual" . visual-line-mode)
|
||
"t w" '("whitespace" . whitespace-mode))
|
||
#+end_src
|
||
**** Line Numbers
|
||
Since we can't automatically toggle between relative and absolute line numbers, we create this function:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-toggle-relative-line-numbers ()
|
||
(interactive)
|
||
(if (eq display-line-numbers 'relative)
|
||
(setq display-line-numbers t)
|
||
(setq display-line-numbers 'relative)))
|
||
#+end_src
|
||
Add it to the toggle menu:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"t r" '("relative lines" . ha-toggle-relative-line-numbers))
|
||
#+end_src
|
||
**** Narrowing
|
||
I like the focus the [[info:emacs#Narrowing][Narrowing features]] offer, but what a /dwim/ aspect:
|
||
#+begin_src emacs-lisp
|
||
(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))))
|
||
#+end_src
|
||
And put it on the toggle menu:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "t n" '("narrow" . ha-narrow-dwim))
|
||
#+end_src
|
||
*** Window Operations
|
||
While it comes with Emacs, I use [[https://www.emacswiki.org/emacs/WinnerMode][winner-mode]] to undo window-related changes:
|
||
#+begin_src emacs-lisp
|
||
(use-package winner
|
||
:custom
|
||
(winner-dont-bind-my-keys t)
|
||
:config
|
||
(winner-mode +1))
|
||
#+end_src
|
||
Use the [[https://github.com/abo-abo/ace-window][ace-window]] project to jump to any window you see:
|
||
#+begin_src emacs-lisp
|
||
(use-package ace-window)
|
||
#+end_src
|
||
This package, bound to ~SPC w w~, also allows operations specified before choosing the window:
|
||
- ~x~ - delete window
|
||
- ~m~ - swap windows
|
||
- ~M~ - move window
|
||
- ~c~ - copy window
|
||
- ~j~ - select buffer
|
||
- ~n~ - select the previous window
|
||
- ~u~ - select buffer in the other window
|
||
- ~c~ - split window, either vertically or horizontally
|
||
- ~v~ - split window vertically
|
||
- ~b~ - split window horizontally
|
||
- ~o~ - maximize current window
|
||
- ~?~ - show these command bindings
|
||
Keep in mind, these shortcuts work with more than two windows open. For instance, ~SPC w w x 3~ closes the "3" window.
|
||
|
||
To jump to a window even quicker, use the [[https://github.com/deb0ch/emacs-winum][winum package]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package winum
|
||
:config
|
||
(winum-mode +1))
|
||
#+end_src
|
||
And when creating new windows, why isn't the new window selected?
|
||
#+begin_src emacs-lisp
|
||
(defun jump-to-new-window (&rest _arg)
|
||
"Advice function to jump to newly spawned window."
|
||
(other-window 1))
|
||
|
||
(dolist (command '(split-window-below split-window-right
|
||
evil-window-split evil-window-vsplit))
|
||
(advice-add command :after #'jump-to-new-window))
|
||
#+end_src
|
||
This is nice since the window numbers are always present on a Doom modeline, but they order the window numbers /differently/ than =ace-window=. Let's see which I end up liking better.
|
||
|
||
The ~0~ key/window should be always associated with a project-specific tree window:
|
||
#+begin_src emacs-lisp
|
||
(add-to-list 'winum-assign-functions
|
||
(lambda ()
|
||
(when (string-match-p (buffer-name) ".*\\*NeoTree\\*.*") 10)))
|
||
#+end_src
|
||
|
||
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.
|
||
#+begin_src emacs-lisp
|
||
(use-package hydra
|
||
:config
|
||
(defhydra hydra-window-resize (:color blue :hint nil) "
|
||
_w_: select _n_: new _^_: taller (t) _z_: Swap _+_: text larger
|
||
_c_: cycle _d_: delete _V_: shorter (T) _u_: undo _-_: text smaller
|
||
_j_: go up _=_: balance _>_: wider _U_: undo+ _F_: font larger
|
||
_k_: down _m_: maximize _<_: narrower _r_: redo _f_: font smaller
|
||
_h_: left _s_: h-split _e_: balanced _R_: redo+ _0_: toggle neotree
|
||
_l_: right _v_: v-split _o_: choose by number (also 1-9)
|
||
"
|
||
("w" ace-window)
|
||
("c" other-window)
|
||
("=" balance-windows)
|
||
("m" delete-other-windows)
|
||
("d" delete-window)
|
||
("D" ace-delete-window)
|
||
|
||
("z" ace-swap-window)
|
||
("u" winner-undo)
|
||
("U" winner-undo :color pink)
|
||
("C-r" winner-redo)
|
||
("r" winner-redo)
|
||
("R" winner-redo :color pink)
|
||
|
||
("n" evil-window-new)
|
||
("j" evil-window-up)
|
||
("k" evil-window-down)
|
||
("h" evil-window-left)
|
||
("l" evil-window-right)
|
||
("o" other-window)
|
||
|
||
("s" evil-window-split)
|
||
("v" evil-window-vsplit)
|
||
|
||
("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)
|
||
("e" balance-windows)
|
||
|
||
("o" winum-select-window-by-number)
|
||
("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)
|
||
("0" neotree-toggle)
|
||
|
||
;; Extra bindings:
|
||
("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)
|
||
("q" nil :color blue)))
|
||
|
||
(ha-leader "w" '("windows" . hydra-window-resize/body))
|
||
#+end_src
|
||
*** 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 [[https://github.com/BurntSushi/ripgrep][ripgrep]], which are really fast.
|
||
**** ripgrep
|
||
Install the [[https://github.com/dajva/rg.el][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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(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"))
|
||
|
||
;; Old habits die hard ...
|
||
(define-key global-map [remap xref-find-references] 'rg-dwim)
|
||
|
||
(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) ; or projectile-ripgrep
|
||
"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))
|
||
|
||
(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)))
|
||
#+end_src
|
||
Note we bind the key ~M-R~ to the [[help:rg-menu][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:
|
||
#+begin_src emacs-lisp
|
||
(use-package rg
|
||
:general (:states 'normal "gr" 'rg-dwim))
|
||
#+end_src
|
||
**** wgrep
|
||
The [[https://github.com/mhayashi1120/Emacs-wgrep][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:
|
||
#+begin_src emacs-lisp
|
||
(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)))
|
||
#+end_src
|
||
*** Text Operations
|
||
Stealing much of this from Spacemacs.
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
|
||
Unfilling a paragraph joins all the lines in a paragraph into a single line. Taken [[http://www.emacswiki.org/UnfillParagraph][from here]] … I use this all the time:
|
||
#+begin_src emacs-lisp
|
||
(defun unfill-paragraph ()
|
||
"Convert a multi-line paragraph into a single line of text."
|
||
(interactive)
|
||
(let ((fill-column (point-max)))
|
||
(fill-paragraph nil)))
|
||
#+end_src
|
||
*** 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:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"h" '(:ignore t :which-key "help")
|
||
"h a" '("apropos" . apropos-command)
|
||
"h c" '("elisp cheatsheet" . shortdoc-display-group)
|
||
"h e" '("errors" . view-echo-area-messages)
|
||
"h E" '("emacs-lisp" . (lambda () (interactive) (info "elisp")))
|
||
"h f" '("function" . describe-function)
|
||
"h F" '("font" . describe-font)
|
||
"h =" '("face" . describe-face)
|
||
"h k" '("key binding" . describe-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" . describe-variable)
|
||
"h i" '("info" . info)
|
||
"h I" '("info manual" . info-display-manual)
|
||
"h j" '("info jump" . info-apropos))
|
||
#+end_src
|
||
|
||
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:
|
||
#+begin_src emacs-lisp
|
||
(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)) ; Old habit die hard
|
||
#+end_src
|
||
*** Consult
|
||
The [[https://github.com/minad/consult][consult project]] aims to use libraries like [[*Vertico][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).
|
||
#+begin_src emacs-lisp
|
||
(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)
|
||
|
||
:init
|
||
;; Use Consult to select xref locations with preview
|
||
(setq xref-show-xrefs-function #'consult-xref
|
||
xref-show-definitions-function #'consult-xref)
|
||
|
||
(ha-leader
|
||
"RET" '("bookmark" . consult-bookmark)
|
||
"o i" '("imenu" . consult-imenu)
|
||
"x y" '("preview yank" . consult-yank-pop))
|
||
|
||
:bind ("s-v" . consult-yank-pop)
|
||
|
||
:general
|
||
(:states 'normal
|
||
"gp" 'consult-yank-pop
|
||
"gs" 'consult-line))
|
||
#+end_src
|
||
*** Consult for Projects
|
||
One of the reasons that Consult hasn’t been too important to me, is that I often narrow my searching based on projectile. The [[https://gitlab.com/OlMon/consult-projectile][consult-projectile]] can help with this.
|
||
#+begin_src emacs-lisp
|
||
(use-package consult-projectile
|
||
:after consult general
|
||
:straight (:host gitlab :repo "OlMon/consult-projectile" :branch "master")
|
||
:config
|
||
(ha-leader
|
||
"p ." '("switch to..." . consult-projectile)
|
||
"b b" '("switch buffer" . consult-projectile-switch-to-buffer)
|
||
"p p" '("switch project" . consult-projectile-switch-project)
|
||
"p f" '("find file" . consult-projectile-find-file)
|
||
"p r" '("find recent file" . consult-projectile-recentf)))
|
||
#+end_src
|
||
The advantage of [[help:persp-switch-to-buffer][persp-switch-to-buffer]] over =consult-projectile-switch-to-buffer= is that is shows non-file buffers.
|
||
*** Embark
|
||
The [[https://github.com/oantolin/embark/][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?
|
||
#+begin_src emacs-lisp
|
||
(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)))
|
||
#+end_src
|
||
|
||
According to [[https://elpa.gnu.org/packages/embark-consult.html#orgc76b5de][this essay]], Embark cooperates well with the [[https://github.com/minad/marginalia][Marginalia]] and [[https://github.com/minad/consult][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:
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
|
||
According to the [[https://elpa.gnu.org/packages/embark-consult.html][Embark-Consult page]]:
|
||
#+begin_quote
|
||
Users of the popular [[https://github.com/justbur/emacs-which-key][which-key]] package may prefer to use the =embark-which-key-indicator= from the [[https://github.com/oantolin/embark/wiki/Additional-Configuration#use-which-key-like-a-key-menu-prompt][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=.
|
||
#+end_quote
|
||
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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(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)
|
||
#+end_src
|
||
** Evil Extensions
|
||
*** Evil Exchange
|
||
I often use the Emacs commands, ~M-t~ and whatnot to exchange words and whatnot, but this requires a drop out of normal state mode. The [[https://github.com/Dewdrops/evil-exchange][evil-exchange]] project attempts to do something similar, but in a VI-way, and the /objects/ do not need to be adjacent.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-exchange
|
||
:init
|
||
(setq evil-exchange-key (kbd "gx")
|
||
evil-exchange-cancel-key (kbd "gX"))
|
||
|
||
:general (:states 'normal
|
||
"g x" 'evil-exchange
|
||
"g X" 'evil-exchange-cancel
|
||
|
||
;; What about a "normal mode" binding to regular emacs transpose?
|
||
"z x" 'transpose-words
|
||
"z X" 'transpose-sexps
|
||
"z T" 'transpose-lines)
|
||
|
||
:config (evil-exchange-install))
|
||
#+end_src
|
||
|
||
Let’s explain how this works as the documentation assumes some previous knowledge. If you had a sentence:
|
||
|
||
The ball was blue and the boy was red.
|
||
|
||
Move the point to the word, /red/, and type ~g x i w~ (anywhere since we are using the inner text object). Next, jump to the word /blue/, and type the sequence, ~g x i w~ again, and you have:
|
||
|
||
The ball was blue and the boy was red.
|
||
|
||
The idea is that you can exchange anything. The ~g x~ marks something (like what we would normally do in /visual mode/), and then by marking something else with a ~g x~ sequence, it swaps them.
|
||
|
||
Notice that you can swap:
|
||
- ~gx i w~ :: words, ~W~ words with dashes, or ~o~ for programming symbols (like variables)
|
||
- ~gx i s~ :: sentences
|
||
- ~gx i p~ :: paragraphs
|
||
- ~gx i g~ :: programming s-expressions between parens, braces, etc.
|
||
- ~gx i l~ :: lines, with the [[Evil Text Object Line][line-based text object]] project installed
|
||
*** Evil Commentary
|
||
The [[https://github.com/linktohack/evil-commentary][evil-commentary]] is a VI-like way of commenting text. Yeah, I typically type ~M-;~ to call Emacs’ originally functionality, but in this case, ~g c c~ comments out a line(s), and ~g c~ takes text objects and whatnot. For instance, ~g c $~ comments to the end of the line.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-commentary
|
||
:config (evil-commentary-mode))
|
||
#+end_src
|
||
*** Evil Collection
|
||
Dropping into Emacs state is better than pure Evil state for applications, however, [[https://github.com/emacs-evil/evil-collection][the evil-collection package]] creates a hybrid between the two, that I like.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-collection
|
||
:after evil
|
||
:config
|
||
(evil-collection-init))
|
||
#+end_src
|
||
|
||
Do I want to specify the list of modes to change for =evil-collection-init=, e.g.
|
||
#+begin_src emacs-lisp :tangle no :eval no
|
||
'(eww magit dired notmuch term wdired)
|
||
#+end_src
|
||
*** Evil Owl
|
||
Not sure what is in a register? Have it show you when you hit ~”~ or ~@~ with [[https://github.com/mamapanda/evil-owl][evil-owl]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package posframe)
|
||
|
||
(use-package evil-owl
|
||
:after posframe
|
||
:config
|
||
(setq evil-owl-display-method 'posframe
|
||
evil-owl-extra-posframe-args '(:width 50 :height 20 :background-color "#444")
|
||
evil-owl-max-string-length 50)
|
||
(evil-owl-mode))
|
||
#+end_src
|
||
*** Evil Snipe
|
||
Doom introduced me to [[https://github.com/hlissner/evil-snipe][evil-snipe]], like =f= and =t=, but with two characters, and can, when configured, search more than the current line. My issue is that [[Evil Surround]] uses the same keybindings. Since surround doesn’t work in /normal/ and /visual/ states, we’ll bind snipe only for those:
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-snipe
|
||
:after evil
|
||
:init
|
||
(setq evil-snipe-scope 'visible)
|
||
|
||
:general
|
||
(:states '(normal visual)
|
||
"s" 'evil-snipe-s
|
||
"S" 'evil-snipe-S)
|
||
:config
|
||
(evil-snipe-mode +1))
|
||
#+end_src
|
||
It highlights all potential matches, use ~;~ to skip to the next match, and ~,~ to jump back.
|
||
*** Evil Surround
|
||
I like both [[https://github.com/emacs-evil/evil-surround][evil-surround]] and Henrik's [[https://github.com/hlissner/evil-snipe][evil-snipe]], but they both start with ~s~, and conflict, and getting them to work together means I have to remember when does ~s~ call sniper and when it calls surround. As an original Emacs person, I am not bound by that key history, but I do need them consistent, so I’m choosing the ~s~ to be /surround/.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package evil-surround
|
||
:config
|
||
(defun evil-surround-elisp ()
|
||
(push '(?\` . ("`" . "'")) evil-surround-pairs-alist))
|
||
(defun evil-surround-org ()
|
||
(push '(?\" . ("“" . "”")) evil-surround-pairs-alist)
|
||
(push '(?\' . ("‘" . "’")) evil-surround-pairs-alist)
|
||
(push '(?b . ("*" . "*")) evil-surround-pairs-alist)
|
||
(push '(?* . ("*" . "*")) evil-surround-pairs-alist)
|
||
(push '(?i . ("/" . "/")) evil-surround-pairs-alist)
|
||
(push '(?/ . ("/" . "/")) evil-surround-pairs-alist)
|
||
(push '(?= . ("=" . "=")) evil-surround-pairs-alist)
|
||
(push '(?~ . ("~" . "~")) evil-surround-pairs-alist))
|
||
|
||
(global-evil-surround-mode 1)
|
||
|
||
:hook
|
||
(org-mode . evil-surround-org)
|
||
(emacs-lisp-mode . evil-surround-elisp))
|
||
#+end_src
|
||
Notes:
|
||
- ~cs'"~ :: to convert surrounding single quote string to double quotes.
|
||
- ~ds"~ :: to delete the surrounding double quotes.
|
||
- ~yse"~ :: puts single quotes around the next word.
|
||
- ~ysiw'~ :: puts single quotes around the word, no matter the points position.
|
||
- ~yS$<p>~ :: surrouds the line with HTML =<p>= tag (with extra carriage returns).
|
||
- ~ysiw'~ :: puts single quotes around the word, no matter the points position.
|
||
- ~(~ :: puts spaces /inside/ the surrounding parens, but ~)~ doesn't. Same with ~[~ and ~]~.
|
||
** Additional Global Packages
|
||
*** Visual Replace with Visual Regular Expressions
|
||
I appreciated the [[https://github.com/benma/visual-regexp.el][visual-regexp package]] to see what you want to change /before/ executing the replace.
|
||
#+begin_src emacs-lisp
|
||
(use-package visual-regexp
|
||
:bind (("C-c r" . vr/replace)
|
||
("C-c q" . vr/query-replace))
|
||
:general (:states 'normal "gR" '("replace" . vr/replace))
|
||
:config (ha-leader
|
||
"r" '("replace" . vr/replace)
|
||
"R" '("query replace" . vr/query-replace)))
|
||
#+end_src
|
||
|
||
*** Jump with Avy
|
||
While I grew up on =Control S=, I am liking the /mental model/ associated with the [[https://github.com/abo-abo/avy][avy project]] that allows a /jump/ among matches across all visible windows. I use the ~F18~ key on my keyboard that should be easy to use, but ~g o~ seems obvious.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package avy
|
||
:init
|
||
(setq avy-all-windows t
|
||
avy-single-candidate-jump t
|
||
avy-orders-alist
|
||
'((avy-goto-char . avy-order-closest)
|
||
(avy-goto-word-0 . avy-order-closest)))
|
||
|
||
:config (ha-leader "j" '("jump" . avy-goto-char-timer))
|
||
|
||
:general
|
||
(:states 'normal "go" 'avy-goto-char-timer)
|
||
|
||
:bind ("<f18>" . avy-goto-char-timer))
|
||
#+end_src
|
||
*Note:* The links should be shorter near the point as opposed to starting from the top of the window.
|
||
|
||
If you hit the following keys /before/ you select a target, you get a special action:
|
||
- ~n~ :: copies the matching target word
|
||
*** Link Hint, the Link Jumper
|
||
I originally appreciated [[https://github.com/abo-abo/ace-link][ace-link]] to work with hyperlinks on Org, EWW and Info pages, but the [[https://github.com/noctuid/link-hint.el][link-hint]] project works with more types of links:
|
||
#+begin_src emacs-lisp
|
||
(use-package link-hint
|
||
:bind
|
||
("s-o" . link-hint-open-link)
|
||
("C-c l o" . link-hint-open-link)
|
||
("C-c l c" . link-hint-copy-link)
|
||
:general
|
||
(:states 'normal
|
||
"gl" 'link-hint-open-link
|
||
"gL" 'link-hint-copy-link)
|
||
(:states 'normal :keymaps 'eww-mode-map
|
||
"o" 'link-hint-open-link)
|
||
(:states 'normal :keymaps 'Info-mode-map
|
||
"o" 'link-hint-open-link))
|
||
#+end_src
|
||
|
||
*** Expand Region
|
||
Magnar Sveen's [[https://github.com/magnars/expand-region.el][expand-region]] project allows me to hit ~v~ in =visual= mode, and have the selection grow by syntactical units.
|
||
#+begin_src emacs-lisp
|
||
(use-package expand-region
|
||
:bind ("C-=" . er/expand-region)
|
||
|
||
:general
|
||
;; Use escape to get out of visual mode, but hitting v again expands the selection.
|
||
(:states 'visual "v" 'er/expand-region))
|
||
#+end_src
|
||
* Working Layout
|
||
While editing any file on disk is easy enough, I like the mental context switch associated with a full-screen window frame showing all the buffers of a /project task/ (often a direct link to a repository project, but not always).
|
||
** Projects
|
||
While I don't /need/ all the features that [[https://github.com/bbatsov/projectile][projectile]] provides, it has all the features I do need, and is easy enough to install. I am referring to the fact that I /could/ use the built-in =project.el= system (see [[https://cestlaz.github.io/post/using-emacs-79-project/][this essay]] for details on what I mean as an alternative).
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package projectile
|
||
:custom
|
||
(projectile-sort-order 'recentf)
|
||
(projectile-project-root-functions '(projectile-root-bottom-up))
|
||
|
||
:config
|
||
(ha-leader
|
||
"p" '(:ignore t :which-key "projects")
|
||
"p W" '("initialize workspace" . ha-workspace-initialize)
|
||
"p n" '("new project space" . ha-project-persp)
|
||
"p !" '("run cmd in project root" . projectile-run-shell-command-in-root)
|
||
"p &" '("async cmd in project root" . projectile-run-async-shell-command-in-root)
|
||
"p a" '("add new project" . projectile-add-known-project)
|
||
"p b" '("switch to project buffer" . projectile-switch-to-buffer)
|
||
"p c" '("compile in project" . projectile-compile-project)
|
||
"p C" '("repeat last command" . projectile-repeat-last-command)
|
||
"p d" '("remove known project" . projectile-remove-known-project)
|
||
"p e" '("edit project .dir-locals" . projectile-edit-dir-locals)
|
||
"p f" '("find file in project" . projectile-find-file)
|
||
"p g" '("configure project" . projectile-configure-project)
|
||
"p i" '("invalidate project cache" . projectile-invalidate-cache)
|
||
"p k" '("kill project buffers" . projectile-kill-buffers)
|
||
"p o" '("find other file" . projectile-find-other-file)
|
||
"p p" '("switch project" . projectile-switch-project)
|
||
"p r" '("find recent project files" . projectile-recentf)
|
||
"p R" '("run project" . projectile-run-project)
|
||
"p S" '("save project files" . projectile-save-project-buffers)
|
||
"p T" '("test project" . projectile-test-project)))
|
||
#+end_src
|
||
** Workspaces
|
||
A /workspace/ (at least to me) requires a quick jump to a collection of buffer windows organized around a project or task. For this, I'm basing my work on the [[https://github.com/nex3/perspective-el][perspective.el]] project.
|
||
|
||
I build a Hydra to dynamically list the current projects as well as select the project.
|
||
To do this, we need a way to generate a string of the perspectives in alphabetical order:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha--persp-label (num names)
|
||
"Return string of numbered elements. NUM is the starting
|
||
number and NAMES is a list of strings."
|
||
(when names
|
||
(concat
|
||
(format " %d: %s%s" ; Shame that the following doesn't work:
|
||
num ; (propertize (number-to-string num) :foreground "#00a0")
|
||
(car names) ; Nor does surrounding the number with underbars.
|
||
(if (equal (car names) (projectile-project-name)) "*" ""))
|
||
(ha--persp-label (1+ num) (cdr names)))))
|
||
|
||
(defun ha-persp-labels ()
|
||
"Return a string of numbered elements from a list of names."
|
||
(ha--persp-label 1 (sort (hash-table-keys (perspectives-hash)) 's-less?)))
|
||
#+end_src
|
||
|
||
Build the hydra as well as configure the =perspective= project.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package perspective
|
||
:custom
|
||
(persp-modestring-short t)
|
||
(persp-show-modestring t)
|
||
|
||
:config
|
||
(persp-mode +1)
|
||
|
||
(defhydra hydra-workspace-leader (:color blue :hint nil) "
|
||
Workspaces- %s(ha-persp-labels)
|
||
_n_: new project _r_: rename _a_: add buffer _l_: load worksp
|
||
_]_: next worksp _d_: delete _b_: goto buffer _s_: save worksp
|
||
_[_: previous _W_: init all _k_: remove buffer _`_: to last worksp "
|
||
("TAB" persp-switch-quick)
|
||
("RET" persp-switch)
|
||
("`" persp-switch-last)
|
||
("1" (persp-switch-by-number 1))
|
||
("2" (persp-switch-by-number 2))
|
||
("3" (persp-switch-by-number 3))
|
||
("4" (persp-switch-by-number 4))
|
||
("5" (persp-switch-by-number 5))
|
||
("6" (persp-switch-by-number 6))
|
||
("7" (persp-switch-by-number 7))
|
||
("8" (persp-switch-by-number 8))
|
||
("9" (persp-switch-by-number 9))
|
||
("0" (persp-switch-by-number 0))
|
||
("n" ha-project-persp)
|
||
("N" ha-new-persp)
|
||
("]" persp-next :color pink)
|
||
("[" persp-prev :color pink)
|
||
("r" persp-rename)
|
||
("d" persp-kill)
|
||
("W" ha-workspace-initialize)
|
||
("a" persp-add-buffer)
|
||
("b" persp-switch-to-buffer)
|
||
("k" persp-remove-buffer)
|
||
("K" persp-kill-buffer)
|
||
("s" persp-state-save)
|
||
("l" persp-state-load)
|
||
("w" ha-switch-to-special) ; The most special perspective
|
||
("q" nil)
|
||
("C-g" nil))
|
||
|
||
:bind ("C-<tab>" . hydra-workspace-leader/body))
|
||
#+end_src
|
||
|
||
I have no idea why this binding doesn’t work /within/ the =use-package= declaration, but oh well…
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "TAB" '("workspaces" . hydra-workspace-leader/body))
|
||
#+end_src
|
||
|
||
The /special/ perspective is a nice shortcut to the one I use the most:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-switch-to-special ()
|
||
"Change to the projects perspective."
|
||
(interactive)
|
||
(persp-switch "projects"))
|
||
#+end_src
|
||
*** Predefined Workspaces
|
||
Let's describe a list of startup project workspaces. This way, I don't need the clutter of the recent state, but also get back to a state of mental normality.
|
||
Granted, this list is essentially a list of projects that I'm currently developing, so I expect this to change often.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar ha-workspace-projects-personal nil "List of default projects with a name.")
|
||
|
||
(add-to-list 'ha-workspace-projects-personal
|
||
'("projects" "~/projects" ("breathe.org" "tasks.org")))
|
||
(add-to-list 'ha-workspace-projects-personal
|
||
'("personal" "~/personal" ("general.org")))
|
||
(add-to-list 'ha-workspace-projects-personal
|
||
'("technical" "~/technical" ("ansible.org")))
|
||
(add-to-list 'ha-workspace-projects-personal
|
||
'("hamacs" "~/other/hamacs" ("README.org" "ha-config.org")))
|
||
#+end_src
|
||
|
||
Given a list of information about project-workspaces, can we create them all?
|
||
#+begin_src emacs-lisp
|
||
(defun ha-persp-exists? (name)
|
||
"Return non-nill if a perspective of NAME exists."
|
||
(when (fboundp 'perspectives-hash)
|
||
(seq-contains (hash-table-keys (perspectives-hash)) name)))
|
||
|
||
(defun ha-workspace-initialize (&optional projects)
|
||
"Precreate workspace projects from a PROJECTS list.
|
||
Each entry in the list is a list containing:
|
||
- name (as a string)
|
||
- project root directory
|
||
- a optional list of files to display"
|
||
(interactive)
|
||
(unless projects
|
||
(setq projects ha-workspace-projects-personal))
|
||
|
||
(dolist (project projects)
|
||
(-let (((name root files) project))
|
||
(unless (ha-persp-exists? name)
|
||
(message "Creating workspace: %s (from %s)" name root)
|
||
(ha-project-persp root name files)))))
|
||
#+end_src
|
||
Often, but not always, I want a perspective based on an actual Git repository, e.g. a project. Projectile keeps state of a "project" based on the current file loaded, so we /combine/ the two projects by first choosing from a list of /known projects/ and then creating a perspective based on the name. To pin the perspective to a project, we load a file from it, e.g. Like a README or something.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-project-persp (project &optional name files)
|
||
"Create a new perspective, and then switch to the PROJECT using projectile.
|
||
If NAME is not given, then figure it out based on the name of the
|
||
PROJECT. If FILES aren't specified, then see if there is a
|
||
README. Otherwise, pull up Dired."
|
||
(interactive (list (projectile-completing-read "Project: " projectile-known-projects)))
|
||
(when (f-directory-p project)
|
||
(unless name
|
||
(setq name (f-filename project)))
|
||
(persp-switch name)
|
||
|
||
;; Unclear if the following is actually necessary.
|
||
(ignore-errors
|
||
(projectile-add-known-project root)
|
||
(let ((projectile-switch-project-action nil))
|
||
(projectile-switch-project-by-name root)))
|
||
|
||
;; To pin a project in projectile to the perspective, we need to load a file
|
||
;; from that project. The README will do, or at least, the dired of it.
|
||
(let ((readme-org (f-join project "README.org"))
|
||
(readme-md (f-join project "README.md")))
|
||
(cond
|
||
(files (ha--project-show-files project files))
|
||
((f-exists? readme-org) (find-file readme-org))
|
||
((f-exists? readme-md) (find-file readme-md))
|
||
(t (dired project))))))
|
||
#+end_src
|
||
|
||
When starting a new perspective, and I specify more than one file, this function splits the window horizontally for each file.
|
||
#+begin_src emacs-lisp
|
||
(defun ha--project-show-files (root files)
|
||
"Display a list of FILES in a project ROOT directory.
|
||
Each file gets its own window (so don't make the list of files
|
||
long)."
|
||
(message "Loading files from %s ... %s" root files)
|
||
(let* ((file (car files))
|
||
(more (cdr files))
|
||
(filename (format "%s/%s" root file)))
|
||
(find-file filename)
|
||
(when more
|
||
(split-window-horizontally)
|
||
(ha--project-show-files root more))))
|
||
#+end_src
|
||
|
||
The =persp-switch= allows me to select or create a new project, but what if we insisted on a new workspace?
|
||
#+begin_src emacs-lisp
|
||
(defun ha-new-persp (name)
|
||
(interactive "sNew Workspace: ")
|
||
(persp-switch name)
|
||
(cond
|
||
((s-ends-with? "mail" name) (notmuch))
|
||
((s-starts-with? "twit" name) (twit))))
|
||
#+end_src
|
||
Once we create the new perspective workspace, if it matches a particular name, I pretty much know what function I would like to call.
|
||
* Applications
|
||
Can we call these /applications/?
|
||
** Magit
|
||
Can not live without [[https://magit.vc/][Magit]], a Git porcelain for Emacs. I stole the bulk of this work from Doom Emacs.
|
||
#+begin_src emacs-lisp
|
||
(use-package magit
|
||
:config
|
||
;; The following code re-instates my General Leader key in Magit.
|
||
(general-unbind magit-mode-map "SPC")
|
||
|
||
(ha-leader
|
||
"g" '(:ignore t :which-key "git")
|
||
"g /" '("Magit dispatch" . magit-dispatch)
|
||
"g ." '("Magit file dispatch" . magit-file-dispatch)
|
||
"g b" '("Magit switch branch" . magit-branch-checkout)
|
||
|
||
"g g" '("Magit status" . magit-status)
|
||
"g s" '("Magit status here" . magit-status-here)
|
||
"g D" '("Magit file delete" . magit-file-delete)
|
||
"g B" '("Magit blame" . magit-blame-addition)
|
||
"g C" '("Magit clone" . magit-clone)
|
||
"g F" '("Magit fetch" . magit-fetch)
|
||
"g L" '("Magit buffer log" . magit-log-buffer-file)
|
||
"g r" '("Revert file" . magit-file-checkout)
|
||
"g R" '("Revert file" . vc-revert)
|
||
"g S" '("Git stage file" . magit-stage-file)
|
||
"g U" '("Git unstage file" . magit-unstage-file)
|
||
|
||
"g f" '(:ignore t :which-key "find")
|
||
"g f f" '("Find file" . magit-find-file)
|
||
"g f g" '("Find gitconfig file" . magit-find-git-config-file)
|
||
"g f c" '("Find commit" . magit-show-commit)
|
||
|
||
"g l" '(:ignore t :which-key "list")
|
||
"g l r" '("List repositories" . magit-list-repositories)
|
||
"g l s" '("List submodules" . magit-list-submodules)
|
||
|
||
"g o" '(:ignore t :which-key "open")
|
||
|
||
"g c" '(:ignore t :which-key "create")
|
||
"g c R" '("Initialize repo" . magit-init)
|
||
"g c C" '("Clone repo" . magit-clone)
|
||
"g c c" '("Commit" . magit-commit-create)
|
||
"g c f" '("Fixup" . magit-commit-fixup)
|
||
"g c b" '("Branch" . magit-branch-and-checkout)))
|
||
#+end_src
|
||
*** Git Delta
|
||
The [[https://scripter.co/using-git-delta-with-magit][magit-delta]] project uses [[https://github.com/dandavison/delta][git-delta]] for colorized diffs.
|
||
#+begin_src emacs-lisp
|
||
(use-package magit-delta
|
||
:ensure t
|
||
:hook (magit-mode . magit-delta-mode))
|
||
#+end_src
|
||
I also need to append the following to my [[file:~/.gitconfig][~/.gitconfig]] file:
|
||
#+begin_src conf
|
||
[delta]
|
||
minus-style = normal "#8f0001"
|
||
minus-non-emph-style = normal "#8f0001"
|
||
minus-emph-style = normal bold "#d01011"
|
||
minus-empty-line-marker-style = normal "#8f0001"
|
||
zero-style = syntax
|
||
plus-style = syntax "#006800"
|
||
plus-non-emph-style = syntax "#006800"
|
||
plus-emph-style = syntax "#009000"
|
||
plus-empty-line-marker-style = normal "#006800"
|
||
#+end_src
|
||
*** Git with Difftastic
|
||
I’m stealing the code for this section from [[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][this essay]] by Tassilo Horn, and in fact, I’m going to lift a lot of his explanation too, as I may need to remind myself how this works. The idea is based on using Wilfred’s excellent [[https://github.com/Wilfred/difftastic][difftastic]] tool to do a structural/syntax comparison of code changes in git. To begin, install the binary:
|
||
#+begin_src sh
|
||
brew install difftastic # and the equivalent on Linux
|
||
#+end_src
|
||
Next, we can do this, to use this as a diff tool for everything.
|
||
#+begin_src emacs-lisp
|
||
(setenv "GIT_EXTERNAL_DIFF" "difft")
|
||
#+end_src
|
||
But perhaps integrating it into Magit and selectively calling it (as it is slow). Tassilo suggests making the call to =difft= optional by first creating a helper function to set the =GIT_EXTERNAL_DIFF= to =difft=:
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit--with-difftastic (buffer command)
|
||
"Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
|
||
(let ((process-environment
|
||
(cons (concat "GIT_EXTERNAL_DIFF=difft --width="
|
||
(number-to-string (frame-width)))
|
||
process-environment)))
|
||
;; Clear the result buffer (we might regenerate a diff, e.g., for
|
||
;; the current changes in our working directory).
|
||
(with-current-buffer buffer
|
||
(setq buffer-read-only nil)
|
||
(erase-buffer))
|
||
;; Now spawn a process calling the git COMMAND.
|
||
(make-process
|
||
:name (buffer-name buffer)
|
||
:buffer buffer
|
||
:command command
|
||
;; Don't query for running processes when emacs is quit.
|
||
:noquery t
|
||
;; Show the result buffer once the process has finished.
|
||
:sentinel (lambda (proc event)
|
||
(when (eq (process-status proc) 'exit)
|
||
(with-current-buffer (process-buffer proc)
|
||
(goto-char (point-min))
|
||
(ansi-color-apply-on-region (point-min) (point-max))
|
||
(setq buffer-read-only t)
|
||
(view-mode)
|
||
(end-of-line)
|
||
;; difftastic diffs are usually 2-column side-by-side,
|
||
;; so ensure our window is wide enough.
|
||
(let ((width (current-column)))
|
||
(while (zerop (forward-line 1))
|
||
(end-of-line)
|
||
(setq width (max (current-column) width)))
|
||
;; Add column size of fringes
|
||
(setq width (+ width
|
||
(fringe-columns 'left)
|
||
(fringe-columns 'right)))
|
||
(goto-char (point-min))
|
||
(pop-to-buffer
|
||
(current-buffer)
|
||
`(;; If the buffer is that wide that splitting the frame in
|
||
;; two side-by-side windows would result in less than
|
||
;; 80 columns left, ensure it's shown at the bottom.
|
||
,(when (> 80 (- (frame-width) width))
|
||
#'display-buffer-at-bottom)
|
||
(window-width . ,(min width (frame-width))))))))))))
|
||
#+end_src
|
||
The crucial parts of this helper function are that we "wash" the result using =ansi-color-apply-on-region= so that the function can transform the difftastic highlighting using shell escape codes to Emacs faces. Also, note the need to possibly change the width, as difftastic makes a side-by-side comparison.
|
||
|
||
The functions below depend on [[help:magit-thing-at-point][magit-thing-at-point]], and this depends on the [[https://sr.ht/~pkal/compat/][compat]] library, so let’s grab that stuff:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package compat
|
||
:straight (:host github :repo "emacs-straight/compat"))
|
||
|
||
(use-package magit-section
|
||
:commands magit-thing-at-point)
|
||
#+end_src
|
||
Next, let's define our first command basically doing a =git show= for some revision which defaults to the commit or branch at point or queries the user if there's none.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-show-with-difftastic (rev)
|
||
"Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use if given the REV variable:
|
||
(when (boundp 'rev) rev)
|
||
;; If not invoked with prefix arg, try to guess the REV from
|
||
;; point's position.
|
||
(and (not current-prefix-arg)
|
||
(or (magit-thing-at-point 'git-revision t)
|
||
(magit-branch-or-commit-at-point)))
|
||
;; Otherwise, query the user.
|
||
(magit-read-branch-or-commit "Revision"))))
|
||
(if (not rev)
|
||
(error "No revision specified")
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create (concat "*git show difftastic " rev "*"))
|
||
(list "git" "--no-pager" "show" "--ext-diff" rev))))
|
||
#+end_src
|
||
And here the second command which basically does a =git diff=. It tries to guess what one wants to diff, e.g., when point is on the Staged changes section in a magit buffer, it will run =git diff --cached= to show a diff of all staged changes. If it can not guess the context, it'll query the user for a range or commit for diffing.
|
||
#+begin_src emacs-lisp
|
||
(defun th/magit-diff-with-difftastic (arg)
|
||
"Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
|
||
(interactive
|
||
(list (or
|
||
;; Use If RANGE is given, just use it.
|
||
(when (boundp 'range) range)
|
||
;; If prefix arg is given, query the user.
|
||
(and current-prefix-arg
|
||
(magit-diff-read-range-or-commit "Range"))
|
||
;; Otherwise, auto-guess based on position of point, e.g., based on
|
||
;; if we are in the Staged or Unstaged section.
|
||
(pcase (magit-diff--dwim)
|
||
('unmerged (error "unmerged is not yet implemented"))
|
||
('unstaged nil)
|
||
('staged "--cached")
|
||
(`(stash . ,value) (error "stash is not yet implemented"))
|
||
(`(commit . ,value) (format "%s^..%s" value value))
|
||
((and range (pred stringp)) range)
|
||
(_ (magit-diff-read-range-or-commit "Range/Commit"))))))
|
||
(let ((name (concat "*git diff difftastic"
|
||
(if arg (concat " " arg) "")
|
||
"*")))
|
||
(th/magit--with-difftastic
|
||
(get-buffer-create name)
|
||
`("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
|
||
#+end_src
|
||
|
||
What's left is integrating the new show and diff commands in Magit. For that purpose, Tasillo created a new transient prefix for all personal commands. Intriguing, but I have a hack that I can use on a leader:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-difftastic-here ()
|
||
(interactive)
|
||
(call-interactively
|
||
(if (eq major-mode 'magit-log-mode)
|
||
'th/magit-show-with-difftastic
|
||
'th/magit-diff-with-difftastic)))
|
||
|
||
(ha-leader "g d" '("difftastic" . ha-difftastic-here))
|
||
#+end_src
|
||
*** Time Machine
|
||
The [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] project visually shows how a code file changes with each iteration:
|
||
#+begin_src emacs-lisp
|
||
(use-package git-timemachine
|
||
:config
|
||
(ha-leader "g t" '("git timemachine" . git-timemachine)))
|
||
#+end_src
|
||
*** Gist
|
||
Using the [[https://github.com/emacsmirror/gist][gist package]] to write code snippets on [[https://gist.github.com/][Github]] seems like it can be useful, but I'm not sure how often.
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package gist
|
||
:config
|
||
(ha-leader
|
||
"g G" '(:ignore t :which-key "gists")
|
||
"g l g" '("gists" . gist-list)
|
||
"g G l" '("list" . gist-list) ; Lists your gists in a new buffer.
|
||
"g G r" '("region" . gist-region) ; Copies Gist URL into the kill ring.
|
||
"g G R" '("private region" . gist-region-private) ; Explicitly create a private gist.
|
||
"g G b" '("buffer" . gist-buffer) ; Copies Gist URL into the kill ring.
|
||
"g G B" '("private buffer" . gist-buffer-private) ; Explicitly create a private gist.
|
||
"g c g" '("gist" . gist-region-or-buffer) ; Post either the current region, or buffer
|
||
"g c G" '("private gist" . gist-region-or-buffer-private))) ; create private gist from region or buffer
|
||
#+end_src
|
||
|
||
The gist project depends on the [[https://github.com/sigma/gh.el][gh library]]. There seems to be a problem with it.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package gh
|
||
:straight (:host github :repo "sigma/gh.el"))
|
||
#+end_src
|
||
|
||
*** Forge
|
||
Let's extend Magit with [[https://github.com/magit/forge][Magit Forge]] for working with Github and Gitlab:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package forge
|
||
:after magit
|
||
:config
|
||
(ha-leader
|
||
"g '" '("Forge dispatch" . forge-dispatch)
|
||
"g f i" '("Find issue" . forge-visit-issue)
|
||
"g f p" '("Find pull request" . forge-visit-pullreq)
|
||
|
||
"g l i" '("List issues" . forge-list-issues)
|
||
"g l p" '("List pull requests" . forge-list-pullreqs)
|
||
"g l n" '("List notifications" . forge-list-notifications)
|
||
|
||
"g o r" '("Browse remote" . forge-browse-remote)
|
||
"g o c" '("Browse commit" . forge-browse-commit)
|
||
"g o i" '("Browse an issue" . forge-browse-issue)
|
||
"g o p" '("Browse a pull request" . forge-browse-pullreq)
|
||
"g o i" '("Browse issues" . forge-browse-issues)
|
||
"g o P" '("Browse pull requests" . forge-browse-pullreqs)
|
||
|
||
"g c i" '("Issue" . forge-create-issue)
|
||
"g c p" '("Pull request" . forge-create-pullreq)))
|
||
#+end_src
|
||
|
||
Every /so often/, pop over to the following URLs and generate a new token where the *Note* is =forge=, and then copy that into the [[file:~/.authinfo.gpg][~/.authinfo.gpg]]:
|
||
- [[https://gitlab.com/-/profile/personal_access_tokens][Gitlab]]
|
||
- [[https://github.com/settings/tokens][Github]]
|
||
and make sure this works:
|
||
|
||
#+begin_src emacs-lisp :tangle no :results replace
|
||
(ghub-request "GET" "/user" nil
|
||
:forge 'github
|
||
:host "api.github.com"
|
||
:username "howardabrams"
|
||
:auth 'forge)
|
||
#+end_src
|
||
*** Pushing is Bad
|
||
Pushing directly to the upstream branch is /bad form/, as one should create a pull request, etc. To prevent an accidental push, we /double-check/ first:
|
||
|
||
#+begin_src emacs-lisp
|
||
(define-advice magit-push-current-to-upstream (:before (args) query-yes-or-no)
|
||
"Prompt for confirmation before permitting a push to upstream."
|
||
(when-let ((branch (magit-get-current-branch)))
|
||
(unless (yes-or-no-p (format "Push %s branch upstream to %s? "
|
||
branch
|
||
(or (magit-get-upstream-branch branch)
|
||
(magit-get "branch" branch "remote"))))
|
||
(user-error "Push to upstream aborted by user"))))
|
||
#+end_src
|
||
** Web Browsing
|
||
*** EWW
|
||
Web pages look pretty good with EWW, but I'm having difficulty getting it to render a web search from DuckDuck.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package eww
|
||
:init
|
||
(setq browse-url-browser-function 'eww-browse-url
|
||
browse-url-secondary-browser-function 'browse-url-default-browser
|
||
eww-browse-url-new-window-is-tab nil
|
||
shr-use-colors nil
|
||
shr-use-fonts t ; I go back and forth on this one
|
||
;; shr-discard-aria-hidden t
|
||
shr-bullet "• "
|
||
shr-inhibit-images nil ; Gotta see the images?
|
||
;; shr-blocked-images '(svg)
|
||
;; shr-folding-mode nil
|
||
url-privacy-level '(email))
|
||
|
||
:config
|
||
(ha-leader "a b" '("eww browser" . eww))
|
||
|
||
:general
|
||
(:states 'normal :keymaps 'eww-mode-map
|
||
"B" 'eww-list-bookmarks
|
||
"Y" 'eww-copy-page-url
|
||
"H" 'eww-back-url
|
||
"L" 'eww-forward-url
|
||
"u" 'eww-top-url
|
||
"p" 'eww-previous-url
|
||
"n" 'eww-next-url
|
||
"q" 'bury-buffer)
|
||
(:states 'normal :keymaps 'eww-buffers-mode-map
|
||
"q" 'bury-buffer))
|
||
#+end_src
|
||
|
||
This function allows Imenu to offer HTML headings in EWW buffers, helpful for navigating long, technical documents.
|
||
#+begin_src emacs-lisp
|
||
(use-package eww
|
||
:config
|
||
(defun unpackaged/imenu-eww-headings ()
|
||
"Return alist of HTML headings in current EWW buffer for Imenu.
|
||
Suitable for `imenu-create-index-function'."
|
||
(let ((faces '(shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6 shr-heading)))
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
(goto-char (point-min))
|
||
(cl-loop for next-pos = (next-single-property-change (point) 'face)
|
||
while next-pos
|
||
do (goto-char next-pos)
|
||
for face = (get-text-property (point) 'face)
|
||
when (cl-typecase face
|
||
(list (cl-intersection face faces))
|
||
(symbol (member face faces)))
|
||
collect (cons (buffer-substring (point-at-bol) (point-at-eol)) (point))
|
||
and do (forward-line 1))))))
|
||
:hook (eww-mode .
|
||
(lambda ()
|
||
(setq-local imenu-create-index-function #'unpackaged/imenu-eww-headings))))
|
||
#+end_src
|
||
*** Get Pocket
|
||
The [[https://github.com/alphapapa/pocket-reader.el][pocket-reader]] project connects to the [[https://getpocket.com/en/][Get Pocket]] service.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pocket-reader
|
||
:init
|
||
(setq org-web-tools-pandoc-sleep-time 1)
|
||
:config
|
||
(ha-leader "o p" '("get pocket" . pocket-reader))
|
||
|
||
;; Instead of jumping into Emacs mode to get the `pocket-mode-map',
|
||
;; we add the keybindings to the normal mode that makes sense.
|
||
:general
|
||
(:states 'normal :keymaps 'pocket-reader-mode-map
|
||
"RET" 'pocket-reader-open-url
|
||
"TAB" 'pocket-reader-pop-to-url
|
||
|
||
"*" 'pocket-reader-toggle-favorite
|
||
"B" 'pocket-reader-open-in-external-browser
|
||
"D" 'pocket-reader-delete
|
||
"E" 'pocket-reader-excerpt-all
|
||
"F" 'pocket-reader-show-unread-favorites
|
||
"M" 'pocket-reader-mark-all
|
||
"R" 'pocket-reader-random-item
|
||
"S" 'tabulated-list-sort
|
||
"a" 'pocket-reader-toggle-archived
|
||
"c" 'pocket-reader-copy-url
|
||
"d" 'pocket-reader
|
||
"e" 'pocket-reader-excerpt
|
||
"f" 'pocket-reader-toggle-favorite
|
||
"l" 'pocket-reader-limit
|
||
"m" 'pocket-reader-toggle-mark
|
||
"o" 'pocket-reader-more
|
||
"q" 'quit-window
|
||
"s" 'pocket-reader-search
|
||
"u" 'pocket-reader-unmark-all
|
||
"t a" 'pocket-reader-add-tags
|
||
"t r" 'pocket-reader-remove-tags
|
||
"t s" 'pocket-reader-tag-search
|
||
"t t" 'pocket-reader-set-tags
|
||
|
||
"g s" 'pocket-reader-resort
|
||
"g r" 'pocket-reader-refresh))
|
||
#+end_src
|
||
|
||
Use these special keywords when searching:
|
||
|
||
- =:*=, =:favorite= Return favorited items.
|
||
- =:archive= Return archived items.
|
||
- =:unread= Return unread items (default).
|
||
- =:all= Return all items.
|
||
- =:COUNT= Return at most /COUNT/ (a number) items. This limit persists until you start a new search.
|
||
- =:t:TAG=, =t:TAG= Return items with /TAG/ (you can search for one tag at a time, a limitation of the Pocket API).
|
||
*** External Browsing
|
||
Browsing on a work laptop is a bit different. According to [[http://ergoemacs.org/emacs/emacs_set_default_browser.html][this page]], I can set a /default browser/ for different URLs, which is great, as I can launch my browser for personal browsing, or another browser for work access, or even EWW. To make this clear, I'm using the abstraction associated with [[https://github.com/rolandwalker/osx-browse][osx-browse]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package osx-browse
|
||
:init
|
||
(setq browse-url-handlers
|
||
'(("docs\\.google\\.com" . osx-browse-url-personal)
|
||
("grafana.com" . osx-browse-url-personal)
|
||
("dndbeyond.com" . osx-browse-url-personal)
|
||
("tabletopaudio.com" . osx-browse-url-personal)
|
||
("youtu.be" . osx-browse-url-personal)
|
||
("youtube.com" . osx-browse-url-personal)
|
||
("." . eww-browse-url)))
|
||
|
||
:config
|
||
(defun osx-browse-url-personal (url &optional new-window browser focus)
|
||
"Open URL in Firefox for my personal surfing.
|
||
The parameters, URL, NEW-WINDOW, and FOCUS are as documented in
|
||
the function, `osx-browse-url'."
|
||
(interactive (osx-browse-interactive-form))
|
||
(cl-callf or browser "org.mozilla.Firefox")
|
||
(osx-browse-url url new-window browser focus)))
|
||
#+end_src
|
||
** Neotree
|
||
I primarily use [[https://github.com/jaypei/emacs-neotree][Neotree]] when I am screen-sharing my Emacs session with collegues as it shows a /project/ like an IDE.
|
||
#+begin_src emacs-lisp
|
||
(use-package neotree
|
||
:general ; evil-collection forgot a couple:
|
||
(:states 'normal :keymaps 'neotree-mode-map
|
||
"TAB" 'neotree-enter
|
||
"SPC" 'neotree-quick-look
|
||
"RET" 'neotree-enter
|
||
"H" 'neotree-hidden-file-toggle))
|
||
#+end_src
|
||
** Annotations
|
||
Let's try [[https://github.com/bastibe/annotate.el][annotate-mode]], which allows you to drop "notes" and then move to them (yes, serious overlap with bookmarks, which we will return to).
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package annotate
|
||
:config
|
||
(ha-leader
|
||
"t A" '("annotations" . annotate-mode)
|
||
|
||
"n" '(:ignore t :which-key "notes")
|
||
"n a" '("toggle mode" . annotate-mode)
|
||
"n n" '("annotate" . annotate-annotate)
|
||
"n d" '("delete" . annotate-delete)
|
||
"n s" '("summary" . annotate-show-annotation-summary)
|
||
"n j" '("next" . annotate-goto-next-annotation)
|
||
"n k" '("prev" . annotate-goto-previous-annotation)
|
||
|
||
;; If a shift binding isn't set, it defaults to non-shift version
|
||
;; Use SPC N N to jump to the next error:
|
||
"n N" '("next error" . flycheck-next-error)))
|
||
#+end_src
|
||
Keep the annotations simple, almost /tag-like/, and then the summary allows you to display them.
|
||
** Keepass
|
||
Use the [[https://github.com/ifosch/keepass-mode][keepass-mode]] to view a /read-only/ version of my Keepass file in Emacs:
|
||
#+begin_src emacs-lisp
|
||
(use-package keepass-mode)
|
||
#+end_src
|
||
When having your point on a key entry, you can copy fields to kill-ring using:
|
||
- ~u~ :: URL
|
||
- ~b~ :: user name
|
||
- ~c~ :: password
|
||
|
||
** Demo It
|
||
Making demonstrations /within/ Emacs with my [[https://github.com/howardabrams/demo-it][demo-it]] project. While on MELPA, I want to use my own cloned version to make sure I can keep debugging it.
|
||
#+begin_src emacs-lisp
|
||
(use-package demo-it
|
||
:straight (:host github :repo "howardabrams/demo-it")
|
||
:commands (demo-it-create demo-it-start))
|
||
#+end_src
|
||
** PDF Viewing
|
||
Why not [[https://github.com/politza/pdf-tools][view PDF files]] better? To do this, first install the following on a Mac:
|
||
#+begin_src sh
|
||
brew install poppler automake
|
||
#+end_src
|
||
Instead run [[help:pdf-tools-install][pdf-tools-install]], as this command will do the above for the system.
|
||
|
||
Let’s install the Emacs connection to the =pdfinfo= program:
|
||
#+begin_src emacs-lisp
|
||
(use-package pdf-tools
|
||
:mode ("\\.pdf\\'" . pdf-view-mode)
|
||
:init
|
||
(setq pdf-info-epdfinfo-program "/usr/local/bin/epdfinfo")
|
||
:general
|
||
(:states 'normal :keymaps 'pdf-view-mode-map
|
||
"gp" 'pdf-view-goto-page
|
||
">" 'doc-view-fit-window-to-page))
|
||
#+end_src
|
||
|
||
Make sure the [[help:pdf-info-check-epdfinfo][pdf-info-check-epdfinfo]] function works.
|
||
* Technical Artifacts :noexport:
|
||
Let's provide a name so we can =require= this file:
|
||
#+begin_src emacs-lisp :exports none
|
||
(provide 'ha-config)
|
||
;;; ha-config.el ends here
|
||
#+end_src
|
||
|
||
Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
|
||
|
||
#+DESCRIPTION: A literate programming file for configuring Emacs.
|
||
|
||
#+PROPERTY: header-args:sh :tangle no
|
||
#+PROPERTY: header-args:emacs-lisp :tangle yes
|
||
#+PROPERTY: header-args :results none :eval no-export :comments no
|
||
|
||
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
|
||
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|