Emacs tips

#emacs

describe-prefix-bindings

Woah! When was this introduced? Why did no one tell me? Emacs essentially ships a which-key. With vertico/helm/ivy, it's pretty elegant. After typing a prefix, such as C-x, type C-h or ? or F1.

Tips on usage

Do it right the first time around🔗

Duncan, have I not told you that thinking you know something is the most perfect barrier against learning?

–Leto II Atreides

You tend to use the solution you know. If you can copy-paste by pointing and clicking in a certain way, you'll use that solution next time, and the time after that, even if there is a better way.

Knowing a trick bars learning new tricks.

That's why it's best to learn the best way to do it the first time around.

Auto-save-visited-mode in the tens🔗

It's the 2010s. Several operating system have auto-save by default on everything. And it is nice. Think about it: if you learned today the answer to the question, what is a "save" operation?

"Oh, a low-level behind-the-scenes function. What, you want me to call it manually every time? It was obviously written for use in scripts, not for binding to a hotkey. How odd that it comes bound by default, was the keybind designer even an user? I'll automate it away."

(auto-save-visited-mode)
(global-unset-key (kbd "C-x C-s"))
(global-unset-key (kbd "C-x s"))

Constantly learn something difficult🔗

The Tao of Emacs is to constantly be learning something difficult.

The tutorial is your first hurdle, though fortunately the most frustrating. You'll be settling down into a habit of taking on such hurdles, of sitting down with the manual and learning. Whether you learn about a built-in package, like Calc, an external package like Helm, or even a community config like Spacemacs, it's always the same story. It's like being back in the tutorial. "How does any of this work?"

And that's a good thing.

The secret to improving at a skill is to retain some degree of conscious control over it while practicing–to force oneself to stay out of autopilot.

– Moonwalking with Einstein: The Art and Science of Remembering Everything

The above principle, called deliberative practice, is why some say to avoid repetitive tasks in Emacs, and find the smart, efficient way to do each and every thing. Unless you're in the middle of a conference, you almost certainly have time, and it will pay off.

Suppose you want to stand on the shoulders of giants, to find the best way to interact with a computer, by co-opting all the best things other people have done. The first step is to get Emacs. That's where people like you have always went, so there's a lot of prior work in its ecosystem not present elsewhere.

Suppose further that after all the cards are down, you want to end up ahead of someone who sticks with "software for the masses", i.e. you want the time investment to pay off. It's not easy, because Emacs can invite yak-shaving and NIH syndrome, but even aside from that, it may never pay off for you if you postpone learning smart tricks in favour of familiar methods.

Want to save time?

  • Don't get distracted by attempts to get your color theme or modeline just so. Though it can feel like learning, it's a whole different class of activity from going through the Calc manual.
  • Learn the popular packages early. I mean packages like smartparens, yasnippet. It's the gift that keeps on giving. It's also painful to see a five-year Emacs user still be managing parens manually.
    • Learn them all, don't defer something until five years later. Think of it like catching Pokemon. The tutorial is your first Pokemon, and smartparens is maybe your second or fifth or fifteenth pokemon. But you must catch a few hundred before you can think of slowing down. There's that much useful stuff! Invest, it gives compound interest.
  • Quit having premature opinions. If someone else's figured out a neat way to do things, hop in the fire and learn their way. Afterward, it's OK to have an opinion.
  • To minimize NIH syndrome and the amount of private code (to which no-one can contribute), maintain your configuration on top of someone else's and submit patches upstream. You'll probably migrate several times, encountering a ton of solutions you wouldn't otherwise. That means you will be a whiz at Git and Magit.
    • Try to co-opt their way as much as possible every time, making your own config as small a "delta" as possible. It's painful, bur your neurons will add the necessary connections, and then it will be banal.
  • Spin packages out of your own code to reduce the amount you have to maintain alone.
  • Blog about neat tricks you discover. This both helps other people, and helps you.

The Eshell manual says this:

The role of a command shell is to give you more control over what your computer does for you. Not everyone needs this amount of control, and it does come at a cost: Learning the necessary script commands to express what you want done. A complicated query, such as [an example above this paragraph], takes time to learn. But if you find yourself using your computer frequently enough, it is more than worthwhile in the long run. Any tool you use often deserves the time spent learning to master it.

