Posts

  • Preview Regex Replacements as Diffs

    If you’ve ever hesitated before running query-replace-regexp across a large file (or worse, across many files), you’re not alone. Even experienced Emacs users get a bit nervous about large-scale regex replacements. What if the regex matches something unexpected? What if the replacement is subtly wrong?

    Emacs 30 has a brilliant answer to this anxiety: replace-regexp-as-diff.

    How it works

    Run M-x replace-regexp-as-diff, enter your search regexp and replacement string, and instead of immediately applying changes, Emacs shows you a diff buffer with all proposed replacements. You can review every single change in familiar unified diff format before committing to anything.

    If you’re happy with the changes, you can apply them as a patch. If something looks off, just close the diff buffer and nothing has changed.

    Multi-file support

    It gets even better. There are two companion commands for working across files:

    • multi-file-replace-regexp-as-diff — prompts you for a list of files and shows all replacements across them as a single diff.
    • dired-do-replace-regexp-as-diff — works on marked files in Dired. Mark the files you want to transform, run the command, and review the combined diff.

    The Dired integration is particularly nice — mark files with m, run the command from the Dired buffer, and you get a comprehensive preview of all changes.

    Note to self - explore how to hook this into Projectile.

    A practical example

    Say you want to rename a function across your project. In Dired:

    1. Mark all relevant files with m (or % m to mark by regexp)
    2. Run dired-do-replace-regexp-as-diff
    3. Enter the search pattern: \bold_function_name\b
    4. Enter the replacement: new_function_name
    5. Review the diff, apply if it looks good

    No more sweaty palms during large refactorings.1

    Closing Thoughts

    I have a feeling that in the age of LLMs probably few people will get excited about doing changes via patches, but it’s a pretty cool workflow overall. I love reviewing changes as diffs and I’ll try to incorporate some of the commands mentioned in this article in my Emacs workflow.

    That’s all I have for you today. Keep hacking!

    1. Assuming you’re still doing any large-scale refactorings “old-school”, that is. And that you actually read the diffs carefully! 

  • So Many Ways to Work with Comments

    I’ve been using Emacs for over 20 years and I still keep discovering (and rediscovering) comment-related commands and variables. You’d think that after two decades I’d have comments figured out, but it turns out there’s a surprising amount of depth hiding behind a few keybindings.

    What prompted this article was my recent work on neocaml, a tree-sitter based major mode for OCaml. OCaml uses (* ... *) block comments – no line comments at all – and that unusual syntax forced me to dig deeper into how Emacs handles comments internally. I learned more about comment variables in the past few months than in the previous 20 years combined.

    Read More
  • Hide Minor Modes in the Modeline in Emacs 31

    Most Emacs users run a tone of minor modes and many of them contribute something (usually useless) to the modeline. The problem is that the modeline is not infinite and can quickly get quite cluttered. That’s why for the longest time I’ve been using the third-party diminish package and I have something like this in my config:

    (use-package diminish
      :config
      (diminish 'abbrev-mode)
      (diminish 'flyspell-mode)
      (diminish 'flyspell-prog-mode)
      (diminish 'eldoc-mode))
    

    diminish gets the job done, but it’s a bit annoying that you need a third-party package for something so basic. Fortunately that’s about to change…

    I just learned that in Emacs 31 it’s finally possible to hide minor modes in the modeline using built-in functionality! Here’s how you can do the above:

    (setq mode-line-collapse-minor-modes '(abbrev-mode flyspell-mode flyspell-prog-mode eldoc-mode))
    

    And here’s how you can hide all minor modes (probably a bad idea, though, as some add useful info to the modeline):

    (setq mode-line-collapse-minor-modes '(not))
    

    For more info on what you can do with this new functionality see C-h v mode-line-collapse-minor-modes. After all, they don’t call Emacs the “self-documenting editor” for no reason.

    From the docs you’ll learn that hidden mode “lighters” (Emacs lingo for a mode’s modeline indicator) get compressed into one. It’s ... by default, but it can be customized via the variable mode-line-collapse-minor-modes-to.

    Apart from diminish, there are also the newer delight and minions packages that tackle more or less the same problem. As explained here for minions, they might still be useful, depending on your use-cases. One of the great aspects of Emacs is having options and when it comes to dealing with the minor mode lighters we have plenty of options!

    That’s all I have for you today. Happy Christmas holidays! Keep hacking!

  • Tree-sitter powered code completion

    Tree-sitter has taken the world of programming by a storm. Together with LSP, it’s probably the technology that has influenced the most programming editors and IDEs in the past several years. And now that Emacs 29+ comes with built-in Tree-sitter support I’ve been spending a lot of quality time with it, working on clojure-ts-mode and neocaml-mode.

    There’s a lot I’d like to share with you about using Tree-sitter effectively, but today I’ll focus on a different topic. When most people hear about Tree-sitter they think of font-locking (syntax highlighting) and indentation powered by the abstract syntax tree (AST), generated by a Tree-sitter grammar. For a while I’ve also been thinking that the AST data can also be used for simple, yet reasonably accurate, code completion. (within the context of a single code buffer, that is) That’s definitely not nearly as powerful of what you’d normally get from a dedicated tool (e.g. an LSP server), as those usually have project-wide completion capabilities, but it’s pretty sweet given that it’s trivial to implement and doesn’t require any external dependencies.

    Below, you’ll find a simple proof of concept for such a completion, in the context of clojure-ts-mode:1

    (defvar clojure-ts--completion-query-globals
      (treesit-query-compile 'clojure
                             `((source
                                (list_lit
                                 ((sym_lit) @sym
                                  (:match ,clojure-ts--variable-definition-symbol-regexp @sym))
                                 :anchor [(comment) (meta_lit) (old_meta_lit)] :*
                                 :anchor ((sym_lit) @var-candidate)))
                               (source
                                (list_lit
                                 ((sym_lit) @sym
                                  (:match ,clojure-ts--function-type-regexp @sym))
                                 :anchor [(comment) (meta_lit) (old_meta_lit)] :*
                                 :anchor ((sym_lit) @fn-candidate))))))
    
    (defconst clojure-ts--completion-annotations
      (list 'var-candidate " Global variable"
            'fn-candidate " Function"))
    
    (defun clojure-ts--completion-annotation-function (candidate)
      (thread-last minibuffer-completion-table
                   (alist-get candidate)
                   (plist-get clojure-ts--completion-annotations)))
    
    (defun clojure-ts-completion-at-point-function ()
      (when-let* ((bounds (bounds-of-thing-at-point 'symbol))
                  (source (treesit-buffer-root-node 'clojure))
                  (nodes (treesit-query-capture source clojure-ts--completion-query-globals)))
        (list (car bounds)
              (cdr bounds)
              (thread-last nodes
                     (seq-filter (lambda (item) (not (equal (car item) 'sym))))
                     (seq-map (lambda (item) (cons (treesit-node-text (cdr item) t) (car item)))))
              :exclusive 'no
              :annotation-function #'clojure-ts--completion-annotation-function)))
    

    I hope you’ll agree that the code is both simple and easy to follow (especially if you know a bit about Tree-sitter queries and Emacs’s completion APIs). The meat of the example is clojure-ts--completion-annotation-function, the rest is just completion scaffolding.

    And the result looks like this:

    clojure-ts-completion.png

    Not too shabby for 30 lines of code, right? With a bit more efforts this can be made smarter (e.g. to include local bindings as well), and potentially we can even be consulting all open buffers running clojure-ts-mode to fetch completion data from the as well. (although that’s probably an overkill)

    Still, I think that’s an interesting use of Tree-sitter that some of you might find useful. It seems that Nic Ferrier has been playing with this idea recently as well - check out his recent video on the subject here.

    In time Tree-sitter will redefine how we’re building Emacs major modes and what they can do.2 It’s still early days and sky is the limit. Exciting times ahead!

    That’s all I have for you today. Keep hacking!

    P.S. I plan to write more on the topic of Tree-sitter and how to use it in Emacs major modes, but in the mean time you might find some of my development notes useful:

    1. Kudos to Roman Rudakov, who put this prototype together earlier today after a short discussion we had on the topic. 

    2. I can easily imagine things like Tree-sitter based linters or complex refactoring commands. 

  • Little known macOS keybindings

    Today’s article is going to be a bit more weird than usual… mostly because I’ve set to write about one topic, and ended up about writing something completely different in the end… Here we go!

    TL;DR Many common macOS keybindings (e.g. Command-s, Command-z, Command-f, etc) work in Emacs. And, of course, it’s well known that macOS uses by default Emacs-like (readline) keybindings everywhere. (e.g. C-a and C-e)

    I’m guessing 99% of Emacs users know that the most common ways to start isearch are with isearch-forward (C-s) and isearch-backward (C-r). That’s not the full story, though! While working on my recent isearch article I noticed that out-of-the-box there are two other keybindings for those commands:

    • s-f (isearch-forward)
    • s-F (isearch-backward)

    Note: s in this context means Super, which is usually Win in Windows and Command in macOS.

    When I saw those I was like “hmm, seems someone wanted to make Emacs a bit more approachable to macOS users coming other editors”. But here things got interesting…

    I tried to find out where those extra keybindings were defined, and after a bit of digging I found them in the ns-win.el library1, which defines a ton of macOS-specific keybindings:

    ;; Here are some Nextstep-like bindings for command key sequences.
    (define-key global-map [?\s-,] 'customize)
    (define-key global-map [?\s-'] 'next-window-any-frame)
    (define-key global-map [?\s-`] 'other-frame)
    (define-key global-map [?\s-~] 'ns-prev-frame)
    (define-key global-map [?\s--] 'center-line)
    (define-key global-map [?\s-:] 'ispell)
    (define-key global-map [?\s-?] 'info)
    (define-key global-map [?\s-^] 'kill-some-buffers)
    (define-key global-map [?\s-&] 'kill-current-buffer)
    (define-key global-map [?\s-C] 'ns-popup-color-panel)
    (define-key global-map [?\s-D] 'dired)
    (define-key global-map [?\s-E] 'edit-abbrevs)
    (define-key global-map [?\s-L] 'shell-command)
    (define-key global-map [?\s-M] 'manual-entry)
    (define-key global-map [?\s-S] 'ns-write-file-using-panel)
    (define-key global-map [?\s-a] 'mark-whole-buffer)
    (define-key global-map [?\s-c] 'ns-copy-including-secondary)
    (define-key global-map [?\s-d] 'isearch-repeat-backward)
    (define-key global-map [?\s-e] 'isearch-yank-kill)
    (define-key global-map [?\s-f] 'isearch-forward)
    (define-key esc-map [?\s-f] 'isearch-forward-regexp)
    (define-key minibuffer-local-isearch-map [?\s-f]
      'isearch-forward-exit-minibuffer)
    (define-key isearch-mode-map [?\s-f] 'isearch-repeat-forward)
    (define-key global-map [?\s-F] 'isearch-backward)
    (define-key esc-map [?\s-F] 'isearch-backward-regexp)
    (define-key minibuffer-local-isearch-map [?\s-F]
      'isearch-reverse-exit-minibuffer)
    (define-key isearch-mode-map [?\s-F] 'isearch-repeat-backward)
    (define-key global-map [?\s-g] 'isearch-repeat-forward)
    (define-key global-map [?\s-h] 'ns-do-hide-emacs)
    (define-key global-map [?\s-H] 'ns-do-hide-others)
    (define-key global-map [?\M-\s-h] 'ns-do-hide-others)
    (define-key global-map [?\s-j] 'exchange-point-and-mark)
    (define-key global-map [?\s-k] 'kill-current-buffer)
    (define-key global-map [?\s-l] 'goto-line)
    (define-key global-map [?\s-m] 'iconify-frame)
    (define-key global-map [?\s-n] 'make-frame)
    (define-key global-map [?\s-o] 'ns-open-file-using-panel)
    (define-key global-map [?\s-p] 'ns-print-buffer)
    (define-key global-map [?\s-q] 'save-buffers-kill-emacs)
    (define-key global-map [?\s-s] 'save-buffer)
    (define-key global-map [?\s-t] 'menu-set-font)
    (define-key global-map [?\s-u] 'revert-buffer)
    (define-key global-map [?\s-v] 'yank)
    (define-key global-map [?\s-w] 'delete-frame)
    (define-key global-map [?\s-x] 'kill-region)
    (define-key global-map [?\s-y] 'ns-paste-secondary)
    (define-key global-map [?\s-z] 'undo)
    (define-key global-map [?\s-+] 'text-scale-adjust)
    (define-key global-map [?\s-=] 'text-scale-adjust)
    (define-key global-map [?\s--] 'text-scale-adjust)
    (define-key global-map [?\s-0] 'text-scale-adjust)
    (define-key global-map [?\s-|] 'shell-command-on-region)
    (define-key global-map [s-kp-bar] 'shell-command-on-region)
    (define-key global-map [?\C-\s- ] 'ns-do-show-character-palette)
    (define-key global-map [s-right] 'move-end-of-line)
    (define-key global-map [s-left] 'move-beginning-of-line)
    
    (define-key global-map [home] 'beginning-of-buffer)
    (define-key global-map [end] 'end-of-buffer)
    (define-key global-map [kp-home] 'beginning-of-buffer)
    (define-key global-map [kp-end] 'end-of-buffer)
    (define-key global-map [kp-prior] 'scroll-down-command)
    (define-key global-map [kp-next] 'scroll-up-command)
    
    ;; Allow shift-clicks to work similarly to under Nextstep.
    (define-key global-map [S-mouse-1] 'mouse-save-then-kill)
    (global-unset-key [S-down-mouse-1])
    
    ;; Special Nextstep-generated events are converted to function keys.  Here
    ;; are the bindings for them.  Note, these keys are actually declared in
    ;; x-setup-function-keys in common-win.
    (define-key global-map [ns-power-off] 'save-buffers-kill-emacs)
    (define-key global-map [ns-open-file] 'ns-find-file)
    (define-key global-map [ns-open-temp-file] [ns-open-file])
    (define-key global-map [ns-open-file-line] 'ns-open-file-select-line)
    (define-key global-map [ns-spi-service-call] 'ns-spi-service-call)
    (define-key global-map [ns-new-frame] 'make-frame)
    (define-key global-map [ns-toggle-toolbar] 'ns-toggle-toolbar)
    (define-key global-map [ns-show-prefs] '
    

    Some of them look quite convenient (easy to press), so I might add a few to my daily work. I’m shocked I never trying any of the standard macOS keybindings for things like adjusting text size in Emacs. Or perhaps I tried them and then I forgot about them… :D

    Still, even though I’m a macOS users (at least for the time being), I doubt I’ll end up using many of them. The reason for this is that I learned Emacs on Linux and I’m extremely used to the default keybindings. Between remembering all of those, and trying to master Vim (as of late), it’s hard to teach this old dog any new tricks. That being sad, I can imagine those keybindings being useful to many other people, especially if they haven’t learned Emacs on Linux 20 years ago.

    Tip: Do a M-x find-library RET ns-win to see what else the library has in store for macOS users.

    All of this is, of course, made possible by the fact that macOS relies heavily on the Command key which normally isn’t used in Emacs at all. For similar reasons it’s “easier” to copy/paste text from/in your shell on macOS, compared to Linux and Windows, as keybindings like Command + c and Command + v are not used by any shell.

    That’s all I have for you today! Keep hacking!

    P.S. After writing this article I was really amused that I’ve been using macOS on and off for over 10 years and I never bothered to try whether something like Command-s or Command-z works in Emacs! Oh, well… habits!

    1. Emacs stubbornly keeps referring to macOS by its ancient name NextStep in much of the code and its documentation. 

Subscribe via RSS | View Older Posts