Revision 6 not available (showing current revision instead)

Blog

For the English part of the blog, see Content AND Presentation.

2024-04-15 Improving recenter-top-bottom and reposition-window

If one can be a fan of an Emacs command, then I am a huge fan of recenter-top-bottom (C-l) and reposition-window (C-M-l). I use them all the time to see the context of what I’m editing at the moment. However, they are not always that useful. They are rather crude – recenter-top-bottom only has three “settings”, as the name suggests, and reposition-window has only two (it either puts the first line of a function, or the first line of a comment preceding the function at the top). As I mentioned a few weeks ago, I sometimes work with rather long functions – and sometimes I am in the second of two shorter ones, but I want to see the first one, too. Also, I don’t only edit code – I edit prose, too, where paragraph play the role of functions, and Org files, where there are even other structural elements – headlines, tables and source blocks in addition to paragraphs, for example.

I decided to write a variation on the theme of reposition-window, which – instead of putting the first line of the function I’m in at the top, it tries to put the first line of a “section” I’m in at the top. What a “section” is can be a bit vague. For example, in the simplest case, a “section” is just a fragment of the buffer separated from the rest by one or more blank lines. This may mean a paragraph (in prose), or a logical part of a function in code (assuming that the programmer puts such blank lines in suitable places, which is a very good practice anyway – putting them between functions is a minimum, and putting a few of them in longer and more complicated functions is what we do at my company anyway). For a more complex scenario, consider an Org mode headline or block which also start a new section.

Of course, this is still pretty imprecise, and there are quite a few ways it could work, especially in corner cases. One possible implementation I came up with is the following code.

