Skip navigation

This was once the most important portion of elisp code I had put into my dot emacs.  Thanks to it I began to conciliate with the editor which I sort of resisted at first, by finding it foreign to my habits and liking. I was accustomed to the way Dreamveaver handled the indenting of selected text in blocks, so getting emacs to behave in such familiar and practical style was critical to ease the experience of using it in a daily basis -as I needed  a couple of years ago when entering a new position.

Pretty soon I saw why emacs was so superior in regard of its extensibility: with basic elisp knowledge, the help of this discussion and a few lines of code tweaked I was able to get indent/unindent function the way I wanted. Now to select text as a block, I not longer needed to set the mark and point exactly from the beginning to the end of a line or group of lines. The code conveniently extended the highlighted area via the tab key, turning the sub-selection into a block containing all the text of the line(s) involved.

Besides, the selection was configured so to not disappear unless you click outside it, that is, while keeping  pressed the tab (or Shift-tab) key(s) you could get the text to shift continuously right (or left).

Having bound the tab key with the indentation functions required a few adjustment to make it context-sensitive. Here is how the tab key behaves after the changes:

  • First, to insert a real tab space, (when the cursor is before a word), you need to use Ctrl -q-tab.
  • If the text point is, at least, a space away from the end of a word, a standard tab-space is inserted.
  • If the text point is exactly at the end of a word, the tab uses the hippie-expand function to auto-complete the word. If Shift+tab are used instead, words are deleted backward one at a time up to the beginning of the line.
  • When editing in org-mode an exception was set to respect its visibility cycling function (org-mode cleverly uses the tab key to rotate the current state of tree and sub-trees).

Finally, Mac and PC users would probably appreciate the added function that makes Shift + click to end a selection of text, which is the default way on both platforms (I grabbed it here, from Dave Peck).

So, as you can see, the whole point of the code below is to ease the manual indentation of code. Not being comfortable with the automatic indentation in emacs, I like to avoid it at times, or for certain modes. Check the associated hack posted on how to deactivate it.

Please, have a look at the elisp lines. Remember that you could copy and paste all into the scratch buffer and do “M-x evaluate-buffer” to get them immediately working.

;; turn on transient mark mode
;;(that is, we highlight the selected text)
(transient-mark-mode t)

(setq my-tab-width 2)

(defun indent-block()
  (shift-region my-tab-width)
  (setq deactivate-mark nil))

(defun unindent-block()
  (shift-region (- my-tab-width))
  (setq deactivate-mark nil))

(defun shift-region(numcols)
" my trick to expand the region to the beginning and end of the area selected
 much in the handy way I liked in the Dreamweaver editor."
  (if (< (point)(mark))
    (if (not(bolp))    (progn (beginning-of-line)(exchange-point-and-mark) (end-of-line)))
    (progn (end-of-line)(exchange-point-and-mark)(beginning-of-line)))
  (setq region-start (region-beginning))
  (setq region-finish (region-end))
  (save-excursion
    (if (< (point) (mark)) (exchange-point-and-mark))
    (let ((save-mark (mark)))
      (indent-rigidly region-start region-finish numcols))))

(defun indent-or-complete ()
  "Indent region selected as a block; if no selection present either indent according to mode,
or expand the word preceding point. "
  (interactive)
  (if  mark-active
      (indent-block)
    (if (looking-at "\\>")
  (hippie-expand nil)
      (insert "\t"))))

(defun my-unindent()
  "Unindent line, or block if it's a region selected.
When pressing Shift+tab, erase words backward (one at a time) up to the beginning of line.
Now it correctly stops at the beginning of the line when the pointer is at the first char of an indented line. Before the command would (unconveniently)  kill all the white spaces, as well as the last word of the previous line."

  (interactive)
  (if mark-active
      (unindent-block)
    (progn
      (unless(bolp)
        (if (looking-back "^[ \t]*")
            (progn
              ;;"a" holds how many spaces are there to the beginning of the line
              (let ((a (length(buffer-substring-no-properties (point-at-bol) (point)))))
                (progn
                  ;; delete backwards progressively in my-tab-width steps, but without going further of the beginning of line.
                  (if (> a my-tab-width)
                      (delete-backward-char my-tab-width)
                    (backward-delete-char a)))))
          ;; delete tab and spaces first, if at least 2 exist, before removing words
          (progn
            (if(looking-back "[ \t]\\{2,\\}")
                (delete-horizontal-space)
              (backward-kill-word 1))))))))

