emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: html, css, and js modes working together


From: Tom Tromey
Subject: Re: html, css, and js modes working together
Date: Sun, 12 Feb 2017 09:52:27 -0700
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/25.1.91 (gnu/linux)

>>>>> "Dmitry" == Dmitry Gutov <address@hidden> writes:

Dmitry> How does your buffer-locals approach work?

I've appended the current code.

It makes a temporary buffer for each submode and captures a bunch of
locals.  There are two kinds of capture: those that are applied when
entering or leaving a region, and those that are only let-bound in
certain situations.

Dmitry> In mmm-mode, you first call a major mode to get its values of
Dmitry> indent-line-function and so on. Without them being available after
Dmitry> calling html-mode, we'd have to hardcode an annoying amount of stuff,
Dmitry> and become less resilient against renames.

Yeah, this is the same; it was Stefan's suggestion upthread.

Maybe all of this should just be done by pulling mmm-mode into Emacs?

Tom

;;; mhtml-mode.el --- HTML editing mode that handles CSS and JS -*- 
lexical-binding:t -*-

;; Copyright (C) 2017 Free Software Foundation, Inc.

;; Keywords: wp, hypermedia, comm, languages

;; This file is part of GNU Emacs.

;; GNU Emacs 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.

;; GNU Emacs 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/>.

;;; Code:

