diff --git a/Eask b/Eask index 6c3ad837..6fe9bb80 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 9b722e9b..ff55d907 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.md b/README.md index 490b3f08..3f47c8c9 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,39 @@ 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 +(custom-set-variables + '(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 490ff7b8..4b050d24 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-*") - (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 3124ced0..d4b46994 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 6881ec54..8985c2d6 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 "
\n\n
\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 "
\n\n
\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))