1 ;;; multi-web-mode.el --- multiple major mode support for web editing
3 ;; Copyright (C) 2012 Fabián Ezequiel Gallina.
5 ;; Author: Fabián E. Gallina <fabian@anue.biz>
6 ;; URL: https://github.com/fgallina/multi-web-mode
9 ;; Keywords: convenience, languages, wp
11 ;; This file is part of Multi Web Mode
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.
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.
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/>.
28 ;; Multi Web Mode is a minor mode wich makes web editing in Emacs much easier.
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.
41 (defvar multi-web-mode))
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)
49 "Keymaps for command `multi-web-mode'.")
51 (defvar mweb-mode-hook nil
52 "Hooks to run when command `multi-web-mode' is initialized.")
54 (defvar mweb-extra-indentation 0
55 "Extra indentation for chunks.
56 Automatically calculated when the major mode has changed.")
58 (defcustom mweb-default-major-mode nil
59 "Default major mode when not in chunk."
61 :group 'multi-web-mode
64 (defcustom mweb-filename-extensions
66 "File extensions that trigger activation.
68 This is an example configuration:
69 '(\"php\" \"htm\" \"html\" \"ctp\" \"phtml\" \"php4\" \"php5\")"
71 :group 'multi-web-mode
72 :safe #'(lambda (extensions)
74 (dolist (ext extensions)
75 (when (not (stringp ext))
78 ;; What you read in the docstring translates to:
79 ;; ((php-mode "<\\?php\\|<\\? \\|<\\?=" "\\?>")
80 ;; (js-mode "<script +\\(type=\"text/javascript\"\\|language=\"javascript\"\\)[^>]*>" "</script>")
81 ;; (css-mode "<style +type=\"text/css\"[^>]*>" "</style>"))
84 "Tags enabled for command `multi-web-mode'.
85 This var is an alist on which each element has the form
86 \(major-mode \"open tag regex\" \"close tag regex\").
88 This is an example configuration:
90 \(\(php-mode \"<\\\\?php\\|<\\\\? \\|<\\\\?=\" \"\\\\?>\")
92 \"<script +\\\\(type=\\\"text/javascript\\\"\\\\
93 |language=\\\"javascript\\\"\\\\)[^>]*>\"
95 \(css-mode \"<style +type=\\\"text/css\\\"[^>]*>\" \"</style>\"))"
96 :type '(repeat (symbol string string))
97 :group 'multi-web-mode
98 :safe #'(lambda (tags)
102 (not (symbolp (mweb-get-tag-attr tag 'mode)))
103 (not (stringp (mweb-get-tag-attr tag 'open)))
104 (not (stringp (mweb-get-tag-attr tag 'close))))
105 (throw 'fail t)))))))
107 (defcustom mweb-submode-indent-offset 2
108 "Indentation offset for code inside chunks."
110 :group 'multi-web-mode
113 (defcustom mweb-ignored-commands
117 'yas/next-field-or-maybe-expand
120 'isearch-other-control-char)
121 "Commands that prevent changing the major mode."
122 :type '(repeat symbol)
123 :group 'multi-web-mode
124 :safe #'(lambda (names)
127 (when (not (symbolp name))
128 (throw 'fail t)))))))
130 (defun mweb-get-tag-attr (tag attribute)
132 ATTRIBUTE values can be 'mode to get the tag's major mode or
133 'open/'close to get the open/close regexp respectively."
137 (close (caddr tag))))
139 (defun mweb-get-tag (tag-major-mode)
140 "Return tag from `mweb-tags' matching TAG-MAJOR-MODE."
141 (assoc tag-major-mode mweb-tags))
143 (defun mweb--looking-at-tag (&optional type)
144 "Return non-nil if pointer is looking at an open or close tag.
146 Possible values of TYPE are:
147 * nil: to check if point is looking at an open or close tag.
148 * 'open: to check if point is looking at an open tag
149 * 'close: to check if point is looking at a close tag"
156 (back-to-indentation)
157 (while (and (< index (length mweb-tags))
159 (setq open-tag (mweb-get-tag-attr (elt mweb-tags index) 'open))
160 (setq close-tag (mweb-get-tag-attr (elt mweb-tags index) 'close))
162 (open (setq tag-regexp open-tag))
163 (close (setq tag-regexp close-tag))
164 (otherwise (setq tag-regexp (concat open-tag "\\|" close-tag))))
165 (when (looking-at tag-regexp)
167 (setq index (+ 1 index))))
170 (defsubst mweb-looking-at-open-tag-p ()
171 "Return t if point is looking at an open tag."
172 (mweb--looking-at-tag 'open))
174 (defsubst mweb-looking-at-close-tag-p ()
175 "Return t if point is looking at a close tag."
176 (mweb--looking-at-tag 'close))
178 (defsubst mweb-looking-at-tag-p ()
179 "Return t if point is looking at an open or close tag."
180 (mweb--looking-at-tag))
182 (defun mweb-change-major-mode ()
183 "Call the appropriate major mode for the pointed chunk.
184 If the current `major-mode' is the correct one it doesn't funcall the
185 major mode and returns nil, otherwise changes the `major-mode' and
186 returns a symbol with its name."
187 (let ((closest-chunk-point 0)
188 (closest-chunk-mode mweb-default-major-mode)
192 (dolist (tag mweb-tags)
193 (setq result (mweb-closest-starting-chunk-point tag))
194 (when (and (integerp result)
195 (<= closest-chunk-point result))
196 (setq closest-chunk-point result)
197 (setq closest-chunk-mode (mweb-get-tag-attr tag 'mode)))))
198 (when (not (equal closest-chunk-mode major-mode))
199 (funcall closest-chunk-mode)
200 closest-chunk-mode)))
202 (defun mweb-change-indent-line-function ()
203 "Set the correct value for `indent-line-function'.
204 Depending of `major-mode'."
205 (when (not (equal major-mode mweb-default-major-mode))
206 (setq indent-line-function 'mweb-indent-line)))
208 (defun mweb-closest-starting-chunk-point (tag)
209 "Return the point of the closest chunk for TAG.
210 Where TAG is one of the tags contained in the `mweb-tags'
211 list. If the chunk is not found then it returns nil."
215 (setq open-tag (re-search-backward (mweb-get-tag-attr tag 'open) nil t)))
217 (setq close-tag (re-search-backward (mweb-get-tag-attr tag 'close) nil t)))
218 (cond ((not open-tag)
223 ((> open-tag close-tag)
226 (defun mweb-multiple-chunks-p ()
227 "Check if multiple chunks exist in the current buffer."
231 (goto-char (point-min))
232 (re-search-forward "[^\s\t\n]" nil t)
233 (or (not (mweb-looking-at-open-tag-p))
235 (dolist (tag mweb-tags)
236 (when (re-search-forward (mweb-get-tag-attr tag 'close) nil t)
237 (throw 'break (not (not (re-search-forward "[^\s\t\n]" nil t)))))))))))
239 (defun mweb-update-context ()
240 "Update extra indentation value for chunks."
241 (let ((changed-major-mode (mweb-change-major-mode)))
242 (if (and changed-major-mode
243 (not (equal major-mode mweb-default-major-mode)))
244 (setq mweb-extra-indentation (mweb-calculate-indentation))
245 (setq mweb-extra-indentation 0)))
246 (mweb-change-indent-line-function))
248 (defun mweb-calculate-indentation ()
249 "Calculate the correct indentation given previous submode."
250 (let ((indentation 0)
252 (changed-major-mode major-mode)
253 (buffer-modified-flag (buffer-modified-p)))
257 (mweb-goto-current-mode-open-tag)
258 (if (progn (mweb-forward-nonblank-line -1) (bobp))
259 (if (mweb-multiple-chunks-p)
261 (setq indentation (- mweb-submode-indent-offset)))
263 (setq prev-line-pos (point-marker))
265 (mweb-change-major-mode)
266 (indent-according-to-mode)
267 (setq indentation (current-indentation))
268 (delete-region prev-line-pos (line-end-position))))
269 (funcall changed-major-mode)
270 (set-buffer-modified-p buffer-modified-flag)
273 (defun mweb-mark-whole-buffer ()
274 "Multi-web-mode's version of `mark-whole-buffer'."
277 (goto-char (point-min))
278 (mweb-change-major-mode)
279 (push-mark (point-max) nil t))
281 (defun mweb-indent-line ()
282 "Function to use when indenting a submode line."
284 ;; Yes, indent according to mode will do what we expect
285 (setq mweb-extra-indentation (mweb-calculate-indentation))
286 (if (not (mweb-looking-at-open-tag-p))
287 (if (not (mweb-looking-at-close-tag-p))
288 ;; Normal indentation
289 (if (equal major-mode mweb-default-major-mode)
290 (indent-according-to-mode)
293 (delete-horizontal-space)
295 (indent-according-to-mode)
296 (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset)))))
297 ;; Close tag indentation routine
298 (let ((open-tag-indentation 0))
300 (mweb-goto-current-mode-open-tag)
301 (setq open-tag-indentation (current-indentation)))
303 (delete-horizontal-space)
304 (indent-to open-tag-indentation)))
305 ;; Open tag indentation routine
307 (delete-horizontal-space)
309 (delete-horizontal-space)
311 (mweb-update-context)
312 (indent-according-to-mode)
313 (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset))
315 (and (bolp) (back-to-indentation)))
317 (defun mweb-indent-region (start end)
318 "Indent a region taking care of chunks.
319 This routine considers the relative position of the chunks within
320 the buffer. It follows the same filosophy than
321 `mweb-indent-line-forward' because that function is what is used
322 to indent the chunks which are not for the default major mode.
323 Called from a program, START and END specify the region to indent."
325 (let ((delete-active-region nil)
329 (setq end (point-marker))
331 (mweb-change-major-mode)
332 (or (bolp) (forward-line 1))
333 (while (< (point) end)
334 (mweb-update-context)
335 (if (equal major-mode mweb-default-major-mode)
336 (indent-according-to-mode)
339 (move-marker end nil))))
341 (defun mweb-get-current-mode-tag-point (type)
342 "Gets the point marker of current chunk's open/close tag.
344 The TYPE argument can be a 'open for the open tag or 'close for
346 (when (not (equal major-mode mweb-default-major-mode))
351 (re-search-func (if (equal type 'open)
353 're-search-forward)))
354 (while (and (< index (length mweb-tags))
356 (setq tag (elt mweb-tags index))
357 (when (or (equal (mweb-get-tag-attr tag 'mode) major-mode)
358 (equal major-mode mweb-default-major-mode))
361 (if (looking-at (mweb-get-tag-attr tag type))
363 (back-to-indentation)
364 (setq result (point)))
365 (setq result (funcall re-search-func
366 (mweb-get-tag-attr tag type)
368 (setq index (+ 1 index)))
371 (defun mweb-goto-current-mode-open-tag ()
372 "Move the point to the open tag of the current chunk."
374 (let ((tag-point (mweb-get-current-mode-tag-point 'open)))
376 (goto-char tag-point))))
378 (defun mweb-goto-current-mode-close-tag ()
379 "Move the point to the close tag of the current chunk."
381 (let ((tag-point (mweb-get-current-mode-tag-point 'close)))
383 (goto-char tag-point))))
385 (defun mweb-set-extra-indentation (number)
386 "Set the new value for `mweb-extra-indentation' to NUMBER."
387 (interactive "nNew mweb-extra-indentation value: ")
388 (setq mweb-extra-indentation number)
389 (message "mweb-extra-indentation = %d" mweb-extra-indentation))
391 (defun mweb-set-default-major-mode (major-mode)
392 "Set the new value for `mweb-default-major-mode' to MAJOR-MODE."
393 (interactive "CNew default major mode: ")
394 (setq mweb-default-major-mode major-mode)
395 (mweb-change-major-mode)
396 (message "mweb-default-major-mode = %s" mweb-default-major-mode))
398 (defun mweb-forward-nonblank-line (&optional number)
399 "Move the cursor to the next/previous non blank line.
401 When NUMBER is positive it moves forward and when is negative
409 (forward-line number)
410 (while (and (equal (mweb-get-current-line-trimmed-contents) "")
411 (not (or (bobp) (eobp))))
412 (forward-line number)))
414 (defun mweb-get-current-line-trimmed-contents ()
415 "Gets the contents of the current line.
416 It trims all space characters at the beginning and end of the line."
422 (setq start-point (point))
424 (setq end-point (point))
425 (setq contents (buffer-substring start-point end-point))
426 (when (string-match "[ \t]*$" contents)
427 (setq contents (replace-match "" nil nil contents)))
428 (when (string-match "^[ \t]*" contents)
429 (setq contents (replace-match "" nil nil contents))))
432 (defun mweb-post-command-hook ()
433 "The function which is appended to the `post-command-hook'."
434 (when (and multi-web-mode
435 (not (region-active-p))
436 (not (member last-command mweb-ignored-commands)))
437 (mweb-update-context)))
439 (defun mweb-enable ()
440 "Setup the minor mode."
441 (set (make-local-variable 'indent-region-function)
443 (make-local-variable 'indent-line-function)
444 (add-hook 'post-command-hook 'mweb-post-command-hook)
445 (assq-delete-all 'multi-web-mode minor-mode-map-alist)
446 (push (cons 'multi-web-mode mweb-mode-map)
447 minor-mode-map-alist)
448 (run-hooks 'mweb-mode-hook))
450 (defun mweb-disable ()
451 "Disable the minor mode."
452 (assq-delete-all 'multi-web-mode minor-mode-map-alist))
455 (define-minor-mode multi-web-mode
456 "Enables the multi web mode chunk detection and indentation"
457 :lighter " Multi-Web" :group 'convenience
462 (defun multi-web-mode-maybe ()
463 "Used to turn on the globalized minor mode."
465 (file-name-extension (or buffer-file-name ""))
466 mweb-filename-extensions)
469 (define-globalized-minor-mode multi-web-global-mode
470 multi-web-mode multi-web-mode-maybe
471 :group 'multi-web-mode
472 :require 'multi-web-mode)
474 (provide 'multi-web-mode)
475 ;;; multi-web-mode.el ends here