Posts
-
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
Read More(* ... *)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. -
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
diminishpackage 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))diminishgets 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 variablemode-line-collapse-minor-modes-to.Apart from
diminish, there are also the newerdelightandminionspackages that tackle more or less the same problem. As explained here forminions, 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:
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-modeto 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:
-
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-aandC-e)I’m guessing 99% of Emacs users know that the most common ways to start isearch are with
isearch-forward(C-s) andisearch-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:
sin this context meansSuper, which is usuallyWinin Windows andCommandin 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.ellibrary1, 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-winto 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
Commandkey 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 likeCommand + candCommand + vare 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-sorCommand-zworks in Emacs! Oh, well… habits!-
Emacs stubbornly keeps referring to macOS by its ancient name NextStep in much of the code and its documentation. ↩
-
Let's make keyboard-quit smarter
I’ll be pretty brief today.
keyboard-quit(C-g) is one of the most used commands, but unfortunately it’s not very smart. Most annoyingly, it doesn’t work as expected when the minibuffer is active.Fortunately, fixing such problems (and then some) is trivial in Emacs:
(defun er-keyboard-quit () "Smater version of the built-in `keyboard-quit'. The generic `keyboard-quit' does not do the expected thing when the minibuffer is open. Whereas we want it to close the minibuffer, even without explicitly focusing it." (interactive) (if (active-minibuffer-window) (if (minibufferp) (minibuffer-keyboard-quit) (abort-recursive-edit)) (keyboard-quit)))I’d suggest to just remap
keyboard-quitto our improved version:(global-set-key [remap keyboard-quit] #'er-keyboard-quit)There are other ways to tackle this particular issue, of course, and different people might prefer an even more complicated version of the smarter
keyboard-quitor one that does fewer things. One of my readers suggested in the comments a similar solution using an advice:(define-advice keyboard-quit (:around (quit) quit-current-context) "Quit the current context. When there is an active minibuffer and we are not inside it close it. When we are inside the minibuffer use the regular `minibuffer-keyboard-quit' which quits any active region before exiting. When there is no minibuffer `keyboard-quit' unless we are defining or executing a macro." (if (active-minibuffer-window) (if (minibufferp) (minibuffer-keyboard-quit) (abort-recursive-edit)) (unless (or defining-kbd-macro executing-kbd-macro) (funcall-interactively quit))))This has the benefit of directly modifying the original command, so you don’t really need to rebind anything. On the other hand - advices are arguably a bit more complicated to understand and debug. Personally, I like to replace functions in my own setup with versions that I prefer, as I think this makes the modifications more obvious.
Another option is a similar function from Prot:1
(defun prot/keyboard-quit-dwim () "Do-What-I-Mean behaviour for a general `keyboard-quit'. The generic `keyboard-quit' does not do the expected thing when the minibuffer is open. Whereas we want it to close the minibuffer, even without explicitly focusing it. The DWIM behaviour of this command is as follows: - When the region is active, disable it. - When a minibuffer is open, but not focused, close the minibuffer. - When the Completions buffer is selected, close it. - In every other case use the regular `keyboard-quit'." (interactive) (cond ((region-active-p) (keyboard-quit)) ((derived-mode-p 'completion-list-mode) (delete-completion-window)) ((> (minibuffer-depth) 0) (abort-recursive-edit)) (t (keyboard-quit))))I know this version of the command is quite popular in the wild, as many people follow Prot’s work, but looking at the code of the actual
keyboard-quitit seems to me that Prot’s version is more complicated than it needs to be:;; This executes C-g typed while Emacs is waiting for a command. ;; Quitting out of a program does not go through here; ;; that happens in the maybe_quit function at the C code level. (defun keyboard-quit () "Signal a `quit' condition. During execution of Lisp code, this character causes a quit directly. At top-level, as an editor command, this simply beeps." (interactive) ;; Avoid adding the region to the window selection. (setq saved-region-selection nil) (let (select-active-regions) (deactivate-mark)) (if (fboundp 'kmacro-keyboard-quit) (kmacro-keyboard-quit)) (when completion-in-region-mode (completion-in-region-mode -1)) ;; Force the next redisplay cycle to remove the "Def" indicator from ;; all the mode lines. (if defining-kbd-macro (force-mode-line-update t)) (setq defining-kbd-macro nil) (let ((debug-on-quit nil)) (signal 'quit nil)))As you can see it already handles things like the selected region and completion in region. But perhaps I’m missing what Prot was trying to achieve with his version.
Which of the three approaches do you prefer? How would you improve
er-keyboard-quit-dwimfurther?That’s all I have for you today! Keep hacking!
Subscribe via RSS | View Older Posts
