Alejandro Ciniglio

Snake Case Elisp

I’ve been using Ocaml for some fun stuff lately (including generating this site). However, it only came to my attention recently that the style guide recommends snake_case for functions and variables. After doing so much Java, my default is CamelCasing all the time so I had some work ahead of me to clean it up.

I did a few by hand via Emacs’ subword-mode (aside: subword-mode is awesome and you should definitely use it), but this got tedious, so I thought it would be a good time to practice some elisp.

I knew I’d want to be able to call my function from the middle of a word and have it transform the whole thing, so I started by moving to the beginning of the word (backward-word is insufficient since if I’m already at the beginning of the word, it will take me to the word prior):

(defun aec/beginning-of-word ()
  "Move point to the beginning of nearest word"
  (interactive)
  (forward-word)
  (backward-word))

Easy enough; now I needed a way to know if I was done with the current word. I called this end-of-word-p, which is perhaps too general of a name, since it has some snake_case specific logic. Regular expressions in elisp are apparently not as straightforward as they are in languages I’m more familiar with, so I had to do some pretty crude expanded conditionals.

(defun aec/end-of-word-p (curpos)
  "whether the point is at the end of a word. Treats numerical digits 
   as non-word characters"
  (interactive "d")
  (or
   ;; end of buffer is obviously the end of the word
   (equal curpos (point-max))

   ;; word character followed by non-underscore non-word character
   (and
    (equal
      (string-match "\\w" (substring (buffer-string) (- curpos 2))) 
      0)
    (and
     (equal 
       (string-match "\\W" (substring (buffer-string) (- curpos 1))) 
       0)
     (not (equal 
            (string-match "_" (substring (buffer-string) (- curpos 1))) 
            0))))

   ;; underscore followed by non-word character
   (and
    (equal 
      (string-match "_" (substring (buffer-string) (- curpos 2))) 
      0)
    (equal 
      (string-match "\\W" (substring (buffer-string) (- curpos 1))) 
      0))))

Now that we have those tools, actually doing the work of snake_casing is easy enough: just convert the subwords to lowercase and insert underscores between them (and remember to delete the last underscore).

(defun snake-case-ify ()
  "Take a camelCased word and transform to snake_case"
  (interactive)
  (aec/beginning-of-word)
  (while (not (aec/end-of-word-p (point)))
    (call-interactively 'subword-downcase)
    (insert "_"))
  (delete-char -1))