Published 2025-09-09
tag(s): #programming #emacs
A couple days ago I mentioned that I learned about page-ext
, a built-in Emacs
package that adds page navigation features.[1]
Turns out page-ext
has some things going against it:
C-x C-p
with a its own prefix map that has lots of
commands. A very minor crime, except...narrow-to-page
, which can be disabled in most, but
not all the commands.And at the end of the day there is only one feature I really liked and want to explore: the "page directory".
I figured I could do the same thing I did with a few packages by now, which is to create a less featureful, and more tailored, version of the code. Most times it ends up being completely different, because for the one or two things I want to do, I don't need to deal with edge cases or complex scenarios.
The first time I did this was
with fill-function-arguments,
which "pretty prints"...well, function arguments. And literals of lists, dictionaries,
etc.
Except that sometimes it would not indent things correctly, or put the second argument in the
wrong place, and a bunch of other corner cases around comments and strings.
And after adding more and more little tweaks in my own code to account for these problems, I
figured I might as well replace the whole package with my own version.[2]
Writing that was an interesting exercise because as I dropped more and more features, I came
to realize that what I really wanted was: to split a line by separators (but keeping strings
alone), and indent the altered text. That was equivalent to 90% of my uses
of fill-function-arguments
.
So I wrote a
single command
that does exactly that.
It is way more limited in scope, but it is also just a few lines of elisp. And since it does
less, it also has less surprising or unexpected behaviours.
I wrote today hoagie-pages
. It is v1, so might take some time until it is
polished. The code
is in
my config repo.
It does away with almost everything in pages-ext
but the "list all pages"
portion. When I was formatting the output, I wanted occur
-like bindings to
navigate to the next and previous page[3]. And that made me realize that
maybe I could get the result I wanted using said command instead of the pages
functionality.
The most promising version of this approach was capturing occur matches to get the page
"title", but there was always something a little bit off with the output. Unless I used a
numeric prefix to show multiple lines around the match, and by then I had to determine how to
augment the page-delimiter
regexp to capture what I wanted, or find other
mechanism to keep the "delimiter regex" easy to use and...
Yeah, it was getting complex rather quickly. And the occur
internals are
gnarly. So I decided to stick to the "mimic the pages directory" ethos.
After all, I don't even know how long the pages stuff will stick!!!
The one thing the I kept from those experiments, was making the output occur-like: on top of
the bindings mentioned before, I also went for the same look. Compare these three set of
results, first using the original pages-directory
, then occur
, and
finally the new hoagie-list-pages
. First in the page-ext.el
buffer:
Note that for occur
, I used a prefix argument of 1 to include in the output the
line with the page title.
Now same comparison, but running the three commands in this very buffer:
As spoiled in the screenshot, I have a couple suggestions of page-delimiter
values that I already set in my config:
"^\\(def\\|class\\) "
, which matches classes and top-level
functions. Removing the "beginning of line" restriction would match methods, too."
*<h[[:digit:]]"
, to match any heading tags.sql-interactive-mode
and shell
to use input lines to delimit pages. Although in comint
derived modes, navigating between inputs is trivial with C-c C-p (or n)
.imenu
related code from my init file, because
I never used it.
As a counterexample, when I learned about M-^
(aka delete-indentation
aka join-line
) I thought it was cute, but I already
had cycle-spacing
(with negative argument), so I didn't need it.
Yet nowadays I always join lines, and mostly cycle spaces without prefix argument. 🤷
Time will tell.
C-n
moves to the next line, but a
single n
moves to the next line AND moves point to the match in original
buffer