beginend.el
Thank you Nicolas for letting me borrow (again) your blog to talk about my work. This time, this will not only be my work, but the one of Matus Goljer too (aka Fuco1). Let me present beginend.
Genesis
Four years ago, I started being really annoyed by the fact that M-<
would go to the beginning of the dired buffer instead of the first
file as shown in this picture:
I then wrote these lines of Emacs-lisp code to fix this.
(defun dired-back-to-top ()
(interactive)
(beginning-of-buffer)
(dired-next-line (if dired-omit-mode 2 4)))
(define-key dired-mode-map
(vector 'remap 'beginning-of-buffer) 'dired-back-to-top)
At this time, I took the same approach to bind M-<
so that point
would go to the beginning of an email body instead of before headers:
(defun mu4e-compose-goto-top ()
(interactive)
(let ((old-position (point)))
(message-goto-body)
(when (equal (point) old-position)
(beginning-of-buffer))))
(define-key mu4e-compose-mode-map
(vector 'remap 'beginning-of-buffer) 'mu4e-compose-goto-top)
You can see that this one is a bit smarter. The function moves point
to the beginning of a message’s body, but if point is already there,
the point is moved to the buffer’s real beginning instead. This makes
it possible to press M-<
several times to switch between the real
buffer beginning and the meaningful one. I called this feature
double-tap.
I did the same for M->
and this served me well for the next two
years.
Start of the beginend project
Two years ago, I started cleaning my init.el file and decided to extract useful bits of Emacs Lisp code into separate packages. That was the beginning of the beginend project.
I also took the opportunity to improve the code to avoid duplication by introducing a macro:
(defun beginend-message-goto-beginning ()
"Go to the beginning of an email, after the headers."
(interactive)
(beginend--double-tap-begin
(message-goto-body)))
(defmacro beginend--double-tap-begin (&rest body)
"Evaluate &BODY and goto real beginning if that did not change point."
(let ((tempvar (make-symbol "old-position")))
`(let ((,tempvar (point)))
,@body
(when (equal ,tempvar (point))
(call-interactively #'beginning-of-buffer)))))
The function beginend-message-goto-beginning
is equivalent to the
function mu4e-compose-goto-top
defined above except it is
shorter. The macro beginend--double-tap-begin
implements double-tap
in a way independent of the kind of buffer being visited. The code
handling dired buffers used the same macro.
I released version 1 of the project.
Matus blog post and beginend on steroids
The project did not change for the next two years even though I was using it extensively. One day, I read Matus’ blog post titled:
Enhanced beginning- and end-of-buffer in special mode buffers (dired etc.)
This gave me energy to work on the package again. With Matus, we released version 2 featuring many changes:
- User visible:
- Add missing space to the mode lighter
- Add support for many major modes (magit-status, prog, occur, org-agenda, compilation, notmuch-search, elfeed, prodigy, …)
- Add a global minor mode
- Push mark when beginend moves point
- Make sure beginend is reasonable when buffer is narrowed
- Update README and include screencasts
- Make the end position coherent across modes
- Build process:
- Add Emacs-25.2 as build target
- Remove compiler warnings
- Add automated linting
- Implementation:
- Factor out common code into new macro making it easy to support more modes
- Testing:
- 84% of the code base is now covered by tests
- Convert tests to buttercup
Adding support for a new mode is a matter of a few lines of code now. Here is how beginend supports going to the meaningful beginning and end of message buffers now:
(beginend-define-mode message-mode
(progn
(message-goto-body))
(progn
(when (re-search-backward "^-- $" nil t)
(beginend--goto-nonwhitespace))))
The first progn
specifies how to go to the meaningful beginning
(i.e., after message headers) and the second one specifies how to go
to the meaningful end (i.e., before the signature). These six lines of
code also support double-tap, bind M-<
and M->
, take care of
buffer narrowing and set the mark appropriately.
Here are some screencasts demonstrating the behavior of beginend in some major modes.
Dired mode
The following shows how beginend reacts in dired mode when dired-hide-details or dired-omit is activated.
Message mode
This screencast shows how beginend allows ignoring both a message headers and signature.
Programming mode
This shows how beginend moves point at start and end of the code block in programming buffers, ignoring comments and blank lines.
Conclusion
I hope you enjoy using beginend as much as I enjoyed writing it.