I have just released m-buffer version 0.10. A pretty minor point release, but I thought I would post about it, as I have not done so far.

Interacting with the text in an Emacs buffer can be a little bit painful at times. The main issue is that Emacs makes heavy use of global state its functions, something which has just rather gone out of fashion since Emacs was designed, and probably for good reason. I know that I am not the only to find this painful (http://www.wilfred.me.uk/blog/2013/03/31/essential-elisp-libraries). Perhaps, most obvious, is that many functions work on the current-buffer buffer, or at point. As a result, Emacs code tends to have lots of save-excursion and with-current-buffer calls sprinkled throughout.

We see the same thing in the regexp matching code. So for instance, the Emacs Lisp manual suggests the following:

(while (re-search-forward "foo[ \t]+bar" nil t)
   (replace-match "foobar"))

to replace all matches in a buffer. As you should be able to see, the call to replace-match contains no reference to what match is to be replaced. This is passed through the use of the global match data. Again, this made managable through the use of macros to save and restore the global state, in this case save-match-data. So, you end up with:

(save-match-data
  (save-excursion
    (while (re-search-forward "foo[ \t]+bar" nil t)
      (replace-match "foobar"))))

If you want to see this taken to extreme, have a look at the definition of m-buffer-match which is a good demonstration of the problem to which it is a solution.

The use of while is also less than ideal. It may not be obvious, but the code above contains a potential non-termination. For instance, the following should print out the position of every end-of-line in the buffer. Try it in an Emacs you don’t want.

(while (re-search-forward "$" nil t)
  (message "point at: %s" (point)))

It fails (and hangs) because the regexp $ is zero-width in the length. So, the re-search-forward moves point to just in front of the first new line, reports point, and then never moves again. This problem is a real one, and happens in real code.

m-buffer.el is my solution. Inspired partially by dash.el it provides (nearly) stateless, list orientated operations of buffer contents. So:

(m-buffer-match (current-buffer) "buffer")

returns a list of match-data to every occurrence of “buffer” in the current-buffer. There are also many, many convienience functions. So to match the end of the line, you could do:

(m-buffer-match
  (current-buffer)
  "$" :post-match 'm-buffer-post-match-forward-char)

which avoids the zero-width regexp problem, but it’s even easier to do.

(m-buffer-match-line-end (current-buffer))

Or to print to mini-buffer as before, you can instead combine with dash.el.

(--map
  (message "point at: %s" it)
 (m-buffer-match-line-end (current-buffer)))

m-buffer has been written for ease of use and not performance. So, by default, it uses markers everywhere which can potentially cause slowdowns in Emacs.I have started to add some functions for dealing with markers which need explicitly clearing explicitly; a traditional use for macros in lisp of closing existing resources. So

(m-buffer-with-markers
   ((end (m-buffer-match-line-end (current-buffer))))
 (--map
    (message "point at: %s" it)
   end))

leaves the buffer free of stray markers. The m-buffer-with-markers macro behaves like let* with cleanup.

I am now working on stateless functions for point — so m-buffer-at-point does the same as point but needs a buffer as an argument. Most of the code is trivial in this case, it just needs writing.

I am rather biased, but I like m-buffer very much. It massively simplified the development of lentic (http://www.russet.org.uk/blog/3047). Writing that with embedded while loops would have been painful in the extreme. I think I saved more time in using m-buffer than it took to write it. I think it could simplify many parts of Emacs development significantly. I hope others think so to.

m-buffer is available on MELPA and github

Bibliography

2 Comments

  1. Matus Goljer says:

    Well, thanks to you I now don’t have to write a library like this :)

    I started my own a year ago or so but never had time to finish it: https://github.com/Fuco1/b.el/blob/master/b.el

    It is/was more targeted at extracting content from buffers, not only with a regexp search but any “searcher” function (which had to follow some interface)

    I think I’ll add a link to your package instead, which seems to be going in the right direction.

    Thanks!

  2. Phillip Lord says:

    Glad you like it. Let me know if there is anything that you want added to it. m-buffer started off being for modifying buffers (that’s where the “m” came from) but I’ll be happy to add anything to do with a buffer — single buffers that it, I don’t want this to stray into manipulating the buffer list or window placement.

    I like the “b.el” name — I decided for something a bit longer, because we run the risk of getting many single letter libraries!

Leave a Reply