(add-hook 'find-file-hooks (function (lambda ()
 (unless (eq major-mode 'org-mode)
(local-set-key (kbd "<tab>") 'indent-or-complete)))))

(if (not (eq  major-mode 'org-mode))
    (progn
      (define-key global-map "\t" 'indent-or-complete) ;; with this you have to force tab (C-q-tab) to insert a tab after a word
      (define-key global-map [S-tab] 'my-unindent)
      (define-key global-map [C-S-tab] 'my-unindent)))

;; mac and pc users would like selecting text this way
(defun dave-shift-mouse-select (event)
 "Set the mark and then move point to the position clicked on with
 the mouse. This should be bound to a mouse click event type."
 (interactive "e")
 (mouse-minibuffer-check event)
 (if mark-active (exchange-point-and-mark))
 (set-mark-command nil)
 ;; Use event-end in case called from mouse-drag-region.
 ;; If EVENT is a click, event-end and event-start give same value.
 (posn-set-point (event-end event)))

;; be aware that this overrides the function for picking a font. you can still call the command
;; directly from the minibufer doing: "M-x mouse-set-font"
(define-key global-map [S-down-mouse-1] 'dave-shift-mouse-select)

;; to use in into emacs for  unix I  needed this instead
; define-key global-map [S-mouse-1] 'dave-shift-mouse-select)

;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; this final line is only necessary to escape the *scratch* fundamental-mode
;; and let this demonstration work
(text-mode)

NOTE:
I revised the code and modified some things. Especially the function ‘my-unindent” in order to add some sophistication on how the shifting backwards now occurs. The main improvement is that deleting backwards will never lead to jumping to the previous line (and removing words from there) . Here’s how the movement to left is done when pressing Shift + tab:

  • killing word by word, if the insertion point is at the right of any text.
  • stepping back, in increments (set by the variable my-tab-width), BUT ALWAYS stopping at the beginning of the line when the cursor is at the left of any indented text.

You can see how much simpler the function looked before I decided to better it:

(defun my-unindent()
  "Unindent line, or block if it's a region selected"
  (interactive)
  (if mark-active
      (unindent-block)
    (if(not(bolp))(delete-backward-char 2 ))))

ADDENDUM:
Having discovered that C-i means “tab” (remember that in this setup, tab is associated with the function ‘indent-or-complete) I’m changing “M-i”, to act the same as “Shift-tab” does.
Might come as a good alternative, one supposed to be less stressful on the wrists.
(“Meta + i” comes originally bound to ‘tab-to-tab-stop)

(global-set-key (kbd "M-i") 'my-unindent)
Advertisements

8 Comments

  1. You might consider refactoring your code to use indent-rigidly, which is capable of shifting regions right and left.

    • Thank you for stopping by, Ian.
      The code actually uses indent-rigidly for doing the shifting of columns. In what other way do you mean that function could be used?
      The code certainly could be improved. For example you could see than I didn’t know how to pass the negative variable my-tab-width as a negative argument to the function shift-region. (I had to define the same value again, which I know is lame for something so trivial, any clue on that one?)

  2. Oh, sorry, never mind about the negative argument!. It took me a minute to figure that parenthesis should be put around them. I conveniently updated the code:
    (shift-region (- my-tab-width)) so the shifting, to both right and left, will depend on the variable my-tab-width, which a user could change anytime with eval-expression doing M-: RET (setq my-tab-width NUM) RET

  3. Thanks for this, you made my RoR editing much more pleasant!

  4. This looks great, NTemacs on windows it does not work for me though, it says it’s indenting, but nothing happens. Any ideas?

    • Hi Aidan,
      I used this code in emacs 22.1 and emacs 23 on Windows and Unix platforms but I don’t have NTemacs installed to check your problem now.
      Does “shift-region” work? Try selecting text and use the command eval-expression to see if that moves the text at least, something like, “M-:” and then: “(shift-region 4)”

      • Hey Ignacio!

        Thanks for the advice I got it working, turns out there was just a couple bad characters when I pasted it in the first time.

        Thanks a bunch for the great library!

  5. You’re welcome, I am glad you like it!


One Trackback/Pingback

  1. […] at it I decided to also rebind the TAB functionality so I could add readability to my queries indenting them as […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: