Add isearch-yank-symbol-or-char
[emacs.git] / lisp / textmodes / mhtml-mode.el
blob28c248fb0c4a3ff30fc603b88f94ae6ccb391b68
1 ;;; mhtml-mode.el --- HTML editing mode that handles CSS and JS -*- lexical-binding:t -*-
3 ;; Copyright (C) 2017-2018 Free Software Foundation, Inc.
5 ;; Keywords: wp, hypermedia, comm, languages
7 ;; This file is part of GNU Emacs.
9 ;; GNU Emacs is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
22 ;;; Code:
24 (eval-and-compile
25 (require 'flyspell)
26 (require 'sgml-mode))
27 (require 'js)
28 (require 'css-mode)
29 (require 'prog-mode)
30 (require 'font-lock)
32 (defcustom mhtml-tag-relative-indent t
33 "How <script> and <style> bodies are indented relative to the tag.
35 When t, indentation looks like:
37 <script>
38 code();
39 </script>
41 When nil, indentation of the script body starts just below the
42 tag, like:
44 <script>
45 code();
46 </script>
48 When `ignore', the script body starts in the first column, like:
50 <script>
51 code();
52 </script>"
53 :group 'sgml
54 :type '(choice (const nil) (const t) (const ignore))
55 :safe 'symbolp
56 :version "26.1")
58 (cl-defstruct mhtml--submode
59 ;; Name of this submode.
60 name
61 ;; HTML end tag.
62 end-tag
63 ;; Syntax table.
64 syntax-table
65 ;; Propertize function.
66 propertize
67 ;; Keymap.
68 keymap
69 ;; Captured locals that are set when entering a region.
70 crucial-captured-locals
71 ;; Other captured local variables; these are not set when entering a
72 ;; region but let-bound during certain operations, e.g.,
73 ;; indentation.
74 captured-locals)
76 (defconst mhtml--crucial-variable-prefix
77 (regexp-opt '("comment-" "uncomment-" "electric-indent-"
78 "smie-" "forward-sexp-function" "completion-" "major-mode"))
79 "Regexp matching the prefix of \"crucial\" buffer-locals we want to capture.")
81 (defconst mhtml--variable-prefix
82 (regexp-opt '("font-lock-" "indent-line-function"))
83 "Regexp matching the prefix of buffer-locals we want to capture.")
85 (defun mhtml--construct-submode (mode &rest args)
86 "A wrapper for make-mhtml--submode that computes the buffer-local variables."
87 (let ((captured-locals nil)
88 (crucial-captured-locals nil)
89 (submode (apply #'make-mhtml--submode args)))
90 (with-temp-buffer
91 (funcall mode)
92 ;; Make sure font lock is all set up.
93 (font-lock-set-defaults)
94 ;; This has to be set to a value other than the mhtml-mode
95 ;; value, to avoid recursion.
96 (unless (variable-binding-locus 'font-lock-fontify-region-function)
97 (setq-local font-lock-fontify-region-function
98 #'font-lock-default-fontify-region))
99 (dolist (iter (buffer-local-variables))
100 (when (string-match mhtml--crucial-variable-prefix
101 (symbol-name (car iter)))
102 (push iter crucial-captured-locals))
103 (when (string-match mhtml--variable-prefix (symbol-name (car iter)))
104 (push iter captured-locals)))
105 (setf (mhtml--submode-crucial-captured-locals submode)
106 crucial-captured-locals)
107 (setf (mhtml--submode-captured-locals submode) captured-locals))
108 submode))
110 (defun mhtml--mark-buffer-locals (submode)
111 (dolist (iter (mhtml--submode-captured-locals submode))
112 (make-local-variable (car iter))))
114 (defvar-local mhtml--crucial-variables nil
115 "List of all crucial variable symbols.")
117 (defun mhtml--mark-crucial-buffer-locals (submode)
118 (dolist (iter (mhtml--submode-crucial-captured-locals submode))
119 (make-local-variable (car iter))
120 (push (car iter) mhtml--crucial-variables)))
122 (defconst mhtml--css-submode
123 (mhtml--construct-submode 'css-mode
124 :name "CSS"
125 :end-tag "</style>"
126 :syntax-table css-mode-syntax-table
127 :propertize css-syntax-propertize-function
128 :keymap css-mode-map))
130 (defconst mhtml--js-submode
131 (mhtml--construct-submode 'js-mode
132 :name "JS"
133 :end-tag "</script>"
134 :syntax-table js-mode-syntax-table
135 :propertize #'js-syntax-propertize
136 :keymap js-mode-map))
138 (defmacro mhtml--with-locals (submode &rest body)
139 (declare (indent 1))
140 `(cl-progv
141 (when ,submode (mapcar #'car (mhtml--submode-captured-locals ,submode)))
142 (when ,submode (mapcar #'cdr (mhtml--submode-captured-locals ,submode)))
143 (cl-progv
144 (when ,submode (mapcar #'car (mhtml--submode-crucial-captured-locals
145 ,submode)))
146 (when ,submode (mapcar #'cdr (mhtml--submode-crucial-captured-locals
147 ,submode)))
148 ,@body)))
150 (defun mhtml--submode-lighter ()
151 "Mode-line lighter indicating the current submode."
152 ;; The end of the buffer has no text properties, so in this case
153 ;; back up one character, if possible.
154 (let* ((where (if (and (eobp) (not (bobp)))
155 (1- (point))
156 (point)))
157 (submode (get-text-property where 'mhtml-submode)))
158 (if submode
159 (mhtml--submode-name submode)
160 "")))
162 (defvar font-lock-beg)
163 (defvar font-lock-end)
165 (defun mhtml--extend-font-lock-region ()
166 "Extend the font lock region according to HTML sub-mode needs.
168 This is used via `font-lock-extend-region-functions'. It ensures
169 that the font-lock region is extended to cover either whole
170 lines, or to the spot where the submode changes, whichever is
171 smallest."
172 (let ((orig-beg font-lock-beg)
173 (orig-end font-lock-end))
174 ;; The logic here may look odd but it is needed to ensure that we
175 ;; do the right thing when trying to limit the search.
176 (save-excursion
177 (goto-char font-lock-beg)
178 ;; previous-single-property-change starts by looking at the
179 ;; previous character, but we're trying to extend a region to
180 ;; include just characters with the same submode as this
181 ;; character.
182 (unless (eobp)
183 (forward-char))
184 (setq font-lock-beg (previous-single-property-change
185 (point) 'mhtml-submode nil
186 (line-beginning-position)))
187 (unless (eq (get-text-property font-lock-beg 'mhtml-submode)
188 (get-text-property orig-beg 'mhtml-submode))
189 (cl-incf font-lock-beg))
191 (goto-char font-lock-end)
192 (unless (bobp)
193 (backward-char))
194 (setq font-lock-end (next-single-property-change
195 (point) 'mhtml-submode nil
196 (line-beginning-position 2)))
197 (unless (eq (get-text-property font-lock-end 'mhtml-submode)
198 (get-text-property orig-end 'mhtml-submode))
199 (cl-decf font-lock-end)))
201 ;; Also handle the multiline property -- but handle it here, and
202 ;; not via font-lock-extend-region-functions, to avoid the
203 ;; situation where the two extension functions disagree.
204 ;; See bug#29159.
205 (font-lock-extend-region-multiline)
207 (or (/= font-lock-beg orig-beg)
208 (/= font-lock-end orig-end))))
210 (defun mhtml--submode-fontify-one-region (submode beg end &optional loudly)
211 (if submode
212 (mhtml--with-locals submode
213 (save-restriction
214 (font-lock-fontify-region beg end loudly)))
215 (font-lock-set-defaults)
216 (font-lock-default-fontify-region beg end loudly)))
218 (defun mhtml--submode-fontify-region (beg end loudly)
219 (syntax-propertize end)
220 (let ((orig-beg beg)
221 (orig-end end)
222 (new-beg beg)
223 (new-end end))
224 (while (< beg end)
225 (let ((submode (get-text-property beg 'mhtml-submode))
226 (this-end (next-single-property-change beg 'mhtml-submode
227 nil end)))
228 (let ((extended (mhtml--submode-fontify-one-region submode beg
229 this-end loudly)))
230 ;; If the call extended the region, take note. We track the
231 ;; bounds we were passed and take the union of any extended
232 ;; bounds.
233 (when (and (consp extended)
234 (eq (car extended) 'jit-lock-bounds))
235 (setq new-beg (min new-beg (cadr extended)))
236 ;; Make sure that the next region starts where the
237 ;; extension of this region ends.
238 (setq this-end (cddr extended))
239 (setq new-end (max new-end this-end))))
240 (setq beg this-end)))
241 (when (or (/= orig-beg new-beg)
242 (/= orig-end new-end))
243 (cons 'jit-lock-bounds (cons new-beg new-end)))))
245 (defvar-local mhtml--last-submode nil
246 "Record the last visited submode.
247 This is used by `mhtml--pre-command'.")
249 (defvar-local mhtml--stashed-crucial-variables nil
250 "Alist of stashed values of the crucial variables.")
252 (defun mhtml--stash-crucial-variables ()
253 (setq mhtml--stashed-crucial-variables
254 (mapcar (lambda (sym)
255 (cons sym (buffer-local-value sym (current-buffer))))
256 mhtml--crucial-variables)))
258 (defun mhtml--map-in-crucial-variables (alist)
259 (dolist (item alist)
260 (set (car item) (cdr item))))
262 (defun mhtml--pre-command ()
263 (let ((submode (get-text-property (point) 'mhtml-submode)))
264 (unless (eq submode mhtml--last-submode)
265 ;; If we're entering a submode, and the previous submode was
266 ;; nil, then stash the current values first. This lets the user
267 ;; at least modify some values directly. FIXME maybe always
268 ;; stash into the current mode?
269 (when (and submode (not mhtml--last-submode))
270 (mhtml--stash-crucial-variables))
271 (mhtml--map-in-crucial-variables
272 (if submode
273 (mhtml--submode-crucial-captured-locals submode)
274 mhtml--stashed-crucial-variables))
275 (setq mhtml--last-submode submode))))
277 (defun mhtml--syntax-propertize-submode (submode end)
278 (save-excursion
279 (when (search-forward (mhtml--submode-end-tag submode) end t)
280 (setq end (match-beginning 0))))
281 (set-text-properties (point) end
282 (list 'mhtml-submode submode
283 'syntax-table (mhtml--submode-syntax-table submode)
284 ;; We want local-map here so that we act
285 ;; more like the sub-mode and don't
286 ;; override minor mode maps.
287 'local-map (mhtml--submode-keymap submode)))
288 (funcall (mhtml--submode-propertize submode) (point) end)
289 (goto-char end))
291 (defun mhtml-syntax-propertize (start end)
292 ;; First remove our special settings from the affected text. They
293 ;; will be re-applied as needed.
294 (remove-list-of-text-properties start end
295 '(syntax-table local-map mhtml-submode))
296 (goto-char start)
297 ;; Be sure to look back one character, because START won't yet have
298 ;; been propertized.
299 (unless (bobp)
300 (let ((submode (get-text-property (1- (point)) 'mhtml-submode)))
301 (if submode
302 (mhtml--syntax-propertize-submode submode end)
303 ;; No submode, so do what sgml-mode does.
304 (sgml-syntax-propertize-inside end))))
305 (funcall
306 (syntax-propertize-rules
307 ("<style.*?>"
308 (0 (ignore
309 (goto-char (match-end 0))
310 ;; Don't apply in a comment.
311 (unless (syntax-ppss-context (syntax-ppss))
312 (mhtml--syntax-propertize-submode mhtml--css-submode end)))))
313 ("<script.*?>"
314 (0 (ignore
315 (goto-char (match-end 0))
316 ;; Don't apply in a comment.
317 (unless (syntax-ppss-context (syntax-ppss))
318 (mhtml--syntax-propertize-submode mhtml--js-submode end)))))
319 sgml-syntax-propertize-rules)
320 ;; Make sure to handle the situation where
321 ;; mhtml--syntax-propertize-submode moved point.
322 (point) end))
324 (defun mhtml-indent-line ()
325 "Indent the current line as HTML, JS, or CSS, according to its context."
326 (interactive)
327 (let ((submode (save-excursion
328 (back-to-indentation)
329 (get-text-property (point) 'mhtml-submode))))
330 (if submode
331 (save-restriction
332 (let* ((region-start
333 (or (previous-single-property-change (point) 'mhtml-submode)
334 (point)))
335 (base-indent (save-excursion
336 (goto-char region-start)
337 (sgml-calculate-indent))))
338 (cond
339 ((eq mhtml-tag-relative-indent nil)
340 (setq base-indent (- base-indent sgml-basic-offset)))
341 ((eq mhtml-tag-relative-indent 'ignore)
342 (setq base-indent 0)))
343 (narrow-to-region region-start (point-max))
344 (let ((prog-indentation-context (list base-indent)))
345 (mhtml--with-locals submode
346 ;; indent-line-function was rebound by
347 ;; mhtml--with-locals.
348 (funcall indent-line-function)))))
349 ;; HTML.
350 (sgml-indent-line))))
352 (defun mhtml--flyspell-check-word ()
353 (let ((submode (get-text-property (point) 'mhtml-submode)))
354 (if submode
355 (flyspell-generic-progmode-verify)
356 t)))
358 ;;;###autoload
359 (define-derived-mode mhtml-mode html-mode
360 '((sgml-xml-mode "XHTML+" "HTML+") (:eval (mhtml--submode-lighter)))
361 "Major mode based on `html-mode', but works with embedded JS and CSS.
363 Code inside a <script> element is indented using the rules from
364 `js-mode'; and code inside a <style> element is indented using
365 the rules from `css-mode'."
366 (setq-local indent-line-function #'mhtml-indent-line)
367 (setq-local syntax-propertize-function #'mhtml-syntax-propertize)
368 (setq-local font-lock-fontify-region-function
369 #'mhtml--submode-fontify-region)
370 (setq-local font-lock-extend-region-functions
371 '(mhtml--extend-font-lock-region))
373 ;; Attach this to both pre- and post- hooks just in case it ever
374 ;; changes a key binding that might be accessed from the menu bar.
375 (add-hook 'pre-command-hook #'mhtml--pre-command nil t)
376 (add-hook 'post-command-hook #'mhtml--pre-command nil t)
378 ;; Make any captured variables buffer-local.
379 (mhtml--mark-buffer-locals mhtml--css-submode)
380 (mhtml--mark-buffer-locals mhtml--js-submode)
382 (mhtml--mark-crucial-buffer-locals mhtml--css-submode)
383 (mhtml--mark-crucial-buffer-locals mhtml--js-submode)
384 (setq mhtml--crucial-variables (delete-dups mhtml--crucial-variables))
386 ;: Hack
387 (js--update-quick-match-re)
389 ;; This is sort of a prog-mode as well as a text mode.
390 (run-hooks 'prog-mode-hook))
392 (put 'mhtml-mode 'flyspell-mode-predicate #'mhtml--flyspell-check-word)
394 (provide 'mhtml-mode)
396 ;;; mhtml-mode.el ends here