branch: elpa/php-mode
commit 101b3b5cad0d783a91ffc58e0e43115d06d11034
Merge: 86e788eeea 1a1b285b78
Author: USAMI Kenta <[email protected]>
Commit: GitHub <[email protected]>

    Merge pull request #742 from 
emacs-php/refactor/switch-html-template-major-mode
    
    Make simplify php-check-html-for-indentation and deprecate multi-modes 
support
---
 Eask                   |   2 +
 Makefile               |   1 +
 README.ja.md           |  32 ++++++++++++
 README.md              |  32 ++++++++++++
 lisp/php-mode.el       | 138 +++++++++++++++----------------------------------
 lisp/php.el            |  27 +++++++---
 tests/php-mode-test.el |  46 +++++++++++++++++
 7 files changed, 175 insertions(+), 103 deletions(-)

diff --git a/Eask b/Eask
index 6c3ad83793..6fe9bb805e 100644
--- a/Eask
+++ b/Eask
@@ -33,6 +33,8 @@
  (depends-on "projectile")
  (depends-on "smart-jump")
  (depends-on "shut-up")
+ (depends-on "polymode")
+ (depends-on "web-mode")
  )
 
 (setq network-security-level 'low)  ; see 
https://github.com/jcs090218/setup-emacs-windows/issues/156#issuecomment-932956432
diff --git a/Makefile b/Makefile
index 9b722e9be6..ff55d9071d 100644
--- a/Makefile
+++ b/Makefile
@@ -49,6 +49,7 @@ dev:
 # for an example of using a script like this with the 'git bisect run'
 # command.
 test: clean all
+       $(EASK) install polymode web-mode
        $(EASK) test ert ./tests/php-mode-test.el
 
 .PHONY: all authors autoloads clean test
diff --git a/README.ja.md b/README.ja.md
index 81c0f5104d..2222cbb379 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -75,6 +75,38 @@ M-x package-install php-mode
   (php-project-coding-style . psr2)))
 ```
 
+## HTMLとPHPが混在するファイルの編集
+
+`php-mode`は純粋なPHPスクリプトのためのメジャーモードです。テンプレートのようにHTMLの中にPHPを埋め込んだファイルは、両方の言語を理解するメジャーモードで編集するほうが適しています。特にインデントは、HTML部分を素の`php-mode`で編集すると正しく動作しません。
+
+そうしたファイルのために、PHP 
Modeは`php-html-template-major-mode`(既定は[`web-mode`](https://web-mode.org/))へ処理を委ねます。好みのモードを設定できます。
+
+```lisp
+(setopt php-html-template-major-mode 'web-mode)
+```
+
+### メジャーモードの選ばれ方
+
+拡張子`.php`のファイルは`php-mode-maybe`を通して開かれ、ファイル名と内容からメジャーモードが決まります。
+
+- `php-template-mode-alist`にマッチするファイル名(例: 
`.phtml`や`.blade.php`)は、対応するテンプレート用モードで開きます。
+- それ以外は、ディレクトリローカル変数`php-project-php-file-as-template`に従います。
+  - `auto`(既定): HTMLタグを含むファイルを`php-html-template-major-mode`に切り替えます。
+  - `t`: そのディレクトリのすべての`.php`ファイルをテンプレートとして扱います。
+  - `nil`: すべての`.php`ファイルを素のPHPスクリプトとして扱います。
+- いずれにも当てはまらない場合は`php-default-major-mode`(`php-mode`)で開きます。
+
+`php-project-php-file-as-template`は`.dir-locals.el`でプロジェクトごとに設定できます。
+
+```lisp
+((nil
+  (php-project-php-file-as-template . nil)))
+```
+
+### php-modeからの切り替え
+
+すでに`php-mode`でHTMLタグを含むファイルをインデントしようとすると、PHP 
Modeは警告し、`php-html-template-major-mode`への切り替えを尋ねます。このプロンプトを無効にするには`php-mode-warn-if-html-template`を`nil`に設定してください。
+
 ## 不具合を報告する
 
 バグ報告の際には `M-x php-mode-debug` の出力を含めてください。この情報は問題の再現に役立ちます。
diff --git a/README.md b/README.md
index 490b3f0888..94421fb85b 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,38 @@ You can add project-specific settings by creating a 
`.dir-locals.el` or `.dir-lo
   (php-project-coding-style . psr2)))
 ```
 
+## Editing files that mix HTML and PHP
+
+`php-mode` is designed for pure PHP scripts.  Files that embed PHP inside 
HTML, such as templates, are better edited in a major mode that understands 
both languages.  Indentation in particular is unreliable when the HTML part of 
a file is edited in plain `php-mode`.
+
+For such files, PHP Mode defers to `php-html-template-major-mode`, which 
defaults to [`web-mode`](https://web-mode.org/).  Set it to any mode you prefer:
+
+```lisp
+(setopt php-html-template-major-mode 'web-mode)
+```
+
+### How the major mode is chosen
+
+Files with a `.php` extension are opened through `php-mode-maybe`, which picks 
the major mode from the file name and its content:
+
+- File names matching `php-template-mode-alist` (for example `.phtml` and 
`.blade.php`) open in the matching template mode.
+- Otherwise the choice follows `php-project-php-file-as-template`, a 
directory-local variable:
+  - `auto` (default): switch to `php-html-template-major-mode` when the file 
contains an HTML tag.
+  - `t`: treat every `.php` file in the directory as a template.
+  - `nil`: treat every `.php` file as a plain PHP script.
+- When nothing else applies, the file opens in `php-default-major-mode` 
(`php-mode`).
+
+Set `php-project-php-file-as-template` per project in `.dir-locals.el`:
+
+```lisp
+((nil
+  (php-project-php-file-as-template . nil)))
+```
+
+### Switching away from php-mode
+
+If you are already in `php-mode` and indent a file that contains HTML tags, 
PHP Mode warns you and offers to switch to `php-html-template-major-mode`.  Set 
`php-mode-warn-if-html-template` to `nil` to turn off this prompt.
+
 ## Reporting Bugs
 
 When reporting a bug, please run `M-x php-mode-debug` and include its output 
in your bug report.  This helps us reproduce any issues you may be experiencing.
diff --git a/lisp/php-mode.el b/lisp/php-mode.el
index 490ff7b80f..4b050d2449 100644
--- a/lisp/php-mode.el
+++ b/lisp/php-mode.el
@@ -247,13 +247,12 @@ Turning this on will force PEAR rules on all PHP files."
   :tag "PHP Mode Force Pear"
   :type 'boolean)
 
-(defcustom php-mode-warn-if-mumamo-off t
-  "Warn once per buffer if you try to indent a buffer without
-mumamo-mode turned on.  Detects if there are any HTML tags in the
-buffer before warning, but this is is not very smart; e.g. if you
-have any tags inside a PHP string, it will be fooled."
-  :tag "PHP Mode Warn If MuMaMo Off"
+(defcustom php-mode-warn-if-html-template t
+  "Warn and prompt to switch to an HTML template major mode when indenting 
HTML."
+  :tag "PHP Mode Warn If HTML Template"
+  :safe #'booleanp
   :type '(choice (const :tag "Warn" t) (const :tag "Don't warn" nil)))
+(make-obsolete-variable 'php-mode-warn-if-mumamo-off 
'php-mode-warn-if-html-template "2.0.0")
 
 (defcustom php-mode-coding-style 'pear
   "Select default coding style to use with `php-mode'.
@@ -864,107 +863,54 @@ See `php-beginning-of-defun'."
   (php-beginning-of-defun (- (or arg 1))))
 
 
-(defvar php-warned-bad-indent nil)
+(defvar-local php-warned-bad-indent nil
+  "Non-nil once the user has been warned about indenting this buffer.
+Buffer-local so a warning in one buffer does not suppress it in others.")
 
-;; Do it but tell it is not good if html tags in buffer.
 (defun php-check-html-for-indentation ()
-  (let ((html-tag-re "^\\s-*</?\\sw+.*?>")
-        (here (point)))
-    (goto-char (line-beginning-position))
-    (if (or (when (boundp 'mumamo-multi-major-mode) mumamo-multi-major-mode)
-            ;; Fix-me: no idea how to check for mmm or multi-mode
-            (save-match-data
-              (not (or (re-search-forward html-tag-re (line-end-position) t)
-                       (re-search-backward html-tag-re 
(line-beginning-position) t)))))
-        (prog1 t
-          (goto-char here))
-      (goto-char here)
-      (setq php-warned-bad-indent t)
-      (let* ((known-multi-libs '(("mumamo" mumamo (lambda () (nxhtml-mumamo)))
-                                 ("mmm-mode" mmm-mode (lambda () (mmm-mode 1)))
-                                 ("multi-mode" multi-mode (lambda () 
(multi-mode 1)))
-                                 ("web-mode" web-mode (lambda () (web-mode)))))
-             (known-names (mapcar (lambda (lib) (car lib)) known-multi-libs))
-             (available-multi-libs (delq nil
-                                         (mapcar
-                                          (lambda (lib)
-                                            (when (locate-library (car lib)) 
lib))
-                                          known-multi-libs)))
-             (available-names (mapcar (lambda (lib) (car lib)) 
available-multi-libs))
-             (base-msg
-              (concat
-               "Indentation fails badly with mixed HTML/PHP in the HTML part in
-plain `php-mode'.  To get indentation to work you must use an
-Emacs library that supports 'multiple major modes' in a buffer.
-Parts of the buffer will then be in `php-mode' and parts in for
-example `html-mode'.  Known such libraries are:\n\t"
-               (mapconcat #'identity known-names ", ")
-               "\n"
-               (if available-multi-libs
-                   (concat
-                    "You have these available in your `load-path':\n\t"
-                    (mapconcat #'identity available-names ", ")
-                    "\n\n"
-                    "Do you want to turn any of those on? ")
-                 "You do not have any of those in your `load-path'.")))
-             (is-using-multi
-              (catch 'is-using
-                (dolist (lib available-multi-libs)
-                  (when (and (boundp (cadr lib))
-                             (symbol-value (cadr lib)))
-                    (throw 'is-using t))))))
-        (unless is-using-multi
-          (if available-multi-libs
-              (if (not (y-or-n-p base-msg))
-                  (message "Did not do indentation, but you can try again now 
if you want")
-                (let* ((name
-                        (if (= 1 (length available-multi-libs))
-                            (car available-names)
-                          ;; Minibuffer window is more than one line, fix that 
first:
-                          (message "")
-                          (completing-read "Choose multiple major mode support 
library: "
-                                           available-names nil t
-                                           (car available-names)
-                                           '(available-names . 1)
-                                           )))
-                       (mode (when name
-                               (cl-caddr (assoc name available-multi-libs)))))
-                  (when mode
-                    ;; Minibuffer window is more than one line, fix that first:
-                    (message "")
-                    (load name)
-                    (funcall mode))))
-            (lwarn 'php-indent :warning base-msg)))
-        nil))))
+  "Return non-nil when the current buffer may be indented as PHP.
+Warn and offer to switch to `php-html-template-major-mode' when the
+buffer looks like an HTML template edited in plain `php-mode'."
+  (cond
+   ((not php-mode-warn-if-html-template) t)
+   ;; In a polymode buffer php-mode only ever indents its own PHP chunks,
+   ;; so indent normally and never warn about the surrounding HTML.
+   ((php-in-poly-php-html-mode) t)
+   ((not (php-buffer-has-html-tag)) t)
+   (php-warned-bad-indent nil)
+   ((fboundp php-html-template-major-mode)
+    (if (y-or-n-p (format "This file seems to contain an HTML tag.  Switch to 
%s? "
+                          php-html-template-major-mode))
+        (funcall php-html-template-major-mode)
+      (prog1 nil
+        (setq-local php-warned-bad-indent t))))
+   (t ;; Suppress warnings in Emacs session
+    (setq-local php-warned-bad-indent t)
+    (lwarn 'php-mode
+           :warning "Indentation fails badly with mixed HTML/PHP in the HTML 
part in plain `php-mode'.
+It is highly recommended to install a major mode that supports PHP and HTML 
templates, such as Web Mode.
+
+Set `php-html-template-major-mode' variable to use a mode other than 
`web-mode'.
+Set `php-mode-warn-if-html-template' variable to nil to suppress the warning.
+")
+    nil)))
 
 (defun php-cautious-indent-region (start end &optional quiet)
   "Carefully indent region START to END in contexts other than HTML templates.
 
 If the optional argument QUIET is non-nil then no syntactic errors are
 reported, even if `c-report-syntactic-errors' is non-nil."
-  (if (or (not php-mode-warn-if-mumamo-off)
-          (not (php-in-poly-php-html-mode))
-          php-warned-bad-indent
-          (php-check-html-for-indentation))
-      (funcall 'c-indent-region start end quiet)))
+  (when (php-check-html-for-indentation)
+    (c-indent-region start end quiet)))
 
 (defun php-cautious-indent-line ()
   "Carefully indent lines in contexts other than HTML templates."
-  (if (or (not php-mode-warn-if-mumamo-off)
-          (not (php-in-poly-php-html-mode))
-          php-warned-bad-indent
-          (php-check-html-for-indentation))
-      (let ((here (point))
-            doit)
-        (move-beginning-of-line nil)
-        ;; Don't indent heredoc end mark
-        (save-match-data
-          (unless (and (looking-at "[a-zA-Z0-9_]+;\n")
-                       (php-in-string-p))
-            (setq doit t)))
-        (goto-char here)
-        (when doit
-          (funcall 'c-indent-line)))))
+  (when (and (php-check-html-for-indentation)
+             (save-excursion
+               (beginning-of-line)
+               ;; Don't indent heredoc end mark
+               (not (and (looking-at-p "[a-zA-Z0-9_]+;\n") 
(php-in-string-p)))))
+    (c-indent-line)))
 
 (defun php-c-at-vsemi-p (&optional pos)
   "Return T on HTML lines (including php tag) or PHP8 Attribute, otherwise NIL.
diff --git a/lisp/php.el b/lisp/php.el
index 3124ced08b..d4b46994bd 100644
--- a/lisp/php.el
+++ b/lisp/php.el
@@ -632,14 +632,27 @@ Look at the `php-executable' variable instead of the 
constant \"php\" command."
       (symbol-value php-re-detect-html-tag)
     php-re-detect-html-tag))
 
+(defvar-local php--buffer-has-html-tag-cache nil
+  "Memoized result of `php-buffer-has-html-tag'.
+A cons of (CHARS-MODIFIED-TICK . RESULT) so the scan is repeated only
+after the buffer text changes.")
+
 (defun php-buffer-has-html-tag ()
-  "Return position of HTML tag or NIL in current buffer."
-  (save-excursion
-    (save-restriction
-      (widen)
-      (goto-char (point-min))
-      (save-match-data
-        (re-search-forward (php-re-detect-html-tag) nil t)))))
+  "Return position of HTML tag or NIL in current buffer.
+The result is cached per buffer and recomputed only when the buffer text
+has changed, because this scans the whole buffer and is called on every
+indentation."
+  (let ((tick (buffer-chars-modified-tick)))
+    (if (eql (car php--buffer-has-html-tag-cache) tick)
+        (cdr php--buffer-has-html-tag-cache)
+      (let ((result (save-excursion
+                      (save-restriction
+                        (widen)
+                        (goto-char (point-min))
+                        (save-match-data
+                          (re-search-forward (php-re-detect-html-tag) nil 
t))))))
+        (setq php--buffer-has-html-tag-cache (cons tick result))
+        result))))
 
 (defun php-derivation-major-mode ()
   "Return major mode for PHP file by file-name and its content."
diff --git a/tests/php-mode-test.el b/tests/php-mode-test.el
index 6881ec5417..8985c2d6ce 100644
--- a/tests/php-mode-test.el
+++ b/tests/php-mode-test.el
@@ -653,6 +653,52 @@ Meant for `php-mode-test-issue-503'."
   ;; Proper alignment arglist that contains empty lines.
   (with-php-mode-test ("indent/issue-793.php" :indent t :magic t)))
 
+(ert-deftest php-mode-test-poly-php-html-indentation ()
+  "Indentation must work inside PHP chunks of a PHP-in-HTML polymode.
+Regression: `php-check-html-for-indentation' returned nil in polymode
+buffers, which disabled indentation of the PHP chunks entirely.
+
+The innermode is defined here (mirroring the `poly-php' package) rather
+than depending on `poly-php', because that package pulls in a released
+`php-mode' from an archive that would shadow the one under test."
+  (skip-unless (require 'polymode nil t))
+  ;; `php-in-poly-php-html-mode' checks the `poly-php-html-mode' variable
+  ;; by name, so define the polymode under exactly that name.
+  (eval '(progn
+           (define-hostmode php-mode-test--poly-html-hostmode :mode 'html-mode)
+           (define-innermode php-mode-test--poly-php-innermode
+             :mode 'php-mode
+             :head-matcher "<\\?php\\|<\\?="
+             :tail-matcher "\\?>"
+             :head-mode 'host :tail-mode 'host)
+           (define-polymode poly-php-html-mode
+             :hostmode 'php-mode-test--poly-html-hostmode
+             :innermodes '(php-mode-test--poly-php-innermode)))
+        t)
+  (with-temp-buffer
+    (insert "<div>\n<?php\nif ($x) {\necho 'hello';\n}\n?>\n</div>\n")
+    (poly-php-html-mode)
+    (font-lock-ensure)
+    (goto-char (point-min))
+    (search-forward "echo 'hello';")
+    (beginning-of-line)
+    (indent-according-to-mode)
+    (should (= 4 (current-indentation)))))
+
+(ert-deftest php-mode-test-derive-html-template-major-mode ()
+  "A PHP file that contains HTML tags derives to 
`php-html-template-major-mode'."
+  (skip-unless (fboundp 'web-mode))
+  (let ((php-html-template-major-mode 'web-mode)
+        (php-project-php-file-as-template 'auto))
+    (with-temp-buffer
+      (setq buffer-file-name (expand-file-name "template.php" 
temporary-file-directory))
+      (unwind-protect
+          (progn
+            (insert "<div>\n<?php echo 'hi'; ?>\n</div>\n")
+            (should (eq 'web-mode (php-derivation-major-mode))))
+        (set-buffer-modified-p nil)
+        (setq buffer-file-name nil)))))
+
 (ert-deftest php-mode-test-php74 ()
   "Test highlighting language constructs added in PHP 7.4."
   (with-php-mode-test ("7.4/arrow-function.php" :faces t))

Reply via email to