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