;;; multi-web-mode.el --- multiple major mode support for web editing ;; Copyright (C) 2012 Fabián Ezequiel Gallina. ;; Author: Fabián E. Gallina ;; URL: https://github.com/fgallina/multi-web-mode ;; Version: 0.1 ;; Created: Feb 2009 ;; Keywords: convenience, languages, wp ;; This file is part of Multi Web Mode ;; Multi Web Mode is free software: you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation, either version 3 of the ;; License, or (at your option) any later version. ;; Multi Web Mode is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with Multi Web Mode. If not, see . ;;; Commentary: ;; Multi Web Mode is a minor mode wich makes web editing in Emacs much easier. ;; Basically what it does is select the appropriate major mode ;; automatically when you move the point and also calculates the ;; correct indentation of chunks according to the indentation of the ;; most relevant major mode. ;; ;;; Code: (eval-when-compile (require 'cl) (defvar multi-web-mode)) (defvar mweb-mode-map (let ((mweb-mode-map (make-sparse-keymap))) (define-key mweb-mode-map (kbd "M-") 'mweb-set-default-major-mode) (define-key mweb-mode-map (kbd "M-") 'mweb-set-extra-indentation) (define-key mweb-mode-map [remap mark-whole-buffer] 'mweb-mark-whole-buffer) mweb-mode-map) "Keymaps for command `multi-web-mode'.") (defvar mweb-mode-hook nil "Hooks to run when command `multi-web-mode' is initialized.") (defvar mweb-extra-indentation 0 "Extra indentation for chunks. Automatically calculated when the major mode has changed.") (defcustom mweb-default-major-mode nil "Default major mode when not in chunk." :type 'symbol :group 'multi-web-mode :safe 'symbolp) (defcustom mweb-filename-extensions nil "File extensions that trigger activation. This is an example configuration: '(\"php\" \"htm\" \"html\" \"ctp\" \"phtml\" \"php4\" \"php5\")" :type '(list string) :group 'multi-web-mode :safe #'(lambda (extensions) (not (catch 'fail (dolist (ext extensions) (when (not (stringp ext)) (throw 'fail t))))))) ;; What you read in the docstring translates to: ;; ((php-mode "<\\?php\\|<\\? \\|<\\?=" "\\?>") ;; (js-mode "") ;; (css-mode "")) (defcustom mweb-tags nil "Tags enabled for command `multi-web-mode'. This var is an alist on which each element has the form \(major-mode \"open tag regex\" \"close tag regex\"). This is an example configuration: \(\(php-mode \"<\\\\?php\\|<\\\\? \\|<\\\\?=\" \"\\\\?>\") \(js-mode \"\") \(css-mode \"\"))" :type '(repeat (symbol string string)) :group 'multi-web-mode :safe #'(lambda (tags) (not (catch 'fail (dolist (tag tags) (when (or (not (symbolp (mweb-get-tag-attr tag 'mode))) (not (stringp (mweb-get-tag-attr tag 'open))) (not (stringp (mweb-get-tag-attr tag 'close)))) (throw 'fail t))))))) (defcustom mweb-submode-indent-offset 2 "Indentation offset for code inside chunks." :type 'integer :group 'multi-web-mode :safe 'integerp) (defcustom mweb-ignored-commands (list 'undo 'yas/expand 'yas/next-field-or-maybe-expand 'isearch-forward 'isearch-backward 'isearch-other-control-char) "Commands that prevent changing the major mode." :type '(repeat symbol) :group 'multi-web-mode :safe #'(lambda (names) (not (catch 'fail (dolist (name names) (when (not (symbolp name)) (throw 'fail t))))))) (defun mweb-get-tag-attr (tag attribute) "Get TAG ATTRIBUTE. ATTRIBUTE values can be 'mode to get the tag's major mode or 'open/'close to get the open/close regexp respectively." (case attribute (mode (car tag)) (open (cadr tag)) (close (caddr tag)))) (defun mweb-get-tag (tag-major-mode) "Return tag from `mweb-tags' matching TAG-MAJOR-MODE." (assoc tag-major-mode mweb-tags)) (defun mweb--looking-at-tag (&optional type) "Return non-nil if pointer is looking at an open or close tag. Possible values of TYPE are: * nil: to check if point is looking at an open or close tag. * 'open: to check if point is looking at an open tag * 'close: to check if point is looking at a close tag" (let ((index 0) (looking) (open-tag) (close-tag) (tag-regexp)) (save-excursion (back-to-indentation) (while (and (< index (length mweb-tags)) (not looking)) (setq open-tag (mweb-get-tag-attr (elt mweb-tags index) 'open)) (setq close-tag (mweb-get-tag-attr (elt mweb-tags index) 'close)) (case type (open (setq tag-regexp open-tag)) (close (setq tag-regexp close-tag)) (otherwise (setq tag-regexp (concat open-tag "\\|" close-tag)))) (when (looking-at tag-regexp) (setq looking t)) (setq index (+ 1 index)))) looking)) (defsubst mweb-looking-at-open-tag-p () "Return t if point is looking at an open tag." (mweb--looking-at-tag 'open)) (defsubst mweb-looking-at-close-tag-p () "Return t if point is looking at a close tag." (mweb--looking-at-tag 'close)) (defsubst mweb-looking-at-tag-p () "Return t if point is looking at an open or close tag." (mweb--looking-at-tag)) (defun mweb-change-major-mode () "Call the appropriate major mode for the pointed chunk. If the current `major-mode' is the correct one it doesn't funcall the major mode and returns nil, otherwise changes the `major-mode' and returns a symbol with its name." (let ((closest-chunk-point 0) (closest-chunk-mode mweb-default-major-mode) (result nil)) (save-restriction (widen) (dolist (tag mweb-tags) (setq result (mweb-closest-starting-chunk-point tag)) (when (and (integerp result) (<= closest-chunk-point result)) (setq closest-chunk-point result) (setq closest-chunk-mode (mweb-get-tag-attr tag 'mode))))) (when (not (equal closest-chunk-mode major-mode)) (funcall closest-chunk-mode) closest-chunk-mode))) (defun mweb-change-indent-line-function () "Set the correct value for `indent-line-function'. Depending of `major-mode'." (when (not (equal major-mode mweb-default-major-mode)) (setq indent-line-function 'mweb-indent-line))) (defun mweb-closest-starting-chunk-point (tag) "Return the point of the closest chunk for TAG. Where TAG is one of the tags contained in the `mweb-tags' list. If the chunk is not found then it returns nil." (let ((open-tag) (close-tag)) (save-excursion (setq open-tag (re-search-backward (mweb-get-tag-attr tag 'open) nil t))) (save-excursion (setq close-tag (re-search-backward (mweb-get-tag-attr tag 'close) nil t))) (cond ((not open-tag) nil) ((and open-tag (not close-tag)) open-tag) ((> open-tag close-tag) open-tag)))) (defun mweb-multiple-chunks-p () "Check if multiple chunks exist in the current buffer." (save-excursion (save-restriction (widen) (goto-char (point-min)) (re-search-forward "[^\s\t\n]" nil t) (or (not (mweb-looking-at-open-tag-p)) (catch 'break (dolist (tag mweb-tags) (when (re-search-forward (mweb-get-tag-attr tag 'close) nil t) (throw 'break (not (not (re-search-forward "[^\s\t\n]" nil t))))))))))) (defun mweb-update-context () "Update extra indentation value for chunks." (let ((changed-major-mode (mweb-change-major-mode))) (if (and changed-major-mode (not (equal major-mode mweb-default-major-mode))) (setq mweb-extra-indentation (mweb-calculate-indentation)) (setq mweb-extra-indentation 0))) (mweb-change-indent-line-function)) (defun mweb-calculate-indentation () "Calculate the correct indentation given previous submode." (let ((indentation 0) (prev-line-pos) (changed-major-mode major-mode) (buffer-modified-flag (buffer-modified-p))) (save-restriction (widen) (save-excursion (mweb-goto-current-mode-open-tag) (if (progn (mweb-forward-nonblank-line -1) (bobp)) (if (mweb-multiple-chunks-p) (setq indentation 0) (setq indentation (- mweb-submode-indent-offset))) (end-of-line) (setq prev-line-pos (point-marker)) (insert "\na") (mweb-change-major-mode) (indent-according-to-mode) (setq indentation (current-indentation)) (delete-region prev-line-pos (line-end-position)))) (funcall changed-major-mode) (set-buffer-modified-p buffer-modified-flag) indentation))) (defun mweb-mark-whole-buffer () "Multi-web-mode's version of `mark-whole-buffer'." (interactive) (push-mark (point)) (goto-char (point-min)) (mweb-change-major-mode) (push-mark (point-max) nil t)) (defun mweb-indent-line () "Function to use when indenting a submode line." (interactive) ;; Yes, indent according to mode will do what we expect (setq mweb-extra-indentation (mweb-calculate-indentation)) (if (not (mweb-looking-at-open-tag-p)) (if (not (mweb-looking-at-close-tag-p)) ;; Normal indentation (if (equal major-mode mweb-default-major-mode) (indent-according-to-mode) (save-excursion (beginning-of-line) (delete-horizontal-space) (unless (bobp) (indent-according-to-mode) (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset))))) ;; Close tag indentation routine (let ((open-tag-indentation 0)) (save-excursion (mweb-goto-current-mode-open-tag) (setq open-tag-indentation (current-indentation))) (beginning-of-line) (delete-horizontal-space) (indent-to open-tag-indentation))) ;; Open tag indentation routine (beginning-of-line) (delete-horizontal-space) (insert "a") (delete-horizontal-space) (beginning-of-line) (mweb-update-context) (indent-according-to-mode) (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset)) (delete-char 1)) (and (bolp) (back-to-indentation))) (defun mweb-indent-region (start end) "Indent a region taking care of chunks. This routine considers the relative position of the chunks within the buffer. It follows the same filosophy than `mweb-indent-line-forward' because that function is what is used to indent the chunks which are not for the default major mode. Called from a program, START and END specify the region to indent." (interactive "r") (let ((delete-active-region nil) (line-end)) (save-excursion (goto-char end) (setq end (point-marker)) (goto-char start) (mweb-change-major-mode) (or (bolp) (forward-line 1)) (while (< (point) end) (mweb-update-context) (if (equal major-mode mweb-default-major-mode) (indent-according-to-mode) (mweb-indent-line)) (forward-line 1)) (move-marker end nil)))) (defun mweb-get-current-mode-tag-point (type) "Gets the point marker of current chunk's open/close tag. The TYPE argument can be a 'open for the open tag or 'close for the close tag." (when (not (equal major-mode mweb-default-major-mode)) (let ((index 0) (found nil) (tag) (result nil) (re-search-func (if (equal type 'open) 're-search-backward 're-search-forward))) (while (and (< index (length mweb-tags)) (not found)) (setq tag (elt mweb-tags index)) (when (or (equal (mweb-get-tag-attr tag 'mode) major-mode) (equal major-mode mweb-default-major-mode)) (setq found t) (save-excursion (if (looking-at (mweb-get-tag-attr tag type)) (progn (back-to-indentation) (setq result (point))) (setq result (funcall re-search-func (mweb-get-tag-attr tag type) nil t))))) (setq index (+ 1 index))) result))) (defun mweb-goto-current-mode-open-tag () "Move the point to the open tag of the current chunk." (interactive) (let ((tag-point (mweb-get-current-mode-tag-point 'open))) (when tag-point (goto-char tag-point)))) (defun mweb-goto-current-mode-close-tag () "Move the point to the close tag of the current chunk." (interactive) (let ((tag-point (mweb-get-current-mode-tag-point 'close))) (when tag-point (goto-char tag-point)))) (defun mweb-set-extra-indentation (number) "Set the new value for `mweb-extra-indentation' to NUMBER." (interactive "nNew mweb-extra-indentation value: ") (setq mweb-extra-indentation number) (message "mweb-extra-indentation = %d" mweb-extra-indentation)) (defun mweb-set-default-major-mode (major-mode) "Set the new value for `mweb-default-major-mode' to MAJOR-MODE." (interactive "CNew default major mode: ") (setq mweb-default-major-mode major-mode) (mweb-change-major-mode) (message "mweb-default-major-mode = %s" mweb-default-major-mode)) (defun mweb-forward-nonblank-line (&optional number) "Move the cursor to the next/previous non blank line. When NUMBER is positive it moves forward and when is negative it moves backwards." (when (not number) (setq number 1)) (when (> number 1) (setq number 1)) (when (< number -1) (setq number -1)) (forward-line number) (while (and (equal (mweb-get-current-line-trimmed-contents) "") (not (or (bobp) (eobp)))) (forward-line number))) (defun mweb-get-current-line-trimmed-contents () "Gets the contents of the current line. It trims all space characters at the beginning and end of the line." (let ((start-point) (end-point) (contents)) (save-excursion (beginning-of-line) (setq start-point (point)) (end-of-line) (setq end-point (point)) (setq contents (buffer-substring start-point end-point)) (when (string-match "[ \t]*$" contents) (setq contents (replace-match "" nil nil contents))) (when (string-match "^[ \t]*" contents) (setq contents (replace-match "" nil nil contents)))) contents)) (defun mweb-post-command-hook () "The function which is appended to the `post-command-hook'." (when (and multi-web-mode (not (region-active-p)) (not (member last-command mweb-ignored-commands))) (mweb-update-context))) (defun mweb-enable () "Setup the minor mode." (set (make-local-variable 'indent-region-function) 'mweb-indent-region) (make-local-variable 'indent-line-function) (add-hook 'post-command-hook 'mweb-post-command-hook) (assq-delete-all 'multi-web-mode minor-mode-map-alist) (push (cons 'multi-web-mode mweb-mode-map) minor-mode-map-alist) (run-hooks 'mweb-mode-hook)) (defun mweb-disable () "Disable the minor mode." (assq-delete-all 'multi-web-mode minor-mode-map-alist)) ;;;###autoload (define-minor-mode multi-web-mode "Enables the multi web mode chunk detection and indentation" :lighter " Multi-Web" :group 'convenience (if multi-web-mode (mweb-enable) (mweb-disable))) (defun multi-web-mode-maybe () "Used to turn on the globalized minor mode." (when (member (file-name-extension (or buffer-file-name "")) mweb-filename-extensions) (multi-web-mode 1))) (define-globalized-minor-mode multi-web-global-mode multi-web-mode multi-web-mode-maybe :group 'multi-web-mode :require 'multi-web-mode) (provide 'multi-web-mode) ;;; multi-web-mode.el ends here