1;; -*-Emacs-Lisp-*-
2;;
3;; ruby-electric.el --- electric editing commands for ruby files
4;;
5;; Copyright (C) 2005 by Dee Zsombor <dee dot zsombor at gmail dot com>.
6;; Released under same license terms as Ruby.
7;;
8;; Due credit: this work was inspired by a code snippet posted by
9;; Frederick Ros at http://rubygarden.org/ruby?EmacsExtensions.
10;;
11;; Following improvements where added:
12;;
13;;       - handling of strings of type 'here document'
14;;       - more keywords, with special handling for 'do'
15;;       - packaged into a minor mode
16;;
17;; Usage:
18;;
19;;    0) copy ruby-electric.el into directory where emacs can find it.
20;;
21;;    1) modify your startup file (.emacs or whatever) by adding
22;;       following line:
23;;
24;;            (require 'ruby-electric)
25;;
26;;       note that you need to have font lock enabled beforehand.
27;;
28;;    2) toggle Ruby Electric Mode on/off with ruby-electric-mode.
29;;
30;; Changelog:
31;;
32;;  2005/Jan/14: inserts matching pair delimiters like {, [, (, ', ",
33;;  ' and | .
34;;
35;;  2005/Jan/14: added basic Custom support for configuring keywords
36;;  with electric closing.
37;;
38;;  2005/Jan/18: more Custom support for configuring characters for
39;;  which matching expansion should occur.
40;;
41;;  2005/Jan/18: no longer uses 'looking-back' or regexp character
42;;  classes like [:space:] since they are not implemented on XEmacs.
43;;
44;;  2005/Feb/01: explicitly provide default argument of 1 to
45;;  'backward-word' as it requires it on Emacs 21.3
46;;
47;;  2005/Mar/06: now stored inside ruby CVS; customize pages now have
48;;  ruby as parent; cosmetic fixes.
49
50
51(require 'ruby-mode)
52
53(defgroup ruby-electric nil
54  "Minor mode providing electric editing commands for ruby files"
55  :group 'ruby)
56
57(defconst ruby-electric-expandable-do-re
58  "do\\s-$")
59
60(defconst ruby-electric-expandable-bar
61  "\\s-\\(do\\|{\\)\\s-+|")
62
63(defvar ruby-electric-matching-delimeter-alist
64  '((?\[ . ?\])
65    (?\( . ?\))
66    (?\' . ?\')
67    (?\` . ?\`)
68    (?\" . ?\")))
69
70(defcustom ruby-electric-simple-keywords-re
71  (regexp-opt '("def" "if" "class" "module" "unless" "case" "while" "do" "until" "for" "begin") t)
72  "*Regular expresion matching keywords for which closing 'end'
73is to be inserted."
74  :type 'regexp :group 'ruby-electric)
75
76(defcustom ruby-electric-expand-delimiters-list '(all)
77  "*List of contexts where matching delimiter should be
78inserted. The word 'all' will do all insertions."
79  :type '(set :extra-offset 8
80              (const :tag "Everything" all )
81              (const :tag "Curly brace" ?\{ )
82              (const :tag "Square brace" ?\[ )
83              (const :tag "Round brace" ?\( )
84              (const :tag "Quote" ?\' )
85              (const :tag "Double quote" ?\" )
86              (const :tag "Back quote" ?\` )
87              (const :tag "Vertical bar" ?\| ))
88  :group 'ruby-electric)
89
90(defcustom ruby-electric-newline-before-closing-bracket nil
91  "*Controls whether a newline should be inserted before the
92closing bracket or not."
93  :type 'boolean :group 'ruby-electric)
94
95(define-minor-mode ruby-electric-mode
96  "Toggle Ruby Electric minor mode.
97With no argument, this command toggles the mode.  Non-null prefix
98argument turns on the mode.  Null prefix argument turns off the
99mode.
100
101When Ruby Electric mode is enabled, an indented 'end' is
102heuristicaly inserted whenever typing a word like 'module',
103'class', 'def', 'if', 'unless', 'case', 'until', 'for', 'begin',
104'do'. Simple, double and back quotes as well as braces are paired
105auto-magically. Expansion does not occur inside comments and
106strings. Note that you must have Font Lock enabled."
107  ;; initial value.
108  nil
109  ;;indicator for the mode line.
110  " REl"
111  ;;keymap
112  ruby-mode-map
113  (ruby-electric-setup-keymap))
114
115(defun ruby-electric-setup-keymap()
116  (define-key ruby-mode-map " " 'ruby-electric-space)
117  (define-key ruby-mode-map "{" 'ruby-electric-curlies)
118  (define-key ruby-mode-map "(" 'ruby-electric-matching-char)
119  (define-key ruby-mode-map "[" 'ruby-electric-matching-char)
120  (define-key ruby-mode-map "\"" 'ruby-electric-matching-char)
121  (define-key ruby-mode-map "\'" 'ruby-electric-matching-char)
122  (define-key ruby-mode-map "|" 'ruby-electric-bar))
123
124(defun ruby-electric-space (arg)
125  (interactive "P")
126  (self-insert-command (prefix-numeric-value arg))
127  (if (ruby-electric-space-can-be-expanded-p)
128      (save-excursion
129        (ruby-indent-line t)
130        (newline)
131        (ruby-insert-end))))
132
133(defun ruby-electric-code-at-point-p()
134  (and ruby-electric-mode
135       (let* ((properties (text-properties-at (point))))
136         (and (null (memq 'font-lock-string-face properties))
137              (null (memq 'font-lock-comment-face properties))))))
138
139(defun ruby-electric-string-at-point-p()
140  (and ruby-electric-mode
141       (consp (memq 'font-lock-string-face (text-properties-at (point))))))
142
143(defun ruby-electric-is-last-command-char-expandable-punct-p()
144  (or (memq 'all ruby-electric-expand-delimiters-list)
145      (memq last-command-event ruby-electric-expand-delimiters-list)))
146
147(defun ruby-electric-space-can-be-expanded-p()
148  (if (ruby-electric-code-at-point-p)
149      (let* ((ruby-electric-keywords-re
150              (concat ruby-electric-simple-keywords-re "\\s-$"))
151             (ruby-electric-single-keyword-in-line-re
152              (concat "\\s-*" ruby-electric-keywords-re)))
153        (save-excursion
154          (backward-word 1)
155          (or (looking-at ruby-electric-expandable-do-re)
156              (and (looking-at ruby-electric-keywords-re)
157                   (not (string= "do" (match-string 1)))
158                   (progn
159                     (beginning-of-line)
160                     (looking-at ruby-electric-single-keyword-in-line-re))))))))
161
162
163(defun ruby-electric-curlies(arg)
164  (interactive "P")
165  (self-insert-command (prefix-numeric-value arg))
166  (if (ruby-electric-is-last-command-char-expandable-punct-p)
167      (cond ((ruby-electric-code-at-point-p)
168             (insert " ")
169             (save-excursion
170               (if ruby-electric-newline-before-closing-bracket
171                   (progn
172                     (newline)
173                     (insert "}")
174                     (ruby-indent-line t))
175                 (insert "}"))))
176            ((ruby-electric-string-at-point-p)
177             (if (eq last-command-event ?{)
178                 (save-excursion
179                   (backward-char 1)
180                   (or (char-equal ?\# (preceding-char))
181                       (insert "#"))
182                   (forward-char 1)
183                   (insert "}")))))))
184
185(defun ruby-electric-matching-char(arg)
186  (interactive "P")
187  (self-insert-command (prefix-numeric-value arg))
188  (and (ruby-electric-is-last-command-char-expandable-punct-p)
189       (ruby-electric-code-at-point-p)
190       (save-excursion
191         (insert (cdr (assoc last-command-event
192                             ruby-electric-matching-delimeter-alist))))))
193
194(defun ruby-electric-bar(arg)
195  (interactive "P")
196  (self-insert-command (prefix-numeric-value arg))
197  (and (ruby-electric-is-last-command-char-expandable-punct-p)
198       (ruby-electric-code-at-point-p)
199       (and (save-excursion (re-search-backward ruby-electric-expandable-bar nil t))
200            (= (point) (match-end 0))) ;looking-back is missing on XEmacs
201       (save-excursion
202         (insert "|"))))
203
204
205(provide 'ruby-electric)
206