;;; multi-mode.el --- support for multiple major modes
;; Copyright (C) 2003, 2004, 2007, 2009 Free Software Foundation, Inc.
;; Author: Dave Love <fx@gnu.org>
;; Keywords: languages, extensions, files
;; Created: Sept 2003
;; $Revision: 1.11 $
;; URL: http://www.loveshack.ukfsn.org/emacs
;; This file 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.
;; This file 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This is a framework for a sort of meta mode which provides support
;; for multiple major modes in different regions (`chunks') of a
;; buffer -- sort of. Actually, multiple indirect buffers are used,
;; one per mode (apart from the base buffer, which has the default
;; mode). A post-command hook selects the correct buffer for the mode
;; around point. This is done on the basis of calling the elements of
;; `multi-chunk-fns' and taking the value corresponding to a start
;; position closest to point.
;; [I originally tried maintaining information about the local major
;; mode on a text property maintained by font-lock along with a
;; `point-entered' property to control changing the indirect buffer.
;; This worked less well for reasons I've forgotten, unfortunately.
;; The post-command hook seems to be efficient enough in the simple
;; cases I've tried, and is most general.]
;; Using indirect buffers ensures that we always have the correct
;; local buffer properties like the keymap and syntax table, including
;; the local variable list. There are other implementations of this
;; sort of thing, e.g. for literate programming, but as far as I know,
;; they all work either by continually re-executing the major mode
;; functions or by swapping things like the keymap in and out
;; explicitly. Using indirect buffers seems to be the right thing and
;; is at least potentially more robust, for instance for things like
;; specific subprocesses associated with the buffer for Ispell or
;; whatever. (However, it has the potential confusion of there
;; actually being multiple buffers, even if they mostly act like one.
;; It also may interact badly with other modes using
;; `post-command-hook', as with Flyspell.) Also, it's fairly simple.
;; Maybe it won't turn out to be the best approach, but it currently
;; seems to me to be better than the approach used by MMM mode
;; <URL:http://mmm-mode.sourceforge.net/>, and is a lot less code. I
;; suspect doing the job any better would require fairly serious
;; surgery on buffer behaviour. For instance, consider being able
;; programmatically to treat discontinuous buffer regions as
;; continuous according to, say, text properties. That would work
;; rather like having multiple gaps which the most primitive movement
;; functions skip.
;; The other major difference to MMM mode is that we don't operate on
;; more-or-less static regions in different modes which need to be
;; re-parsed explicitly and/or explicitly inserted. Instead they're
;; dynamically defined, so that random editing DTRT.
;; Things like font-lock and Imenu need to be done piecewise over the
;; chunks. Some functions, such as indentation, should be executed
;; with the buffer narrowed to the current chunk. This ensures they
;; aren't thrown by syntax in other chunks which could confuse
;; `parse-partial-sexp'.
;; The intention is that other major modes can be defined with this
;; framework for specific purposes, e.g. literate programming systems,
;; e.g. literate Haskell, mixing Haskell and LaTeX chunks. See the
;; example of haskell-latex.el, which can activate the literate mode
;; just by adding to `haskell-mode-hook'. The multiple modes can also
;; be chosen on the basis of the file contents, e.g. noweb.el.
;; Problems:
;; * C-x C-v in the indirect buffer just kills both buffers. (Perhaps an
;; Emacs bug.)
;; * Font-locking doesn't always work properly in Emacs 21, due to lack
;; of `font-lock-dont-widen', e.g. see the commentary in
;; haskell-latex.el.
;; * C-x C-w in Emacs 21 may change the mode when it shouldn't.
;; * Flyspell needs modifying so as not to cause trouble with this.
;; For the moment we hack the Flyspell hook functions.
;; * The behaviour of the region can appear odd if point and mark are in
;; regions corresponding to different modes, since they are actually in
;; different buffers. We really want point and marks to be shared among
;; the indirect buffers.
;; * `view-file' doesn't work properly -- only the main buffer gets the
;; minor mode. Probably fixable using `view-mode-hook'.
;; * Doubtless facilities other than Imenu, Font Lock and Flyspell
;; should be supported explicitly or fixed up to work with this,
;; e.g. by narrowing to the chunk around commands. There may be a
;; need for more hooks or other support in base Emacs to help.
;; * Fontification currently doesn't work properly with modes which
;; define their own fontification functions, at least the way nXML mode
;; does it. nXML seems to need hacking to get this right, maybe by
;; converting it to use font-lock. nXML is the only mode I know
;; which defines its own `fontification-functions', although PSGML
;; does its own fontification differently again. Generally, any
;; fontification which doesn't respect the narrowing in force will
;; cause problems. (One might think about `flet'ting `widen' to
;; `ignore' around fontification functions, but that means they can't
;; parse from a previous point, as things like nXML should
;; legitimately be able to do.)
;; Todo:
;; * Provide a simple way to define multi-chunk functions by a series
;; of delimiting regexps, similarly to MMM mode -- someone asked for
;; that.
;; * Provide a means for code describing `island' chunks, which
;; doesn't know about the base or other modes, to describe a chain
;; of chunks in that mode for mapping over chunks.
;;; Code:
(require 'font-lock)
(require 'imenu)
(eval-when-compile (require 'advice))
(defvar multi-indirect-buffers-alist nil
"Alist of direct and indirect buffers v. major modes.
Internal use. Buffer local.")
(make-variable-buffer-local 'multi-indirect-buffers-alist)
(defvar multi-normal-fontify-function nil
"Fontification function normally used by the buffer's major mode.
Internal use. Buffer local.")
(make-variable-buffer-local 'multi-normal-fontify-function)
(defvar multi-normal-fontify-functions nil
"Fontification functions normally used by the buffer's major mode.
Internal use. Buffer local.")
(make-variable-buffer-local 'multi-normal-fontify-functions)
(defvar multi-indirect-buffer-hook nil
"Hook run by `multi-install-mode' in each indirect buffer.
It is run after all the indirect buffers have been set up.")
(defvar multi-select-mode-hook nil
"Hook run after a different mode is selected.")
(defvar multi-chunk-fns nil
"List of functions to determine the modes of chunks.
Each takes a single arg, the position at which to find the mode. It returns
a list (MODE START END).
Buffer local.")
(make-variable-buffer-local 'multi-chunk-fns)
(defsubst multi-base-buffer ()
"Return base buffer of current buffer, or the current buffer if it's direct."
(or (buffer-base-buffer (current-buffer))
(current-buffer)))
(defvar multi-late-index-function nil
"Original value of `imenu-create-index-function' for the buffer's mode.")
(make-variable-buffer-local 'multi-late-index-function)
;; This isn't called `multi-mode-alist', since that will get treated
;; as risky in file variables.
(defvar multi-alist nil
"Alist of elements (MODE . FUNCTION) specifying a buffer's multiple modes.
MODE is a major mode and FUNCTION is a function used as an element of
`multi-chunk-fns' or nil. Use nil if MODE is detected by another element
of the alist.
This is intended to be set as a file variable in a file which specifies
`multi-mode' as its major mode.")
;; See the commentary below.
(defun multi-hack-local-variables ()
"Like `hack-local-variables', but ignore `mode' items."
(let ((late-hack (symbol-function 'hack-one-local-variable)))
(fset 'hack-one-local-variable
(lambda (var val)
(unless (eq var 'mode)
(funcall late-hack var val))))
(unwind-protect
(hack-local-variables)
(fset 'hack-one-local-variable late-hack))))
(defun multi-install-mode (mode &optional chunk-fn base)
"Add MODE to the multiple major modes supported by the current buffer.
CHUNK-FN, if non-nil, is a function to select the mode of a chunk,
added to the list `multi-chunk-fns'. BASE non-nil means that this
is the base mode."
(unless (memq mode multi-indirect-buffers-alist) ; be idempotent
;; This is part of a grim hack for lossage in AUCTeX, which
;; bogusly advises `hack-one-local-variable'. This loses, due to
;; the way advice works, when we run `multi-hack-local-variables'
;; below -- there ought to be a way round this, probably with CL's
;; flet. Any subsequent use of it then fails because advice has
;; captured the now-unbound variable `late-hack'... Thus ensure
;; we've loaded the mode in advance to get any autoloads sorted
;; out. Do it generally in case other modes have similar
;; problems. [The AUCTeX stuff is in support of an undocumented
;; feature which is unnecessary and, anyway, wouldn't need advice
;; to implement. Unfortunately the maintainer seems not to
;; understand the local variables mechanism and wouldn't remove
;; this. To invoke minor modes, you should just use `mode:' in
;; `local variables'.]
(if (eq 'autoload (car-safe (indirect-function mode)))
(with-temp-buffer
(insert "Local Variables:\nmode: fundamental\nEnd:\n")
(funcall mode)
(hack-local-variables)))
(let ((new-buffer (if base
(current-buffer)
;; Perhaps the name uniquification should use
;; the mode name somehow (without getting long).
(make-indirect-buffer (current-buffer)
(generate-new-buffer-name
(buffer-name))))))
(with-current-buffer (multi-base-buffer)
(push (cons mode new-buffer) multi-indirect-buffers-alist)
(let ((alist multi-indirect-buffers-alist)
(hook multi-indirect-buffer-hook)
(fns (if chunk-fn
(add-to-list 'multi-chunk-fns chunk-fn)
multi-chunk-fns))
(alist2 multi-alist)
(file (buffer-file-name))
(base-name (buffer-name))
(coding buffer-file-coding-system)
(multi-mode t)) ; The modes might examine this.
(with-current-buffer new-buffer
(unless (and base (eq mode major-mode))
(funcall mode))
;; Now we can make it local:
(set (make-local-variable 'multi-mode) t)
;; Use file's local variables section to set variables in
;; this buffer. (Don't just copy local variables from the
;; base buffer because it may have set things locally that
;; we don't want in the other modes.) We need to prevent
;; `mode' being processed and re-setting the major mode.
;; It all goes badly wrong if `hack-one-local-variable' is
;; advised. The appropriate mechanism to get round this
;; appears to be `ad-with-originals', but we don't want to
;; pull in the advice package unnecessarily. `flet'-like
;; mechanisms lose with advice because `fset' acts on the
;; advice anyway.
(if (featurep 'advice)
(ad-with-originals (hack-one-local-variable)
(multi-hack-local-variables))
(multi-hack-local-variables))
;; Indentation should first narrow to the chunk. Modes
;; should normally just bind `indent-line-function' to
;; handle indentation.
(when indent-line-function ; not that it should ever be nil...
(set (make-local-variable 'indent-line-function)
`(lambda ()
(save-restriction
(multi-narrow-to-chunk)
(,indent-line-function)))))
;; Now handle the case where the mode binds TAB directly.
;; Bind it in an overriding map to use the local definition,
;; but narrowed to the chunk.
(let ((tab (local-key-binding "\t")))
(when tab
(make-local-variable 'minor-mode-map-alist)
(push (cons multi-mode
(let ((map (make-sparse-keymap)))
(define-key map "\t"
`(lambda ()
(interactive)
(save-restriction
(multi-narrow-to-chunk)
(call-interactively ',tab))))
map))
minor-mode-map-alist)))
(setq multi-normal-fontify-function
font-lock-fontify-region-function)
(set (make-local-variable 'font-lock-fontify-region-function)
#'multi-fontify-region)
(setq multi-normal-fontify-functions fontification-functions)
(setq fontification-functions '(multi-fontify))
;; Don't let parse-partial-sexp get fooled by syntax outside
;; the chunk being fontified. (Not in Emacs 21.)
(set (make-local-variable 'font-lock-dont-widen) t)
(setq multi-late-index-function imenu-create-index-function)
(setq imenu-create-index-function #'multi-create-index
multi-indirect-buffer-hook hook)
;; Kill the base buffer along with the indirect one; careful not
;; to infloop.
(add-hook 'kill-buffer-hook
'(lambda ()
(setq kill-buffer-hook nil)
(kill-buffer (buffer-base-buffer (current-buffer))))
t t)
;; This should probably be at the front of the hook list, so
;; that other hook functions get run in the (perhaps)
;; newly-selected buffer.
(add-hook 'post-command-hook 'multi-select-buffer nil t)
;; Avoid the uniqified name for the indirect buffer in the
;; mode line.
(setq mode-line-buffer-identification
(propertized-buffer-identification base-name))
;; Fixme: Are there other things to copy?
(setq buffer-file-coding-system coding)
;; For benefit of things like VC
(setq buffer-file-name file)
(vc-find-file-hook))
;; Propagate updated values of the relevant buffer-local
;; variables to the indirect buffers.
(dolist (x alist)
(if (car x)
(with-current-buffer (cdr x)
(setq multi-chunk-fns fns)
(setq multi-indirect-buffers-alist alist)
(setq multi-alist alist2)
(run-hooks 'multi-indirect-buffer-hook)))))))))
(defun multi-map-over-chunks (beg end thunk)
"For all chunks between BEG and END, execute THUNK.
THUNK is a function of no args. It is executed with point at the
beginning of the chunk and with the buffer narrowed to the chunk."
(save-excursion
(save-window-excursion
(goto-char beg)
(while (< (point) end)
(multi-select-buffer)
(save-restriction
(multi-narrow-to-chunk)
(funcall thunk)
(goto-char (point-max)))
(unless (multi-next-chunk-start)
(goto-char (point-max)))))))
;; We need this for asynchronous fontification by jit-lock, even
;; though we're redefining `fontification-functions'.
(defun multi-fontify-region (beg end loudly)
"Multi-mode font-lock fontification function.
Fontifies chunk-by chunk within the region.
Assigned to `font-lock-fontify-region-function'."
(let* ((modified (buffer-modified-p))
(buffer-undo-list t)
(inhibit-read-only t)
(inhibit-point-motion-hooks t)
(inhibit-modification-hooks t)
deactivate-mark)
(font-lock-unfontify-region beg end)
(save-restriction
(widen)
(multi-map-over-chunks
beg end (lambda ()
(if (and font-lock-mode font-lock-keywords)
(funcall multi-normal-fontify-function
(point-min) (point-max) loudly)))))
;; In case font-lock isn't done for some mode:
(put-text-property beg end 'fontified t)
(when (and (not modified) (buffer-modified-p))
(set-buffer-modified-p nil))))
;; I'm not sure it's worth trying to support non-font-lock
;; fontification functions like this (see this file's commentary).
(defun multi-fontify (start)
"Multi-mode fontification function.
Fontifies chunk-by-chunk within the region from START for up to
`multi-fontification-chunk-size' characters."
(save-restriction
(multi-narrow-to-chunk)
(run-hook-with-args 'multi-normal-fontify-functions start)))
(defun multi-create-index ()
"Create Imenu index alist for the currently-selected buffer.
Works piece-wise in all the chunks with the same major mode.
Assigned to `imenu-create-index-function'."
(let ((selected-mode major-mode)
imenu-alist ; accumulator
last mode)
(multi-map-over-chunks
(point-min) (point-max)
(lambda ()
(if (eq major-mode selected-mode)
;; Index this chunk and merge results with accumulator.
(dolist (elt (funcall multi-late-index-function))
(if (not (listp (cdr elt)))
(push elt imenu-alist) ; normal element
(let ((elt2 (assoc (car elt) imenu-alist))) ; submenu
;; Fixme: Assumes only a single level of submenu.
(if elt2
(setcdr elt2 (append (cdr elt) (cdr elt2)))
(push elt imenu-alist))))))))
imenu-alist))
(defun multi-next-chunk-start ()
"Move to the start of the next chunk."
(widen)
(goto-char (nth 2 (multi-find-mode-at)))
(unless (eobp)
(forward-char)
t))
(defun multi-narrow-to-chunk ()
"Narrow to the current chunk."
(interactive)
(unless (= (point-min) (point-max))
(apply #'narrow-to-region (cdr (multi-find-mode-at)))))
(defun multi-select-buffer ()
"Select the appropriate (indirect) buffer corresponding to point's context."
;; It may help to catch errors here. If there are context-dependent
;; errors, it may well work correctly when point changes, but if it
;; gets an error, it will be removed from post-command-hook and there
;; won't be useful debugging context anyway.
(condition-case ()
(let ((buffer (cdr (assoc (car (multi-find-mode-at))
multi-indirect-buffers-alist))))
(unless (eq buffer (current-buffer))
(let* ((point (point))
(window-start (window-start))
(visible (pos-visible-in-window-p))
(oldbuf (current-buffer)))
(when (buffer-live-p buffer)
(switch-to-buffer buffer)
(bury-buffer oldbuf)
(goto-char point)
;; Avoid the display jumping around.
(when visible
(set-window-start (get-buffer-window buffer t) window-start))
(unless (eq buffer oldbuf)
(run-hooks 'multi-select-mode-hook))))))
(error nil)))
(defvar multi-mode-list (list t t t))
(defsubst multi-make-list (mode start end)
"Constructor for lists returned by elements of `multi-chunk-fns' &c.
Destructively modifies `multi-mode-list' to avoid consing in
`post-command-hook'."
(setcar multi-mode-list mode)
(setcar (cdr multi-mode-list) start)
(setcar (cddr multi-mode-list) end)
multi-mode-list)
;; It would be nice to cache the results of this on text properties,
;; but that probably won't work well if chunks can be nested. In that
;; case, you can't just mark everything between delimiters -- you have
;; to consider other possible regions between them. For now, we do
;; the calculation each time, scanning outwards from point.
(defun multi-find-mode-at (&optional pos)
"Apply elements of `multi-chunk-fns' to determine major mode at POS.
Return a list (MODE START END), the value returned by the function in the
list for which START is closest to POS (and before it); i.e. the innermost
mode is selected. POS defaults to point."
(let ((fns multi-chunk-fns)
(start (point-min))
(mode (with-current-buffer (multi-base-buffer)
major-mode))
(end (point-max))
(pos (or pos (point)))
val)
(save-restriction
(widen)
(dolist (fn multi-chunk-fns)
(setq val (funcall fn pos))
(if (and val (or (not mode)
(>= (nth 1 val) start)))
(setq mode (nth 0 val)
start (nth 1 val)
end (nth 2 val)))))
(unless (and (<= start end) (<= pos end) (>= pos start))
(error "Bad multi-mode selection: %s, %s"
(multi-make-list mode start end) pos))
(if (= start end)
(setq end (1+ end)))
(multi-make-list mode start end)))
;; This was basically for testing, and isn't a reasonable thing to use
;; otherwise.
;; (define-derived-mode multi-mode fundamental-mode ""
;; "Pseudo major mode controlling multiple major modes apparently in a buffer.
;; Actually maintains multiple views of the data in indirect buffers and
;; switches between them according to the context of point with a post-command
;; hook. Depends on a specification of `multi-alist' in file variables."
;; ;; We need to do the work after file variables have been processed so
;; ;; that we can use a specification of `multi-alist'.
;; (set (make-local-variable 'hack-local-variables-hook)
;; #'multi-mode-install-modes))
(defun multi-mode-install-modes ()
"Process `multi-alist' and create the appropriate buffers."
(if multi-alist
(let ((elt (pop multi-alist)))
(multi-install-mode (car elt) (cdr elt) t)
(dolist (elt multi-alist)
(multi-install-mode (car elt) (cdr elt))))
(fundamental-mode)
(error "`multi-alist' not defined for multi-mode")))
;; In 21.3, Flyspell breaks things, apparently by getting an error in
;; post-command-hook and thus clobbering it. In development code it
;; doesn't do that, but does check indirect buffers it shouldn't. I'm
;; not sure exactly how this happens, but checking flyspell-mode in
;; the hook functions cures this. For the moment, we'll hack this up.
;; (Let's not bring advice into it...)
(eval-after-load "flyspell"
'(progn
(defalias 'flyspell-post-command-hook
`(lambda ()
,(concat (documentation 'flyspell-post-command-hook)
"\n\n[Wrapped by multi-mode.]")
(if flyspell-mode
(funcall ,(symbol-function 'flyspell-post-command-hook)))))
(defalias 'flyspell-pre-command-hook
`(lambda ()
(concat (documentation 'flyspell-pre-command-hook)
"\n\n[Wrapped by multi-mode.]")
(if 'flyspell-mode
(funcall ,(symbol-function 'flyspell-pre-command-hook)))))))
(provide 'multi-mode)
;;; multi-mode.el ends here