Recently I discovered a very cool feature of Emacs. Apparently, it’s been present in Emacs for over 30 years, although the Git log shows that it’s been heavily worked on very recently. Meet Hideshow mode. It’s purpose is simple: it allows to, well, hide or show blocks of code.
There is another feature like this (probably even older), called selective display (in fact, I’ve written about it several years ago). It turns out that Hideshow mode is way better. Its main disadvantage is that its keymap is bound to C-c @, which is probably one of the least ergonomic bindings possible. Apart from that, it’s great. You can press C-c @ C-d and C-c @ C-s to hide and show the current block, or just use C-c @ C-c to toggle the current block’s hide/show status. (You can also click S-mouse-2, that is, shift-click the middle button, if you are a mouse person.) Also, C-c @ C-t hides all top-level blocks in the buffer and C-c @ C-a shows everything. (Go to the manual to see a bit more features.)
But wait, there’s more! If you are brave enough to compile Emacs from the master branch, you’ll get something much better! Very recently, Emacs devs added a few fantastic options to Hideshow mode, and once I saw them, I immediately decided to turn them on.
First of all, you can set hs-display-lines-hidden to t, and then you’ll see the number of hidden lines along with the ellipsis. Then, you can set hs-show-indicators to t to see little chevrons in the fringe whenever there is a block starting in that line that can be shown or hidden. These chevrons can be clicked to toggle the visibility of the block.
One of the coolest features of Hideshow mode is the hs-cycle-filter option. It is nil by default, but you can set it to t so that pressing TAB on a line where a block begins toggles its visibility (via hs-toggle-hiding, that is, like C-c @ C-c). Of course, TAB is usually bound to indent-for-tab-command (at least whenever I edit JavaScript, which is my main use case for Hideshow mode). That’s why I set hs-cycle-filter to #'bolp – this means that the toggling behavior of TAB is only active when the point is at the beginning of line, and I can still use it to fix indentation with point somewhere else.
Now, I’ve saved one thing for the end of this post. There is one more command in Hideshow mode (again, only on the master branch as of writing this): hs-cycle. It works very similar to hs-toggle-hiding, but acts as a three-way toggle between hiding the whole block, showing the block but hiding its nested blocks, and showing everything beneath it. (It also accepts a numerical argument greater than 1 and then hides blocks that many levels underneath.) By default it is bound to C-c @ TAB, but I like it so much that I decided to make it easier to use. Here is what the docstring for the hs-cycle-filter option said pretty recently:
Currently it affects only the command hs-toggle-hiding by default, but it can be easily replaced with the command hs-cycle.
Here, the “easy” part means diving into the source code and changing what needs to be changed:
(keymap-set hs-minor-mode-map
"TAB"
`(menu-item
"" hs-cycle
:filter
,(lambda (cmd)
(when (and hs-cycle-filter
;; On the headline with hideable blocks
(save-excursion
(forward-line 0)
(hs-get-first-block-on-line))
(or (not (functionp hs-cycle-filter))
(funcall hs-cycle-filter)))
cmd))))
This is copied almost verbatim from the (defvar-keymap hs-minor-mode-map ...) form in hideshow.el – I just swapped hs-cycle for hs-toggle-hiding. (I have to admit that I don’t fully understand this snippet. I never studied Emacs menus, which are quite a sophisticated feature, and while I obviously have a general idea of how this code most probably works, I’d prefer not to analyze it too deeply – menus in Emacs are something I never use, and that is not something I want to change in the near future.) And now, TAB at the beginning of line when I edit JavaScript works much like in Org mode!
Of course, it would be nicer if Emacs allowed to make this happen without cargo-culting parts of its source code, so I made a feature request to allow to set this up more easily. Lo and behold, Emacs now has it! Elijah Gabe Pérez added a function called hs-add-cycle-binding, which now allows to do this:
(hs-add-cycle-binding nil "TAB" #'hs-cycle)
instead of my (much longer) snippet. Hurray!
The only thing I slightly miss is that Hideshow mode cannot hide pages, which are another feature of Emacs I often use to divide my source files into “sections”. But that is something which can be remedied in another way, and will be a topic of a future post.
Anyway, that’s it for today. I’m definitely going to keep an eye on any further Hideshow mode developments, and I strongly recommend you to check it out – especially when the next Emacs version is released!
Emacs has had a spell-checker for a very long time. In fact, I’ve been using ispell-word for at least several years now, as I have mentioned for example here. However, I’ve only used Flyspell mode once, over two decades ago, when I tried it out on a machine with maybe 8MB RAM and a Pentium-class processor clocked at about 100MHz and decided that it’s too slow to be really useful.
Well, things have changed a bit since then, haven’t they?
My current laptop has 32GB RAM and a 6-core i7 processor at 2.2GHz. I figured it should be fast enough to run a spell-checker, so I tried to enable flyspell-mode again.
For some reason, it still felt laggy. It may have had something to do with the fact that I ran it on a several-megabytes-large Org mode file. Happily, I remembered reading about Jinx, and I quickly tried it out. After a few minutes I decided to install it and added this to my init.el:
(add-hook 'emacs-startup-hook #'global-jinx-mode) (keymap-global-set "M-$" #'jinx-correct)
The default settings were not enough for me, though. I had quite specific needs. First of all, I needed Jinx to support two languages – English and Polish. That is easy enough:
(setq jinx-languages "en_US pl_PL")
Probably a better solution would be to have some code to detect and set the language of a file when visiting it. That would be a bit more complex, though, and would not help anyway since my personal Org files sometimes have both English and Polish inside.
The next problem was slightly more tricky. I needed Jinx to only use Polish in one specific directory. (I wanted spell-checking to work for my 9-year-old son, who’s just started to learn English, so he only writes in Polish.) Easy enough with per-directory local variables, especially that Emacs has the convenient add-dir-local-variable command. Here are the contents of the .dir-locals.el file in the directory where my son’s files live:
;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((jinx-languages . "pl_PL"))))
Of course, having Emacs variable-safety mechanism kick in whenever any file from that directory is visited is no good, either:
(add-to-list 'safe-local-variable-values '(jinx-languages . "pl_PL"))
The next customizations are even more tricky. Jinx’s way of selecting the correct spelling of a word is pretty sophisticated and I like it quite a lot. However, it turns out that Ivy interferes with it. I could write a simple wrapper around jinx-correct, disabling Ivy for a moment, but that would require rebinding M-$ in the jinx-overlay-map keymap (which is used by jinx-overlay). This means that I want jinx-correct itself to disable Ivy. Advice to the rescue:
(defun disable-ivy (orig-fun &rest args)
"Run ORIG-FUN with Ivy disabled."
(interactive)
(let ((completing-read-function #'completing-read-default))
(apply orig-fun args)))
(advice-add 'jinx-correct :around #'disable-ivy)
The next tweak I wanted was related to an excellent article about Avy, another of Oleh Krehel’s incredibly useful packages. It turns out that Avy can be used for a lot more things than just quickly jumping to places, and spell-checking is one of them. You can use Avy to point Emacs to a misspelled word anywhere in the buffer and select the ispell Avy action to correct it without moving point. (Well actually, the point moves internally of course, but the whole action is wrapped in save-excursion so that the user never notices that. Also, it turns out that when the misspelled word is in another buffer, the point is moved to its current position in that buffer. While technically this is probably a bug, I can definitely live with that – correcting a word in another buffer is weird enough a case that I don’t care too much about this behavior.) So, I want that Avy action to use Jinx, too:
(defun avy-action-jinx-correct (pt)
"Use Jinx to correct word at PT."
(save-excursion
(goto-char pt)
(jinx-correct)))
(setf (alist-get ?i avy-dispatch-alist)
#'avy-action-jinx-correct)
Another issue I wanted to change was that I really did not want Jinx to insert its name together with a very lengthy language name (en_US
pl_PL) into my modeline. My normal way of using Emacs is with two windows side-by-side, and too much stuff in the modeline really annoys me, so I added jinx-mode to mode-line-collapse-minor-modes. (I used to use Diminish, but Emacs now has a good replacement. Notice that as of writing this, you need to compile Emacs from sources to have this very new feature!)
The final thing is that I do have some files I don’t want to be spell-checked. As I learned recently, it is enough to say
# Local Variables: # jinx-mode: nil # End:
in such files.
And that’s it for my Jinx config. I have to say that I love it! Apart from being fast, it has quite a few nice features. For example, jinx-correct just corrects the nearest word if the current one is already spelled correctly. (In that case, “nearest” means the nearest one before point, and if all words visible in the buffer before point are spelled correctly, the nearest one after point.)
Happy spell-checking!
Some time ago, a friend of mine asked if I know an app which could allow him to analyze his spending based on bank statements. I did a quick research and I found a few tools like this, but nothing looked good enough for me to safely recommend it to him. Fortunately for him, he asked me that question in the exact moment in my life when (a) I had a lot of free time and (b) I wanted to spend it learning some potentially useful new skills. It didn’t take me long to decide to code such a tool myself.
After some back and forth with an LLM I settled on Python. I learned a tiny bit of Python in my student years, but never wrote anything more than a few dozen lines. Also, I figured that writing an application from scratch with a coding agent will be a good way to learn to use it. Of course, the risk is that I will just prompt it and repeatedly press enter instead of actually learning Python. That’s why I started with putting this in the instructions for the agent:
# Learning guidance **IMPORTANT** This is a learning project — the user writes the code himself. - When explaining Python concepts, bring up Node.js analogues when they help clarify (e.g. "this is like `require.main === module` in Node.js") - Guide and explain rather than writing implementation code unprompted - Only write code when explicitly asked to. When the user says "let's get coding", "time to code now", or something similar, it means *the user* wants to start coding. - When asked to check or review my code, be critical and honest – I want signal, not comfort. Pay attention to correct but not idiomatic code and suggest improvements.
I started with tkinter, but pretty soon migrated to PySide6 because I couldn’t get the coding agent to do what I needed to do in the former. I also added this to the instructions:
- Exception: I don't need to learn UI coding in Python – it's ok and in fact desirable for you to edit/code UI parts yourself.
A bit later, I also added this:
# User profile Even though I develop this on GNU/Linux, assume that the end-user will be a Windows/Excel user.
I have to say that the about 32 hours I’ve spent on this project, delivering a working prototype (genuinely useful despite some rough edges!) were a very pleasant experience. I learned quite a bit of Python. It turned out that its syntax is even worse than it was two decades ago (and that is quite an accomplishment!), but the standard library is absolutely great (especially compared to Node’s one, although that bar is not particularly high…). Basically, the only external dependencies I needed were PySide6, python-dotenv, pytest and a few developer tools: pyright, Ruff and PyInstaller. Everything else, including SQLite support, parsing and generating csv and timestamp support is built-in. This is in a very stark contrast to Node.js, where you have a ton of, say, CSV parsers on NPM, half of which probably contain some malware, and none built-in…
Still, I missed a few things from the JS ecosystem. For example, hot reload where saving a file immediately updates the web app you are developing is great. A poor man’s version I used was
$ while [ true ]; do uv run python main.py; done
and exiting the app whenever I wanted to see the changes live – but of course it’s far from a real hot reload, where you stay on the same view, with the same data etc. (It was enough for my simple app, though.) I found this project, but haven’t yet had a chance to use it.
Anyway, the app is on GitHub. Expect quite a few rough edges, do not expect I will review (not to mention merge) any PRs etc. – I am really busy these days, and if you happen to need my code, that’s great, but don’t count on me doing any work with it. (That doesn’t mean I won’t – I definitely want to, but without any guarantees.)
See you next week!