This page shows a example of writing a emacs lisp command that changes space to underscore of the current line, or among hypen, underscore, space.
I often need to change all underscore “_” characters to space, or space to underscore (or sometimes to hyphen). This is most often used on file name, or moving file name to article title.
It's very tedious to do this manually.
In emacs, the quickest way is to select a region then call replace-string or query-replace 【Alt+%】. That's usually some 10 keystrokes.
I want a single key press!
In the beginning, i wrote these commands:
(defun space2underscore-region (start end) "Replace space by underscore in region." (interactive "r") (save-restriction (narrow-to-region start end) (goto-char (point-min)) (while (search-forward " " nil t) (replace-match "_")) ) ) (defun underscore2space-region (start end) "Replace underscore by space in region." (interactive "r") (save-restriction (narrow-to-region start end) (goto-char (point-min)) (while (search-forward "_" nil t) (replace-match " ")) ))
To use it, you make a text selection first, then call the right command.
You can give it a key. See: Emacs: How to Define Keys.
After a few months, i find it annoying that i have to think about which command to call. So, i thought: why not make it toggle? So, i wrote this:
(defun replace-underscore-space-toggle () "Replace underscore/space in the current region or line. If the current line contains more “_” char than space, then replace them to space, else replace space to _. If there's a text selection, work on the selected text." (interactive) (let (li bds) (setq bds (if (region-active-p) (cons (region-beginning) (region-end)) (bounds-of-thing-at-point 'line))) (setq li (buffer-substring-no-properties (car bds) (cdr bds))) (if (> (count 32 li) (count 95 li)) (replace-string " " "_" nil (car bds) (cdr bds)) (replace-string "_" " " nil (car bds) (cdr bds)))))
The code is smart. If you have a text selection, it works on the text selection, else the current line. Also, it looks at your text and count the number of occurrence of underscore/space. If there are more space than underscore, then it changes them to space, else it does the other direction.
Note the “32” in (count 32 li). In elisp, char datatype is just integer. 32 is the Unicode codepoint for space char. Similarly, 95 is underscore. (you can find out a char's codepoint by calling describe-char.)
After using the above code for 1 year, today i also find that sometimes i need to replace hypen “-” to underscore. At first i just quickly wrote a “hypen2space-region”, but quickly realized that i'm back to having to think about what command i need to call. It would be great, to have a command that cycle between these chars. Here it is:
(defun cycle-hyphen-underscore-space () "Cyclically replace {underscore, space, hypen} chars current line or text selection. When called repeatedly, this command cycles the {“ ”, “_”, “-”} characters." (interactive) ;; this function sets a property 「'state」. Possible values are 0 to length of charArray. (let (mainText charArray p1 p2 currentState nextState changeFrom changeTo startedWithRegion-p ) (if (region-active-p) (progn (setq startedWithRegion-p t ) (setq p1 (region-beginning)) (setq p2 (region-end)) ) (progn (setq startedWithRegion-p nil ) (setq p1 (line-beginning-position)) (setq p2 (line-end-position)) ) ) (setq charArray [" " "_" "-"]) (setq currentState (if (get 'cycle-hyphen-underscore-space 'state) (get 'cycle-hyphen-underscore-space 'state) 0)) (setq nextState (% (+ currentState 1) (length charArray))) (setq changeFrom (elt charArray currentState )) (setq changeTo (elt charArray nextState )) (setq mainText (replace-regexp-in-string changeFrom changeTo (buffer-substring-no-properties p1 p2)) ) (delete-region p1 p2) (insert mainText) (put 'cycle-hyphen-underscore-space 'state nextState) (when startedWithRegion-p (goto-char p2) (set-mark p1) (setq deactivate-mark nil) ) ) )
Here's the gist of how this command works.
The function sets up a state. So when called repeatedly, it knows which to cycle to.
The state is done in lisp as “properties”. In HTML/XML, a tag can have several pairs of name/value attributes. This is similar to lisp's “properties”. Any elisp function can have a property.
A property is basically a name/value pair. A function can have any number of properties, each with any name. The name's datatype can be a lisp “symbol”, string, or others.
You can get and set properties using get and put.
(info "(elisp) Property Lists")
In our function, first we set a character array, as a vector, like this (setq charArray [" " "_" "-"]). Because we don't need this list to grow, so we use a lisp vector instead of lisp list. 〔☛ Emacs Lisp Tutorial: List & Vector〕
Our property name is a symbol 'state, and possible values are integers, 0, 1, 2, corresponding to the index of charArray.
To cycle thru states, we just use modular arithmetics. For example, if we have states 0 to m, and current state is n, then next state is “mod( n + 1, m)”. Here's the corresponding code:
;; if state exists, get that, else set to state 0 (setq currentState (if (get 'cycle-hyphen-underscore-space 'state) (get 'cycle-hyphen-underscore-space 'state) 0)) (setq nextState (% (+ currentState 1) (length charArray)))
The rest of the code is easy to understand.
When this command is called with a text selection, by default emacs
will de-activate the text selection after a command is
finished. However, for this command, we want the text selection to
stay, because user might call the command again to cycle replace. So,
if region is active, we set “startedWithRegion-p” to true. At the end of the
code, we restore the region's active status.
Emacs has a global variable deactivate-mark used to control whether the mark is automatically de-activated when a command is called (when transient-mark-mode is on). When emacs invokes a command, it sets this variable to true. When the command is finished, emacs checks this variable to see if it should de-activate the mark. So, in our code, at the end we put: (setq deactivate-mark nil).
〔☛ Emacs: What's Region, Active Region, transient-mark-mode?〕
Emacs is fantastic!