PageRenderTime 64ms CodeModel.GetById 18ms app.highlight 16ms RepoModel.GetById 25ms app.codeStats 0ms

/multi-web-mode.el

http://github.com/fgallina/multi-web-mode
Emacs Lisp | 469 lines | 382 code | 54 blank | 33 comment | 20 complexity | cc05f1e1df8f583e2bacd849e8dab08f MD5 | raw file
  1;;; multi-web-mode.el --- multiple major mode support for web editing
  2
  3;; Copyright (C) 2012 Fabián Ezequiel Gallina.
  4
  5;; Author: Fabián E. Gallina <fabian@anue.biz>
  6;; URL: https://github.com/fgallina/multi-web-mode
  7;; Version: 0.1
  8;; Created: Feb 2009
  9;; Keywords: convenience, languages, wp
 10
 11;; This file is part of Multi Web Mode
 12
 13;; Multi Web Mode is free software: you can redistribute it and/or
 14;; modify it under the terms of the GNU General Public License as
 15;; published by the Free Software Foundation, either version 3 of the
 16;; License, or (at your option) any later version.
 17
 18;; Multi Web Mode is distributed in the hope that it will be useful,
 19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 21;; General Public License for more details.
 22
 23;; You should have received a copy of the GNU General Public License
 24;; along with Multi Web Mode. If not, see <http://www.gnu.org/licenses/>.
 25
 26;;; Commentary:
 27
 28;; Multi Web Mode is a minor mode wich makes web editing in Emacs much easier.
 29
 30;; Basically what it does is select the appropriate major mode
 31;; automatically when you move the point and also calculates the
 32;; correct indentation of chunks according to the indentation of the
 33;; most relevant major mode.
 34
 35;;
 36
 37;;; Code:
 38
 39(eval-when-compile
 40  (require 'cl)
 41  (defvar multi-web-mode))
 42
 43(defvar mweb-mode-map
 44  (let ((mweb-mode-map (make-sparse-keymap)))
 45    (define-key mweb-mode-map (kbd "M-<f11>") 'mweb-set-default-major-mode)
 46    (define-key mweb-mode-map (kbd "M-<f12>") 'mweb-set-extra-indentation)
 47    (define-key mweb-mode-map [remap mark-whole-buffer] 'mweb-mark-whole-buffer)
 48    mweb-mode-map)
 49  "Keymaps for command `multi-web-mode'.")
 50
 51(defvar mweb-mode-hook nil
 52  "Hooks to run when command `multi-web-mode' is initialized.")
 53
 54(defvar mweb-extra-indentation 0
 55  "Extra indentation for chunks.
 56Automatically calculated when the major mode has changed.")
 57
 58(defcustom mweb-default-major-mode nil
 59  "Default major mode when not in chunk."
 60  :type 'symbol
 61  :group 'multi-web-mode
 62  :safe 'symbolp)
 63
 64(defcustom mweb-filename-extensions
 65  nil
 66  "File extensions that trigger activation.
 67
 68This is an example configuration:
 69'(\"php\" \"htm\" \"html\" \"ctp\" \"phtml\" \"php4\" \"php5\")"
 70  :type '(list string)
 71  :group 'multi-web-mode
 72  :safe #'(lambda (extensions)
 73            (not (catch 'fail
 74                   (dolist (ext extensions)
 75                     (when (not (stringp ext))
 76                       (throw 'fail t)))))))
 77
 78(defcustom mweb-tags
 79  nil
 80  "Tags enabled for command `multi-web-mode'.
 81This var is an alist on which each element has the form
 82\(major-mode \"open tag regex\" \"close tag regex\").
 83
 84This is an example configuration:
 85
 86\(\(php-mode \"<\\\\?php\\|<\\\\? \\|<\\\\?=\" \"\\\\?>\")
 87 \(js-mode \"<script[^>]*>\" \"</script>\")
 88 \(css-mode \"<style[^>]*>\" \"</style>\"))"
 89  :type '(repeat (symbol string string))
 90  :group 'multi-web-mode
 91  :safe #'(lambda (tags)
 92            (not (catch 'fail
 93                   (dolist (tag tags)
 94                     (when (or
 95                            (not (symbolp (mweb-get-tag-attr tag 'mode)))
 96                            (not (stringp (mweb-get-tag-attr tag 'open)))
 97                            (not (stringp (mweb-get-tag-attr tag 'close))))
 98                       (throw 'fail t)))))))
 99
100(defcustom mweb-submode-indent-offset 2
101  "Indentation offset for code inside chunks."
102  :type 'integer
103  :group 'multi-web-mode
104  :safe 'integerp)
105
106(defcustom mweb-ignored-commands
107  (list
108   'undo
109   'yas/expand
110   'yas/next-field-or-maybe-expand
111   'isearch-forward
112   'isearch-backward
113   'isearch-other-control-char)
114  "Commands that prevent changing the major mode."
115  :type '(repeat symbol)
116  :group 'multi-web-mode
117  :safe #'(lambda (names)
118            (not (catch 'fail
119                   (dolist (name names)
120                     (when (not (symbolp name))
121                       (throw 'fail t)))))))
122
123(defun mweb-get-tag-attr (tag attribute)
124  "Get TAG ATTRIBUTE.
125ATTRIBUTE values can be 'mode to get the tag's major mode or
126'open/'close to get the open/close regexp respectively."
127  (case attribute
128    (mode (car tag))
129    (open (cadr tag))
130    (close (caddr tag))))
131
132(defun mweb-get-tag (tag-major-mode)
133  "Return tag from `mweb-tags' matching TAG-MAJOR-MODE."
134  (assoc tag-major-mode mweb-tags))
135
136(defun mweb--looking-at-tag (&optional type)
137  "Return non-nil if pointer is looking at an open or close tag.
138
139Possible values of TYPE are:
140    * nil: to check if point is looking at an open or close tag.
141    * 'open: to check if point is looking at an open tag
142    * 'close: to check if point is looking at a close tag"
143  (let ((index 0)
144        (looking)
145        (open-tag)
146        (close-tag)
147        (tag-regexp))
148    (save-excursion
149      (back-to-indentation)
150      (while (and (< index (length mweb-tags))
151                  (not looking))
152        (setq open-tag (mweb-get-tag-attr (elt mweb-tags index) 'open))
153        (setq close-tag (mweb-get-tag-attr (elt mweb-tags index) 'close))
154        (case type
155          (open (setq tag-regexp open-tag))
156          (close (setq tag-regexp close-tag))
157          (otherwise (setq tag-regexp (concat open-tag "\\|" close-tag))))
158        (when (looking-at tag-regexp)
159          (setq looking t))
160        (setq index (+ 1 index))))
161    looking))
162
163(defsubst mweb-looking-at-open-tag-p ()
164  "Return t if point is looking at an open tag."
165  (mweb--looking-at-tag 'open))
166
167(defsubst mweb-looking-at-close-tag-p ()
168  "Return t if point is looking at a close tag."
169  (mweb--looking-at-tag 'close))
170
171(defsubst mweb-looking-at-tag-p ()
172  "Return t if point is looking at an open or close tag."
173  (mweb--looking-at-tag))
174
175(defun mweb-change-major-mode ()
176  "Call the appropriate major mode for the pointed chunk.
177If the current `major-mode' is the correct one it doesn't funcall the
178major mode and returns nil, otherwise changes the `major-mode' and
179returns a symbol with its name."
180  (let ((closest-chunk-point 0)
181        (closest-chunk-mode mweb-default-major-mode)
182        (result nil))
183    (save-restriction
184      (widen)
185      (dolist (tag mweb-tags)
186        (setq result (mweb-closest-starting-chunk-point tag))
187        (when (and (integerp result)
188                   (<= closest-chunk-point result))
189          (setq closest-chunk-point result)
190          (setq closest-chunk-mode (mweb-get-tag-attr tag 'mode)))))
191    (when (not (equal closest-chunk-mode major-mode))
192      (funcall closest-chunk-mode)
193      closest-chunk-mode)))
194
195(defun mweb-change-indent-line-function ()
196  "Set the correct value for `indent-line-function'.
197Depending of `major-mode'."
198  (when (not (equal major-mode mweb-default-major-mode))
199    (setq indent-line-function 'mweb-indent-line)))
200
201(defun mweb-closest-starting-chunk-point (tag)
202  "Return the point of the closest chunk for TAG.
203Where TAG is one of the tags contained in the `mweb-tags'
204list.  If the chunk is not found then it returns nil."
205  (let ((open-tag)
206        (close-tag))
207    (save-excursion
208      (setq open-tag (re-search-backward (mweb-get-tag-attr tag 'open) nil t)))
209    (save-excursion
210      (setq close-tag (re-search-backward (mweb-get-tag-attr tag 'close) nil t)))
211    (cond ((not open-tag)
212           nil)
213          ((and open-tag
214                (not close-tag))
215           open-tag)
216          ((> open-tag close-tag)
217           open-tag))))
218
219(defun mweb-multiple-chunks-p ()
220  "Check if multiple chunks exist in the current buffer."
221  (save-excursion
222    (save-restriction
223      (widen)
224      (goto-char (point-min))
225      (re-search-forward "[^\s\t\n]" nil t)
226      (or (not (mweb-looking-at-open-tag-p))
227          (catch 'break
228            (dolist (tag mweb-tags)
229              (when (re-search-forward (mweb-get-tag-attr tag 'close) nil t)
230                (throw 'break (not (not (re-search-forward "[^\s\t\n]" nil t)))))))))))
231
232(defun mweb-update-context ()
233  "Update extra indentation value for chunks."
234  (let ((changed-major-mode (mweb-change-major-mode)))
235    (if (and changed-major-mode
236             (not (equal major-mode mweb-default-major-mode)))
237        (setq mweb-extra-indentation (mweb-calculate-indentation))
238      (setq mweb-extra-indentation 0)))
239  (mweb-change-indent-line-function))
240
241(defun mweb-calculate-indentation ()
242  "Calculate the correct indentation given previous submode."
243  (let ((indentation 0)
244        (prev-line-pos)
245        (changed-major-mode major-mode)
246        (buffer-modified-flag (buffer-modified-p)))
247    (save-restriction
248      (widen)
249      (save-excursion
250        (mweb-goto-current-mode-open-tag)
251        (if (progn (mweb-forward-nonblank-line -1) (bobp))
252            (if (mweb-multiple-chunks-p)
253                (setq indentation 0)
254              (setq indentation (- mweb-submode-indent-offset)))
255          (end-of-line)
256          (setq prev-line-pos (point-marker))
257          (insert "\na")
258          (mweb-change-major-mode)
259          (indent-according-to-mode)
260          (setq indentation (current-indentation))
261          (delete-region prev-line-pos (line-end-position))))
262      (funcall changed-major-mode)
263      (set-buffer-modified-p buffer-modified-flag)
264      indentation)))
265
266(defun mweb-mark-whole-buffer ()
267  "Multi-web-mode's version of `mark-whole-buffer'."
268  (interactive)
269  (push-mark (point))
270  (goto-char (point-min))
271  (mweb-change-major-mode)
272  (push-mark (point-max) nil t))
273
274(defun mweb-indent-line ()
275  "Function to use when indenting a submode line."
276  (interactive)
277  ;; Yes, indent according to mode will do what we expect
278  (setq mweb-extra-indentation (mweb-calculate-indentation))
279  (if (not (mweb-looking-at-open-tag-p))
280      (if (not (mweb-looking-at-close-tag-p))
281          ;; Normal indentation
282          (if (equal major-mode mweb-default-major-mode)
283              (indent-according-to-mode)
284            (save-excursion
285              (beginning-of-line)
286              (delete-horizontal-space)
287	      (unless (bobp)
288		(indent-according-to-mode)
289		(indent-to (+ mweb-extra-indentation mweb-submode-indent-offset)))))
290        ;; Close tag indentation routine
291        (let ((open-tag-indentation 0))
292          (save-excursion
293            (mweb-goto-current-mode-open-tag)
294            (setq open-tag-indentation (current-indentation)))
295          (beginning-of-line)
296          (delete-horizontal-space)
297          (indent-to open-tag-indentation)))
298    ;; Open tag indentation routine
299    (beginning-of-line)
300    (delete-horizontal-space)
301    (insert "a")
302    (delete-horizontal-space)
303    (beginning-of-line)
304    (mweb-update-context)
305    (indent-according-to-mode)
306    (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset))
307    (delete-char 1))
308  (and (bolp) (back-to-indentation)))
309
310(defun mweb-indent-region (start end)
311  "Indent a region taking care of chunks.
312This routine considers the relative position of the chunks within
313the buffer.  It follows the same filosophy than
314`mweb-indent-line-forward' because that function is what is used
315to indent the chunks which are not for the default major mode.
316Called from a program, START and END specify the region to indent."
317  (interactive "r")
318  (let ((delete-active-region nil)
319        (line-end))
320    (save-excursion
321      (goto-char end)
322      (setq end (point-marker))
323      (goto-char start)
324      (mweb-change-major-mode)
325      (or (bolp) (forward-line 1))
326      (while (< (point) end)
327        (mweb-update-context)
328	(if (equal major-mode mweb-default-major-mode)
329	    (indent-according-to-mode)
330	  (mweb-indent-line))
331        (forward-line 1))
332      (move-marker end nil))))
333
334(defun mweb-get-current-mode-tag-point (type)
335  "Gets the point marker of current chunk's open/close tag.
336
337The TYPE argument can be a 'open for the open tag or 'close for
338the close tag."
339  (when (not (equal major-mode mweb-default-major-mode))
340    (let ((index 0)
341          (found nil)
342          (tag)
343          (result nil)
344          (re-search-func (if (equal type 'open)
345                              're-search-backward
346                            're-search-forward)))
347      (while (and (< index (length mweb-tags))
348                  (not found))
349        (setq tag (elt mweb-tags index))
350        (when (or (equal (mweb-get-tag-attr tag 'mode) major-mode)
351                  (equal major-mode mweb-default-major-mode))
352          (setq found t)
353          (save-excursion
354            (if (looking-at (mweb-get-tag-attr tag type))
355                (progn
356                  (back-to-indentation)
357                  (setq result (point)))
358              (setq result (funcall re-search-func
359                                    (mweb-get-tag-attr tag type)
360                                    nil t)))))
361        (setq index (+ 1 index)))
362      result)))
363
364(defun mweb-goto-current-mode-open-tag ()
365  "Move the point to the open tag of the current chunk."
366  (interactive)
367  (let ((tag-point (mweb-get-current-mode-tag-point 'open)))
368    (when tag-point
369      (goto-char tag-point))))
370
371(defun mweb-goto-current-mode-close-tag ()
372  "Move the point to the close tag of the current chunk."
373  (interactive)
374  (let ((tag-point (mweb-get-current-mode-tag-point 'close)))
375    (when tag-point
376      (goto-char tag-point))))
377
378(defun mweb-set-extra-indentation (number)
379  "Set the new value for `mweb-extra-indentation' to NUMBER."
380  (interactive "nNew mweb-extra-indentation value: ")
381  (setq mweb-extra-indentation number)
382  (message "mweb-extra-indentation = %d" mweb-extra-indentation))
383
384(defun mweb-set-default-major-mode (major-mode)
385  "Set the new value for `mweb-default-major-mode' to MAJOR-MODE."
386  (interactive "CNew default major mode: ")
387  (setq mweb-default-major-mode major-mode)
388  (mweb-change-major-mode)
389  (message "mweb-default-major-mode = %s" mweb-default-major-mode))
390
391(defun mweb-forward-nonblank-line (&optional number)
392  "Move the cursor to the next/previous non blank line.
393
394When NUMBER is positive it moves forward and when is negative
395it moves backwards."
396  (when (not number)
397    (setq number 1))
398  (when (> number 1)
399    (setq number 1))
400  (when (< number -1)
401    (setq number -1))
402  (forward-line number)
403  (while (and (equal (mweb-get-current-line-trimmed-contents) "")
404              (not (or (bobp) (eobp))))
405    (forward-line number)))
406
407(defun mweb-get-current-line-trimmed-contents ()
408  "Gets the contents of the current line.
409It trims all space characters at the beginning and end of the line."
410  (let ((start-point)
411        (end-point)
412        (contents))
413    (save-excursion
414      (beginning-of-line)
415      (setq start-point (point))
416      (end-of-line)
417      (setq end-point (point))
418      (setq contents (buffer-substring start-point end-point))
419      (when (string-match "[ \t]*$" contents)
420        (setq contents (replace-match "" nil nil contents)))
421      (when (string-match "^[ \t]*" contents)
422        (setq contents (replace-match "" nil nil contents))))
423    contents))
424
425(defun mweb-post-command-hook ()
426  "The function which is appended to the `post-command-hook'."
427  (when (and multi-web-mode
428             (not (region-active-p))
429             (not (member last-command mweb-ignored-commands)))
430    (mweb-update-context)))
431
432(defun mweb-enable ()
433  "Setup the minor mode."
434  (set (make-local-variable 'indent-region-function)
435       'mweb-indent-region)
436  (make-local-variable 'indent-line-function)
437  (add-hook 'post-command-hook 'mweb-post-command-hook)
438  (assq-delete-all 'multi-web-mode minor-mode-map-alist)
439  (push (cons 'multi-web-mode mweb-mode-map)
440        minor-mode-map-alist)
441  (run-hooks 'mweb-mode-hook))
442
443(defun mweb-disable ()
444  "Disable the minor mode."
445  (assq-delete-all 'multi-web-mode minor-mode-map-alist))
446
447;;;###autoload
448(define-minor-mode multi-web-mode
449  "Enables the multi web mode chunk detection and indentation"
450  :lighter " Multi-Web" :group 'convenience
451  (if multi-web-mode
452      (mweb-enable)
453    (mweb-disable)))
454
455(defun multi-web-mode-maybe ()
456  "Used to turn on the globalized minor mode."
457  (when (member
458         (file-name-extension (or buffer-file-name ""))
459         mweb-filename-extensions)
460    (multi-web-mode 1)))
461
462;;;###autoload
463(define-globalized-minor-mode multi-web-global-mode
464  multi-web-mode multi-web-mode-maybe
465  :group 'multi-web-mode
466  :require 'multi-web-mode)
467
468(provide 'multi-web-mode)
469;;; multi-web-mode.el ends here