Decide if you agree with that sentence: "Any tool you use often deserves the time spent learning to master it."

When it comes to the tool that is your computer, a fully general information-processing system, that you use often… worth it?

Do not be a purist unnecssarily

Unless you are using EXWM, do not be a purist. For example, instead of learning about Emacs 27's tabs facility and taking months to decide on a hotkey for it because you have a hundred other Emacs facilities to learn about at the same time, spawn a new Emacs frame when you need a new "tab" (did you realize they can all be fullscreened simultaneously?) and use the foreign window manager's ability to switch between frames of the same app. In GNOME, that's M-`. It's quick to get started with and works predictably.

List from a set of lines

I looked for a function that would make an Org list out of a set of lines. I found something much more general. Let that be a lesson: when a package hasn't included a way to do a thing, perhaps that's because Emacs already ships a general way to do things of that sort.

For example, rectangle-mark (C-x SPC) the beginnings of these lines:

line1
line2
line3
line4
line5

Then do C-t (string-rectangle) and write "- " or anything you want.

Tips for initfiles

sentence-end-double-space🔗

So, this little guy, sentence-end-double-space. You know it, right? From the Emacs manual:

If you want to use just one space between sentences, you can set the variable ‘sentence-end-double-space’ to ‘nil’ to make the sentence commands stop for single spaces. However, this has a drawback: there is no way to distinguish between periods that end sentences and those that indicate abbreviations. For convenient and reliable editing, we therefore recommend you follow the two-space convention. The variable ‘sentence-end-double-space’ also affects filling.

If you look closely, you can see the double spaces in that very quote.

This has its roots in the era of typewriters. When I first learned of it, I thought it might be nice and endeavored to follow it, but ran into a big issue. Namely:

  • when the variable is true, sentences that don't end in a double space get skipped over by commands like forward-sentence and kill-sentence. Lots of people only type one space, so that's inconvenient when I edit their files.
  • when the variable is nil, every double space I write gets eliminated by fill-paragraph.

So I gave up, and joined the one-space crowd.

I thought there should be a way to modify fill-paragraph, but my Lisp-fu wasn't up to it back then. It just goes to show how little the non-Lisper is able to do in Emacs. Here's the solution:

