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))