(defconst reposition-window-section-re "\\`\\|\n\\s-*\n\\|^\\*+[ ]\\|#\\+begin"
  "Regular expression matching a \"section\" beginning.
For `reposition-window-section' to work properly, this must match
the beginning of the buffer, too.")

(defun reposition-window-section ()
  "Make the current section visible.
Here, \"section\" means a fragment starting with something that
matches `reposition-window-section-re'.  If the current section
is already visible, scroll up so that one more section becomes
visible, and if that is not possible, scroll down so that only
the current section is.  If the current section is larger than
the screen, fall back to `recenter-top-bottom'."
  (interactive)
  (save-excursion
    (let ((orig (point))
          (at-top (pos-visible-in-window-p (point-min))))
      (unless at-top
        (goto-char (window-start))
        (while (progn (re-search-backward reposition-window-section-re nil t)
                      (or (invisible-p (point))
                          (pos-visible-in-window-p (match-end 0)))))
        (goto-char (match-end 0))
        (forward-line 0)
        (set-window-start (selected-window) (point)))
      ;; if going to one section up moves point out of the window, go to the section right before point
      (when (or (not (pos-visible-in-window-p orig))
                ;; do the same if we started at the top of the buffer
                at-top)
        (goto-char orig)
        (re-search-backward reposition-window-section-re nil t)
        (goto-char (match-end 0))
        (forward-line 0)
        (set-window-start (selected-window) (point))
        ;; if that also moves point out of the window, just (recenter-top-bottom)
        (unless (pos-visible-in-window-p orig)
          (goto-char orig)
          (recenter-top-bottom))))))

(global-set-key (kbd "C-S-l") #'reposition-window-section)

It was my third attempt (not counting a lot of minor changes). The first one was needlessly complicated, and depended on (eq last-command #'reposition-window-section) so that pressing C-S-l twice in a row worked differently from pressing it once, doing anything else (e.g. small-scale point motion like C-f) and pressing it again. I generally consider commands which rely on external state like that inferior design, and while sometimes it is exactly what is called for, I decided that I’d better avoid it. (Interestingly, recenter-top-bottom, which is sort of a grandfather of my command, does exactly that. That leads to a subtle issue when the point is already in the middle line. In that case, it does nothing, which is potentially confusing for the user.) The second one was simply buggy (and I really do hope this one is not).

I am still not 100% satisfied with this design, which seems not very elegant with all the unless and when clauses. I like to comfort myself that elegance in Elisp commands is sometimes very difficult to achieve. Text editing is a surprisingly complicated subject (as evidenced for example by an excellent series of articles about tree-sitter and Combobulate by Mickey Petersen), and making your editor behave in an intuitive and useful way means dealing with quite a few non-obvious edge cases. This is exactly the situation here. The main idea is simple – to have a regex matching a “section beginning”, and to move the text in the window so that more and more “sections” are fully visible. Here are the possible special cases I could imagine.

  1. The section beginning regex could match a fragment of one line or could span several lines.
  2. Showing “one more section” could move the point out of the window.
  3. The section the point is in could be larger than the screen.
  4. We could have started at the top of the buffer (that is, (point-min) could be already visible).
  5. There could be one no section beginnings before the point.
  6. The section above the screen could be folded.

The first problem deserves an explanation. As I mentioned, one possible notion of a “section beginning” is one or more blank lines. In that case, that beginning itself should not be visible – there is no point in showing the user empty lines, especially in a command whose main purpose is to make the best use of limited screen real estate. However, an equally valid notion of a “section beginning” (for Org mode files at least) is a line beginning with one or more stars or a #+begin_whatever block. In that case we definitely want it to be visible. My solution, covering both cases, is a bit hackish, but I like to think that it’s clever: the first line visible will be the line where the end of the “section beginning” falls. This covers both cases described above.

Also, this is the reason why it is not enough to go to (window-start) and search for reposition-window-section-re backwards. When looking for the “one or more blank lines” type of section beginning, and assuming that these blank lines fall exactly above what is visible on the screen, when we go to (match-end 0) we end up exactly where we started. This means that in this situation we need to search backwards twice. The most elegant way of expressing this in code is a repeat-until-type loop – which in Elisp is implemented as a while loop with the whole body packed into the while condition – and repeating the search until (match-end 0) is no longer visible on the screen.

The second problem is pretty easy to solve. After we move (window-start) to the beginning of the section we’ve found, we need to check if the starting point is still visible. If not, we need to go back to where we started and look for the nearest section beginning above. This way, our command will “cycle” – show more and more sections until it’s impossible without losing the point from view, and then it will show just the section the point is in and as much as possible below.

However, it is also possible that we couldn’t do even that (item 3). In that case, we just fall back to (recenter-top-bottom). It’s not ideal, but at least we do something, so that the user clearly sees that something is happening. (I wanted to avoid the situation where pressing C-S-l does nothing at all. It is still possible – calling recenter-top-bottom when exactly in the middle of the screen indeed appears to do nothing, as I mentioned before – but working around an extremely rare special case where just pressing C-S-l again solves the issue anyway didn’t seem worth it.)

If all that were not enough, it is also possible that we start at the top of the buffer (that is, with (point-min) already visible). If that is the case, we cannot show more above the point, so we should do the same thing as in item 2 – move the section the point is in to the top.

Similarly, it may happen that there is no section beginning before point - in other words, we are in the first section in the buffer and our regex does not really match a section beginning per se, but a section separator. I’m on the fence with respect to this one – there is a very simple way to avoid this altogether, and that is to include \` as one of the variants in reposition-window-section-re. This regex matches the beginning of the buffer, so including it ensures that it’s always treated as a beginning of a section. On the other hand, if a user forgets to include it when customizing reposition-window-section-re, reposition-window-section will work incorrectly in this situation. After a short consideration I decided that I’ll go for the simpler code and mention in the docstring that this must be included manually. The assumption here is that most users (if my code ever has any user besides me, that is;-)) won’t even customize that option, and if they do, they have the docstring, and if they don’t read it, reposition-window-section will still work in most cirumstances. (Of course, I could add \` to the regex in my function each time it is run, or – if I decided to get really fancy – I could also add some code ensuring \`\| is added to the variable value in the {{{:set}}} parameter of {{{defcustom}}}.)

Last but not least, the section(s) above the portion visible on the screen could be invisible (this happens all the time with Org mode structure folding). That is pretty easy to solve – we can just keep looking for sections until the one we’ve found is visible.

Ok, that was quite a trip. The end result is still not ideal. For instance, I can imagine that a prefix argument could cause this command to scroll down instead of up. This way more and more context below the point would become visible. It is not obvious to me whether it would be better to keep showing whole sections at the top or at the bottom of the screen then – I guess bottom would make more sense. To achieve this, I would revisit the idea of checking last-command – it would be very inconvenient to have to have to type C-u C-S-l C-u C-S-l instead of just C-u C-S-l C-S-l, for example. Another way to make using a prefix argument with a command which tends to be repeated many times in a row would be to bind it to some key sequence, say C-x C-S-l, and use the mechanism of transient keymaps. I have to admit that I never used that and I’m not sure if this would even work, that is, if subsequent invocations would receive the same prefix argument as the first one – I’m afraid not, but it’s something worth investigating. I’ll look into it some day, I think. Still, the command is pretty useful even without that feature, and I have to say that it was useful even before I fleshed it out, while it was still buggy!

The last thing I’d like to say in this already rather long post is that if you’d like to learn how to code various convenience commands like this, I always recommend two sources – Introduction to programming in Emacs Lisp by the late Robert J. Chassell first, and my book, Hacking your way around in Emacs, if you want to dive a bit deeper. In fact, I am very much tempted to add this exact command as a topic of another chapter in that book – it is neither too simplistic nor too complex, and it is potentially useful to everyone (it does not depend on whether you use Emacs for coding or for prose). But that is more like a long-term plan.

CategoryEnglish, CategoryBlog, CategoryEmacs

Comments on this page

2024-04-08 Even more Magit tips

Almost five years ago I wrote a short post with some Magit tips. Well, why not write some more? Magit is slowly but constantly evolving, and recently I discovered something very useful I didn’t even know existed.

Magit defines the variable magit-define-global-key-bindings, with a default value being, well, the symbol default. This value means that Magit defines a few keybindings accessible from anywhere in Emacs. These are:

  • C-x g for magit-status. As far as I remember, this has always been the recommended choice. I am pretty sure that early Magit versions did not bind this by default, since I had this binding in my init.el, but now this Just Works™ (unless you explicitly set magit-define-global-key-bindings to nil, of course, or bind C-x g to something else – Magit does not rebind keys you have already bound).
  • C-x M-g for magit-dispatch. The command has a rather short docstring, but from what I understand after trying it out and reading about it in the manual, it is roughly equivalent to magit-status but without actually displaying the status. What I mean by this is that, for example, C-x g followed by l l shows the Git log of the current branch, and C-x M-g l l does exactly the same, but without showing the Magit status buffer, and pressing q in the log buffer then gets us back to the buffer we were in beforehand. That’s nice, but not indispensable.
  • C-c M-g for magit-file-dispatch. This is definitely the most interesting and non-obvious command of these three. It shows a transient menu for Magit commands operating on the current file. For instance, unlike magit-status and magit-dispatch (where the concept of blaming doesn’t make sense, since you can’t run git blame on the whole repository), you can run magit-blame from that menu. Even better, l runs magit-log-file-buffer, which shows Git log restricted to the current file. You can also stage a file with it and do a bunch of other, perhaps less often useful (but not less useful!) stuff. For instance, poking around I learned about the magit-edit-line-commit command. It is esoteric enough to be disabled by default, but the concept is really cool – it starts an interactive rebase where you can edit the commit which added the line you are in. This way, if you spot some simple mistake like a typo (on a yet unpushed branch), you can easily fix it “in place”, that is, in the commit it was made.

The only drawback I can see is that the C-x M-g and C-c M-g bindings are not very easy to remember. This can be remedied with setting magit-define-global-key-bindings to recommended. In that case, magit-dispatch gets bound to C-c g (I assume that here, g stands for “global”) and magit-file-dispatch to C-c f (supposedly, f standing for file). These bindings could not be made default because keys like C-c <letter> are reserved for the user, but it’s of course fine to use them if the user explicitly asks for that.

That’s it for today, I really hope this is useful for some of you out there!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryGit

Comments on this page

2024-03-31 Easter 2024

Christ has risen from the dead! And so shall we all. I wish you the best for Easter! And of course, as the tradition dictates, I will pray a decade of Rosary for all readers (of both of my blogs).

Happy Easter, rejoice!

(Note: I published this a day later because I had some issue with my computer which I only managed to fix today – it was Easter, after all, I had more important things to do than to play around with a broken laptop! I decided to leave the yesterday’s date of the post, though.)

CategoryEnglish, CategoryBlog, CategoryFaith

Comments on this page

More...