From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED!not-for-mail From: Tom Tromey Newsgroups: gmane.emacs.devel Subject: Re: html, css, and js modes working together Date: Sun, 12 Feb 2017 09:52:27 -0700 Message-ID: <87r3331hdg.fsf@tromey.com> References: <87o9ynarz3.fsf@tromey.com> <877f4z6i8n.fsf@tromey.com> <87poipzr0l.fsf@tromey.com> <87efz44o47.fsf@tromey.com> NNTP-Posting-Host: blaine.gmane.org Mime-Version: 1.0 Content-Type: text/plain X-Trace: blaine.gmane.org 1486918706 12169 195.159.176.226 (12 Feb 2017 16:58:26 GMT) X-Complaints-To: usenet@blaine.gmane.org NNTP-Posting-Date: Sun, 12 Feb 2017 16:58:26 +0000 (UTC) User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/25.1.91 (gnu/linux) Cc: Tom Tromey , Stefan Monnier , emacs-devel@gnu.org To: Dmitry Gutov Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Sun Feb 12 17:58:20 2017 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([208.118.235.17]) by blaine.gmane.org with esmtp (Exim 4.84_2) (envelope-from ) id 1ccxTd-0002WX-3X for ged-emacs-devel@m.gmane.org; Sun, 12 Feb 2017 17:58:17 +0100 Original-Received: from localhost ([::1]:52663 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1ccxTi-0004hk-09 for ged-emacs-devel@m.gmane.org; Sun, 12 Feb 2017 11:58:22 -0500 Original-Received: from eggs.gnu.org ([2001:4830:134:3::10]:58551) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1ccxT5-0004fl-Tg for emacs-devel@gnu.org; Sun, 12 Feb 2017 11:57:46 -0500 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1ccxT2-00024r-Og for emacs-devel@gnu.org; Sun, 12 Feb 2017 11:57:43 -0500 Original-Received: from qproxy5-pub.mail.unifiedlayer.com ([69.89.21.30]:59197) by eggs.gnu.org with smtp (Exim 4.71) (envelope-from ) id 1ccxT2-000241-6N for emacs-devel@gnu.org; Sun, 12 Feb 2017 11:57:40 -0500 Original-Received: (qmail 15828 invoked by uid 0); 12 Feb 2017 16:57:34 -0000 Original-Received: from unknown (HELO cmgw4) (10.0.90.85) by qproxy5.mail.unifiedlayer.com with SMTP; 12 Feb 2017 16:57:34 -0000 Original-Received: from box522.bluehost.com ([74.220.219.122]) by cmgw4 with id jssU1u00S2f2jeq01ssXXy; Sun, 12 Feb 2017 09:52:34 -0700 X-Authority-Analysis: v=2.1 cv=Pets2ERd c=1 sm=1 tr=0 a=GsOEXm/OWkKvwdLVJsfwcA==:117 a=GsOEXm/OWkKvwdLVJsfwcA==:17 a=L9H7d07YOLsA:10 a=9cW_t1CCXrUA:10 a=s5jvgZ67dGcA:10 a=n2v9WMKugxEA:10 a=vaJtXVxTAAAA:8 a=mDV3o1hIAAAA:8 a=XTGcDwc_d5uFYNX8SB8A:9 a=WyacwRpqJ6QA:10 a=L03L2QfmqWoA:10 a=NWVoK91CQyQA:10 a=hCt-GehETBxEYQOojhlW:22 a=_FVE-zBwftR9WsbkzFJk:22 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=tromey.com; s=default; h=Content-Type:MIME-Version:Message-ID:In-Reply-To:Date: References:Subject:Cc:To:From:Sender:Reply-To:Content-Transfer-Encoding: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=3Jq7M41INJhoBbqYYkfTCSmi2gkRE1NuS2MhuKSM86Q=; b=JowcY1b88by0ZR2GSaQxREzoC2 kF9FePL2fdw7Q5MMoAaxDkh9pb/hZYDuwi00hRmnwcks1ouJQfQXu4VjzkOPeU3cQdyyDUJOvDZ1H kLFDAG9iwq0HH2ihx3mbP8NO7; Original-Received: from 174-16-128-54.hlrn.qwest.net ([174.16.128.54]:56884 helo=bapiya) by box522.bluehost.com with esmtpsa (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.87) (envelope-from ) id 1ccxO0-0008Iz-8u; Sun, 12 Feb 2017 09:52:28 -0700 X-Attribution: Tom In-Reply-To: (Dmitry Gutov's message of "Sun, 12 Feb 2017 18:20:16 +0200") X-AntiAbuse: This header was added to track abuse, please include it with any abuse report X-AntiAbuse: Primary Hostname - box522.bluehost.com X-AntiAbuse: Original Domain - gnu.org X-AntiAbuse: Originator/Caller UID/GID - [47 12] / [47 12] X-AntiAbuse: Sender Address Domain - tromey.com X-BWhitelist: no X-Source-IP: 174.16.128.54 X-Exim-ID: 1ccxO0-0008Iz-8u X-Source: X-Source-Args: X-Source-Dir: X-Source-Sender: 174-16-128-54.hlrn.qwest.net (bapiya) [174.16.128.54]:56884 X-Source-Auth: tom+tromey.com X-Email-Count: 7 X-Source-Cap: ZWx5bnJvYmk7ZWx5bnJvYmk7Ym94NTIyLmJsdWVob3N0LmNvbQ== X-detected-operating-system: by eggs.gnu.org: GNU/Linux 3.x [fuzzy] X-Received-From: 69.89.21.30 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.org gmane.emacs.devel:212292 Archived-At: >>>>> "Dmitry" == Dmitry Gutov 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 . ;;; 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 "" :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 "" :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 ("" (0 (ignore (goto-char (match-end 0)) (mhtml--syntax-propertize-submode mhtml--css-submode end)))) ("" (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