(eval-and-compile
  (require 'sgml-mode))
(require 'js)
(require 'css-mode)
(require 'prog-mode)
(require 'font-lock)

(cl-defstruct mhtml--submode
  ;; Name of this submode.
  name
  ;; HTML end tag.
  end-tag
  ;; Syntax table.
  syntax-table
  ;; Propertize function.
  propertize
  ;; Keymap.
  keymap
  ;; Captured locals that are set when entering a region.
  crucial-captured-locals
  ;; Other captured local variables; these are not set when entering a
  ;; region but let-bound during certain operations, e.g.,
  ;; indentation.
  captured-locals)

(defconst mhtml--crucial-variable-prefix
  (regexp-opt '("comment-" "uncomment-" "electric-indent-"
                "smie-" "forward-sexp-function"))
  "Regexp matching the prefix of \"crucial\" buffer-locals we want to capture.")

(defconst mhtml--variable-prefix
  (regexp-opt '("font-lock-" "indent-line-function" "major-mode"))
  "Regexp matching the prefix of buffer-locals we want to capture.")

(defun mhtml--construct-submode (mode &rest args)
  "A wrapper for make-mhtml--submode that computes the buffer-local variables."
  (let ((captured-locals nil)
        (crucial-captured-locals nil)
        (submode (apply #'make-mhtml--submode args)))
    (with-temp-buffer
      (funcall mode)
      ;; Make sure font lock is all set up.
      (font-lock-set-defaults)
      (dolist (iter (buffer-local-variables))
        (when (string-match mhtml--crucial-variable-prefix
                            (symbol-name (car iter)))
          (push iter crucial-captured-locals))
        (when (string-match mhtml--variable-prefix (symbol-name (car iter)))
          (push iter captured-locals)))
      (setf (mhtml--submode-crucial-captured-locals submode)
            crucial-captured-locals)
      (setf (mhtml--submode-captured-locals submode) captured-locals))
    submode))

(defun mhtml--mark-buffer-locals (submode)
  (dolist (iter (mhtml--submode-captured-locals submode))
    (make-local-variable (car iter))))

(defvar-local mhtml--crucial-variables nil
  "List of all crucial variable symbols.")

(defun mhtml--mark-crucial-buffer-locals (submode)
  (dolist (iter (mhtml--submode-crucial-captured-locals submode))
    (make-local-variable (car iter))
    (push (car iter) mhtml--crucial-variables)))

(defconst mhtml--css-submode
  (mhtml--construct-submode 'css-mode
                            :name "CSS"
                            :end-tag "</style>"
                            :syntax-table css-mode-syntax-table
                            :propertize css-syntax-propertize-function
                            :keymap css-mode-map))

(defconst mhtml--js-submode
  (mhtml--construct-submode 'js-mode
                            :name "JS"
                            :end-tag "</script>"
                            :syntax-table js-mode-syntax-table
                            :propertize #'js-syntax-propertize
                            :keymap js-mode-map))

(defmacro mhtml--with-locals (submode &rest body)
  (declare (indent 1))
  `(cl-progv
       (when submode (mapcar #'car (mhtml--submode-captured-locals submode)))
       (when submode (mapcar #'cdr (mhtml--submode-captured-locals submode)))
     (cl-progv
         (when submode (mapcar #'car (mhtml--submode-crucial-captured-locals
                                      submode)))
         (when submode (mapcar #'cdr (mhtml--submode-crucial-captured-locals
                                      submode)))
       ,@body)))

(defun mhtml--submode-lighter ()
  "Mode-line lighter indicating the current submode."
  (let ((submode (get-text-property (point) 'mhtml-submode)))
    (if submode
        (mhtml--submode-name submode)
      "")))

(defun mhtml--submode-fontify-one-region (submode beg end &optional loudly)
  (if submode
      (mhtml--with-locals submode
        (save-restriction
          (narrow-to-region beg end)
          (font-lock-set-defaults)
          (font-lock-default-fontify-region (point-min) (point-max) loudly)))
    (font-lock-set-defaults)
    (font-lock-default-fontify-region beg end loudly)))

(defun mhtml--submode-fontify-region (beg end loudly)
  (while (< beg end)
    (let ((submode (get-text-property beg 'mhtml-submode))
          (this-end (next-single-property-change beg 'mhtml-submode
                                                 nil end)))
      (mhtml--submode-fontify-one-region submode beg this-end loudly)
      (setq beg this-end))))

(defvar-local mhtml--last-submode nil
  "Record the last visited submode, so the cursor-sensor function
can function properly.")

(defvar-local mhtml--stashed-crucial-variables nil
  "Alist of stashed values of the crucial variables.")

(defun mhtml--stash-crucial-variables ()
  (setq mhtml--stashed-crucial-variables
        (mapcar (lambda (sym)
                  (cons sym (buffer-local-value sym (current-buffer))))
                mhtml--crucial-variables)))

(defun mhtml--map-in-crucial-variables (alist)
  (dolist (item alist)
    (set (car item) (cdr item))))

(defun mhtml--cursor-sensor (_window pos _action)
  (let ((submode (get-text-property (point) 'mhtml-submode)))
    ;; If we're entering a submode, and the previous submode was nil,
    ;; then stash the current values first.  This lets the user at
    ;; least modify some values directly.
    (when (and submode (not mhtml--last-submode))
      (mhtml--stash-crucial-variables))
    (mhtml--map-in-crucial-variables
     (if submode
         (mhtml--submode-crucial-captured-locals submode)
       mhtml--stashed-crucial-variables))
    (setq mhtml--last-submode submode)
    (force-mode-line-update)))

(defun mhtml--syntax-propertize-submode (submode end)
  (save-excursion
    (when (search-forward (mhtml--submode-end-tag submode) end t)
      (setq end (match-beginning 0))))
  (set-text-properties (point) end
                       (list 'mhtml-submode submode
                             'syntax-table (mhtml--submode-syntax-table submode)
                             ;; We want local-map here so that we act
                             ;; more like the sub-mode and don't
                             ;; override minor mode maps.
                             'local-map (mhtml--submode-keymap submode)
                             'cursor-sensor-functions
                             '(mhtml--cursor-sensor)))
  (funcall (mhtml--submode-propertize submode) (point) end)
  (goto-char end))

(defun mhtml-syntax-propertize (start end)
  (goto-char start)
  (when (get-text-property (point) 'mhtml-submode)
    (mhtml--syntax-propertize-submode (get-text-property (point) 'mhtml-submode)
                                      end))
  (funcall
   (syntax-propertize-rules
    ("<style.*?>"
     (0 (ignore
         (goto-char (match-end 0))
         (mhtml--syntax-propertize-submode mhtml--css-submode end))))
    ("<script.*?>"
     (0 (ignore
         (goto-char (match-end 0))
         (mhtml--syntax-propertize-submode mhtml--js-submode end))))
    sgml-syntax-propertize-rules)
   ;; Make sure to handle the situation where
   ;; mhtml--syntax-propertize-submode moved point.
   (point) end))

(defun mhtml-indent-line ()
  "Indent the current line as HTML, JS, or CSS, according to its context."
  (interactive)
  (let ((submode (save-excursion
                   (back-to-indentation)
                   (get-text-property (point) 'mhtml-submode))))
    (if submode
        (save-restriction
          (let* ((region-start (previous-single-property-change (point)
                                                                'mhtml-submode))
                 (base-indent (save-excursion
                                (goto-char region-start)
                                (sgml-calculate-indent))))
            (narrow-to-region region-start (point-max))
            (let ((prog-indentation-context (list base-indent
                                                  (cons (point-min) nil)
                                                  nil)))
              (mhtml--with-locals submode
                ;; indent-line-function was rebound by
                ;; mhtml--with-locals.
                (funcall indent-line-function)))))
      ;; HTML.
      (sgml-indent-line))))

;;;###autoload
(define-derived-mode mhtml-mode html-mode
  '((sgml-xml-mode "XHTML+" "HTML+") (:eval (mhtml--submode-lighter)))
  "Major mode based on `html-mode', but works with embedded JS and CSS.

Code inside a <script> element is indented using the rules from
`js-mode'; and code inside a <style> element is indented using
the rules from `css-mode'."
  (cursor-sensor-mode)
  (setq-local indent-line-function #'mhtml-indent-line)
  (setq-local parse-sexp-lookup-properties t)
  (setq-local syntax-propertize-function #'mhtml-syntax-propertize)
  (setq-local font-lock-fontify-region-function
              #'mhtml--submode-fontify-region)

  ;; Make any captured variables buffer-local.
  (mhtml--mark-buffer-locals mhtml--css-submode)
  (mhtml--mark-buffer-locals mhtml--js-submode)

  (mhtml--mark-crucial-buffer-locals mhtml--css-submode)
  (mhtml--mark-crucial-buffer-locals mhtml--js-submode)
  (setq mhtml--crucial-variables (delete-dups mhtml--crucial-variables))

  ;: Hack
  (js--update-quick-match-re))

(provide 'mhtml-mode)

;;; mhtml-mode.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]