(defun my-fill-paragraph-noclobber ()
  "Like `fill-paragraph', but pretend that
`sentence-end-double-space' is non-nil to avoid clobbering
existing double spaces."
  (interactive)
  (let ((sentence-end-double-space t))
    (call-interactively #'fill-paragraph)))

Or if you like unfill-toggle (that MELPA package is only three small defuns, by the way):

(defun my-fill-unfill-respect-double-space ()
  "Toggle filling/unfilling of the current region, or current
paragraph if no region is active.  Also pretend that
`sentence-end-double-space' is non-nil to avoid clobbering
existing double spaces. See `fill-paragraph' for what a prefix
command will do."
  (interactive)
  (let ((deactivate-mark nil)
        (fill-column (if (eq last-command this-command)
                         (prog1 most-positive-fixnum
                           (setq this-command nil))
                       fill-column))
        (sentence-end-double-space t))
    ;; For whatever reason, `fill-paragraph-function' does not get consulted in
    ;; org buffers, so we have to do this manually, even though we don't have
    ;; to call `lisp-fill-paragraph' explicitly in lisp buffers.
    (if (eq major-mode 'org-mode)
        (call-interactively #'org-fill-paragraph)
      (call-interactively #'fill-paragraph))))

Easy check for initfile errors before you restart🔗

(defun my-compile-and-drop ()
  "Compile buffer to see any errors, but don't write an .elc.
Original inspiration was to catch erroneous sexps
like (global-set-key \"c-x c\"...) that would break init, not for nitpicking things
that work, so `byte-compile-warnings' is temporarily overridden."
  (interactive)
  (when (eq major-mode #'emacs-lisp-mode)
    (cl-letf ((byte-compile-dest-file-function (lambda (_) (null-device)))
              (byte-compile-warnings '(suspicious))
              (inhibit-message t))
      (byte-compile-file (buffer-file-name)))))

(add-hook 'after-save-hook #'my-compile-and-drop)

Repeat last command N times

You know about numeric arguments – typing C-u 8 M-t or M-8 M-t would call M-t 8 times.

I'm not good at planning ahead like this. I'd rather choose how many times to do it after the fact. I'm fond of the repeat command, but it has to be spammed to do it many times – it does not take a numeric argument!

Assuming that we have the following key bindings (because putting them under a chord is insane)

  • <f1> for universal-argument
  • <f2> for repeat

the normal command sequence would look like

f1 8 M-t

and you could easily patch repeat to take a standard numeric prefix, enabling

M-t f1 8 f2

but I want a numeric postfix, enabling

M-t f2 8

The following snippet lets you do just that. Unfortunately it's limited to repeating up to 9 times, if you can extend it please teach me! I think typing e.g. 90 should first repeat 9, then undo 9 times and repeat 90 times instead.

(defun repeat-digit-times ()
  "Repeat the last command an amount of times, only up to 9
  because this command only works when you bind it to the keys 0-9.
  To do something 10 or more times, use the usual digit arguments."
  (interactive)
  ;; bits pasted from `repeat'
  (when (eq last-repeatable-command 'repeat)
    (setq last-repeatable-command repeat-previous-repeated-command))
  (when (eq last-repeatable-command 'repeat-digit-times)
    (message "I thought it was impossible to end up here"))
  ;; bits pasted from `digit-argument'
  (let* ((char (if (integerp last-command-event)
                   last-command-event
                 (get last-command-event 'ascii-character)))
         (digit (- (logand char ?\177) ?0)))
    (dotimes (i digit) (call-interactively #'repeat))))

(setq post-repeat-map (make-sparse-keymap))
(dotimes (i 10)
  (define-key post-repeat-map (kbd (int-to-string i)) #'repeat-digit-times))

(defun enable-post-repeat-map ()
  (set-transient-map post-repeat-map))

;; (add-function :after #'repeat #'enable-post-repeat-map)

A builtin way of doing this would be a keyboard macro; apparently, calling <f4> (kmacro-end-or-call-macro) with a numeric prefix 8 will run the macro 8 times. However, it's too many keystrokes:

M-t f3 f2 f1 8 f4

Eshell: prompt with timestamp

Not a big fan of long file paths in the prompt. We have the modeline for that. So I change the prompt to something more appropriate: a timestamp.

It's not straightforward. Everyone who has had the bright idea of a timestamped command prompt has run into the issue of updating it when you run a command, so that the timestamp accurately matches the time you ran it instead of the time the prompt appeared on screen, which could have been days ago. Here you go. (setc embark-prompter #'embark-completing-read-prompter)

(defun my-eshell-timestamp-update ()
  "When added to `eshell-pre-command-hook', the first string --:-- in the
prompt becomes a timestamp like 13:59 after you run a command."
  (save-excursion
    (forward-line -1)
    (when-let* ((unfilled-timestamp "--:--")
                (end (search-forward unfilled-timestamp nil t))
                (beg (- end (length unfilled-timestamp)))
                (inhibit-read-only t))
      (delete-region beg end)
      (insert (format-time-string "%H:%M"))
      (add-text-properties beg (point) '(font-lock-face eshell-prompt)))))

(with-eval-after-load 'eshell
  (add-hook 'eshell-pre-command-hook #'my-eshell-timestamp-update)
  (setq eshell-prompt-regexp (rx bol (repeat 7 nonl) " Sir? ")
        eshell-prompt-function (lambda () (concat "[--:--] Sir? "))))

Universal key for universal arg

This snippet is particularly justified with my as yet unpublished package github.com/meedstrom/deianira.

(my-before-keybinds
  ;; Normally, only C-u does universal-arg. Bind M-u and others too. Now we can do
  ;; M-u 5 M-f instead of C-u 5 M-f.
  (general-def "s-u"   #'universal-argument)
  (general-def "M-u"   #'universal-argument)
  (general-def "C-M-u" #'universal-argument)

  ;; general-def doesn't work with (format)
  (dolist (x (string-to-list "1234567890"))
    ;; Unbind C-1234567890 and co. Too good to waste on `digit-argument'.
    (global-unset-key (kbd (format "C-%c" x)))
    (global-unset-key (kbd (format "M-%c" x)))
    (global-unset-key (kbd (format "C-M-%c" x)))
    ;; After C-u/M-u/s-u, ensure that holding down the modifier makes no diff.
    ;; Now we can do M-u M-5 M-f as well as M-u 5 M-f.
    (define-key universal-argument-map (kbd (format "C-%c" x))   #'digit-argument)
    (define-key universal-argument-map (kbd (format "M-%c" x))   #'digit-argument)
    (define-key universal-argument-map (kbd (format "C-M-%c" x)) #'digit-argument)
    (define-key universal-argument-map (kbd (format "s-%c" x))   #'digit-argument)))

Toggle abnormal parts of config

(defvar my-normie-p nil)

(defun my-normie-toggle ()
  (interactive)
  (if my-normie-p (my-abnormalize) (my-normalize)))

(defun my-normalize ()
  (interactive)
  (setq my-normie-p t)
  (cua-mode)
  (abbrev-mode 0)
  (show-paren-mode 0)  ;; problem: this is buffer local
  (show-smartparens-global-mode 0)
  (global-company-mode 0)
  (rainbow-delimiters-mode 0)
  (objed-mode 0)
  (when-fbound* (aggressive-indent-mode 0))
  ;; (global-unset-key (kbd "<f13>"))
  ;; (global-unset-key (kbd "<f14>"))
  ;; (global-unset-key (kbd "<f15>"))
  (define-key key-translation-map (kbd "[") nil)
  (define-key key-translation-map (kbd "]") nil)
  (define-key key-translation-map (kbd "(") nil)
  (define-key key-translation-map (kbd ")") nil)
  ;; (my-disable-all-themes)
  ;; (load-theme 'base16-darktooth t)
  ;; (my-theme-mods)
  )

(defun my-abnormalize ()
  (interactive)
  (setq my-normie-p nil)
  (cua-mode 0)
  (when (featurep 'objed)
    (objed-mode))
  ;; we should keep track of the buffer where we turned normie on and reenable buffer-local things there specifcially
  ;; (my-reenable-when-default abbrev-mode) ;; started causing error for no reason
  (global-company-mode)
  (show-paren-mode)
  ;; cries about unknown variables
  ;; (my-reenable-when-default show-smartparens-mode)
  ;; (my-reenable-when-default aggressive-indent-mode)
  (define-key key-translation-map (kbd "[") (kbd "("))
  (define-key key-translation-map (kbd "]") (kbd ")"))
  (define-key key-translation-map (kbd "(") (kbd "["))
  (define-key key-translation-map (kbd ")") (kbd "]"))
  )

What has triggered me into learning🔗

  • Typing filenames by hand e.g. in the shell
    • Problem: I know Emacs ought to be able to know which filename I want, because it's open in another buffer
    • Solution: my-insert-other-buffer-file-name, or a properly customized hippie-expand/company
  • Multiline flashcard (org-drill/anki-editor) entries
    • Problem: Why did I experience that as so much worse than a simple list?
    • Solution: Learn some hotkeys for navigating Org headings
      1. Jump to parent headline (cognate to sp-backward-up-sexp?)
      2. Jump to next headline (cognate to sp-next-sexp?)
      3. Jump to next headline of upper level (composition of 1 & 2) (cognate to sp-up-sexp?)
      4. Jump to child headline (cognate to sp-down-sexp?)
  • Skimming many files in a directory
    • Problem: Not fun typing C-x C-f and scrolling thru the list all over again for each file
    • Solutions:
      • next-file-in-dir
      • learn that dired can open many files at once. Then next-buffer or quit-window to leaf thru them
  • Having to call async-shell-command to run a program to open a file whose name is right there (in dired for example)
    • Problem: Not as convenient as a normal file manager, which can just double click
    • Solution: my-dired-open-file-with-default-tool or crux-open-with
  • Browsing a filesystem tree with C-x C-f, then realizing I want to open the candidate location in a shell instead
    • Solution: embark, or just rapid switch between dired and shell
  • My problem with org-fc, org-noter

Documentation ideas

delete-blank-lines and just-one-space are cognates

Should have docstring tips about each other. See also join-line.

Created (3 years ago)