236 lines
9.2 KiB
EmacsLisp
236 lines
9.2 KiB
EmacsLisp
;;; php-format.el --- Code reformatter for PHP buffer -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2020 Friends of Emacs-PHP development
|
||
|
||
;; Author: USAMI Kenta <tadsan@zonu.me>
|
||
;; Created: 5 Mar 2023
|
||
;; Keywords: tools, php
|
||
;; URL: https://github.com/emacs-php/php-mode.el
|
||
;; License: GPL-3.0-or-later
|
||
|
||
;; This program 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.
|
||
|
||
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;; This feature is for execute PHP code formatting tools.
|
||
|
||
;; ## Supported tools:
|
||
;;
|
||
;; - Easy Coding Standard (ecs) https://github.com/easy-coding-standard/easy-coding-standard
|
||
;; - PHP-CS-Fixer (php-cs-fixer) https://github.com/PHP-CS-Fixer/PHP-CS-Fixer
|
||
;; - PHP_CodeSniffer (phpcbf) https://github.com/squizlabs/PHP_CodeSniffer
|
||
;;
|
||
;; It supports both per-project and globally installed ones.
|
||
;;
|
||
;; ## How to use
|
||
;;
|
||
;; Add following line to setup function for php-mode.
|
||
;;
|
||
;; (php-format-auto-mode +1)
|
||
;;
|
||
;; ## Customization
|
||
;;
|
||
;; These variables can be set either by dir-locals.el or by custom-set-variable.
|
||
;;
|
||
;; - php-format-auto-mode-hook-depth
|
||
;; - php-format-command
|
||
;; - php-format-command-dir
|
||
;; - php-format-default-idle-time
|
||
;; - php-format-result-display-method-alist
|
||
;;
|
||
;; ## Display methods
|
||
;;
|
||
;; How formatting is performed and how the results are displayed can be controlled
|
||
;; by the following keywords.
|
||
;;
|
||
;; - idle: Asynchronously apply formatting to idle time in Emacs using `run-with-idle-timer'
|
||
;; - async: Immediately execute an asynchronous process to apply formatting
|
||
;; - compile: Apply formatting using the compile command. Doesn't lock, but results pop up
|
||
;; - silent: Apply formatting immediately and synchronously.
|
||
;; No message is displayed, but Emacs is locked while it is being processed.
|
||
;; - nil: Apply formatting immediately and synchronously.
|
||
;; Emacs will be locked until formatting is done and the result will pop up.
|
||
;;
|
||
|
||
;;; Code:
|
||
(require 'cl-lib)
|
||
(require 'php-project)
|
||
|
||
(defvar php-format-formatter-alist
|
||
'((ecs :marker ("ecs.php")
|
||
:command ("ecs" "check" "--fix" "--no-progress-bar" "--"))
|
||
(php-cs-fixer :marker (".php-cs-fixer.dist.php" ".php-cs-fixer.php")
|
||
:command ("php-cs-fixer" "fix" "--show-progress=none"))
|
||
(phpcbf :marker ("phpcs.xml.dist" "phpcs.xml")
|
||
:command ("phpcbf"))))
|
||
|
||
(defvar php-format-lighter " phpf")
|
||
(defvar php-format-output-buffer " *PHP Format*")
|
||
(defvar php-format--exec-method nil)
|
||
(defvar php-format--idle-timer nil)
|
||
|
||
;; Customize variables
|
||
(defgroup php-format nil
|
||
"Apply code reformat."
|
||
:tag "PHP Format"
|
||
:group 'php)
|
||
|
||
(defcustom php-format-auto-mode-hook-depth -50
|
||
"A depth number in the range -100..100 for `add-hook'."
|
||
:tag "PHP Format Auto Mode Hook Depth"
|
||
:type 'integer
|
||
:safe #'integerp
|
||
:group 'php)
|
||
|
||
(defcustom php-format-command 'auto
|
||
"A formatter symbol, or a list of command and arguments."
|
||
:tag "PHP Format Command"
|
||
:type '(choice (const :tag "Disabled reformat codes" nil)
|
||
(const :tag "Auto" auto)
|
||
(const :tag "Easy Coding Standard" ecs)
|
||
(const :tag "PHP-CS-Fixer" php-cs-fixer)
|
||
(const :tag "PHP Code Beautifier and Fixer" phpcbf)
|
||
(repeat :tag "Command and arguments" string))
|
||
:safe (lambda (v) (or (symbolp v) (listp v)))
|
||
:group 'php-format)
|
||
|
||
(defcustom php-format-command-dir "vendor/bin"
|
||
"A relative path to the directory where formatting tool is installed."
|
||
:tag "PHP Format Command"
|
||
:type 'string
|
||
:safe #'stringp
|
||
:group 'php-format)
|
||
|
||
(defcustom php-format-default-idle-time 3
|
||
"Number of seconds to wait idle before formatting."
|
||
:tag "PHP Format Auto Mode Hook Depth"
|
||
:type 'integer
|
||
:safe #'integerp
|
||
:group 'php)
|
||
|
||
(defcustom php-format-result-display-method-alist '((php-format-on-after-save-hook . idle)
|
||
(php-format-this-buffer-file . silent)
|
||
(php-format-project . compile))
|
||
"An alist of misplay the result method of the formatting process."
|
||
:tag "PHP Format Result Display Method"
|
||
:type '(alist :key-type function
|
||
:value-type symbol)
|
||
:group 'php-format)
|
||
|
||
(defcustom php-format-disable-async-format-buffer-has-modified t
|
||
"When set to non-NIL, disables async formatting if buffer is modified (unsaved)."
|
||
:tag "PHP Format Disable Async Format Buffer Has Modified"
|
||
:type 'boolean
|
||
:group 'php-format)
|
||
|
||
;; Internal functions
|
||
(defsubst php-format--register-timer (sec command-args)
|
||
"Register idle-timer with SEC and COMMAND-ARGS."
|
||
(unless php-format--idle-timer
|
||
(setq php-format--idle-timer
|
||
(run-with-idle-timer sec nil #'php-format--execute-delayed-format
|
||
default-directory command-args))))
|
||
|
||
(defun php-format--execute-format (files)
|
||
"Execute PHP formatter with FILES."
|
||
(let* ((default-directory (php-project-get-root-dir))
|
||
(command-args (php-format--get-command-args))
|
||
command-line)
|
||
(when (null command-args)
|
||
(user-error "No available PHP formatter settings detected"))
|
||
(setq command-args (append command-args files))
|
||
(setq command-line (mapconcat #'shell-quote-argument command-args " "))
|
||
(pcase php-format--exec-method
|
||
(`(idle ,sec) (php-format--register-timer sec command-args))
|
||
('idle (php-format--register-timer php-format-default-idle-time command-args))
|
||
('async (apply #'call-process-shell-command (car command-args) nil nil nil
|
||
(append (cdr command-args) (list "&"))))
|
||
('compile (compile command-line))
|
||
('silent (shell-command-to-string command-line))
|
||
('nil (shell-command command-line))
|
||
(_ (user-error "`%s' is unexpected php-format--exec-method" php-format--exec-method)))))
|
||
|
||
(defun php-format--get-command-args ()
|
||
"Return a list of command and arguments."
|
||
(if (listp php-format-command)
|
||
php-format-command
|
||
(let ((cmd php-format-command)
|
||
args executable vendor)
|
||
(when (eq 'auto cmd)
|
||
(setq cmd (cl-loop for (sym . plist) in php-format-formatter-alist
|
||
for files = (plist-get plist :marker)
|
||
if (cl-find-if
|
||
(lambda (file) (file-exists-p (expand-file-name file default-directory)))
|
||
files)
|
||
return sym))
|
||
(setq-local php-format-command cmd))
|
||
(when-let* ((tup (plist-get (cdr-safe (assq cmd php-format-formatter-alist)) :command)))
|
||
(setq executable (car tup))
|
||
(setq args (cdr tup))
|
||
(setq vendor (expand-file-name executable (expand-file-name php-format-command-dir default-directory)))
|
||
(cond
|
||
((file-exists-p vendor) (cons vendor args))
|
||
((executable-find executable) (cons executable args)))))))
|
||
|
||
(defun php-format--execute-delayed-format (dir command-args)
|
||
"Asynchronously execute PHP format with COMMAND-ARGS in DIR."
|
||
(setq php-format--idle-timer nil)
|
||
(let ((default-directory dir))
|
||
(when (not (and php-format-disable-async-format-buffer-has-modified
|
||
(buffer-modified-p)))
|
||
(apply #'call-process-shell-command (car command-args) nil nil nil
|
||
(append (cdr command-args) (list "&"))))))
|
||
|
||
;; Public functions and minor mode
|
||
|
||
;;;###autoload
|
||
(defun php-format-this-buffer-file ()
|
||
"Apply format this buffer file."
|
||
(interactive)
|
||
(when php-format-command
|
||
(when (null buffer-file-name)
|
||
(user-error "This file has not yet been saved"))
|
||
(when (file-remote-p buffer-file-name)
|
||
(user-error "PHP Format feature does not yet support remote files"))
|
||
(let ((php-format--exec-method (cdr-safe (assq 'php-format-this-buffer-file php-format-result-display-method-alist))))
|
||
(php-format--execute-format (list buffer-file-name)))))
|
||
|
||
;;;###autoload
|
||
(defun php-format-project ()
|
||
"Apply format this buffer file."
|
||
(interactive)
|
||
(unless php-format-command
|
||
(user-error "Disabled `php-format-command' in this project"))
|
||
(let ((php-format--exec-method (cdr-safe (assq 'php-format-project php-format-result-display-method-alist))))
|
||
(php-format--execute-format nil)))
|
||
|
||
;;;###autoload
|
||
(defun php-format-on-after-save-hook ()
|
||
"Apply format on after save hook."
|
||
(when (and php-format-command buffer-file-name (not (file-remote-p buffer-file-name)))
|
||
(let ((php-format--exec-method (cdr-safe (assq 'php-format-on-after-save-hook php-format-result-display-method-alist))))
|
||
(php-format--execute-format nil))))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode php-format-auto-mode
|
||
"Automatically apply formatting when saving an edited file."
|
||
:group 'php-format
|
||
:lighter php-format-lighter
|
||
(if php-format-auto-mode
|
||
(add-hook 'after-save-hook 'php-format-on-after-save-hook php-format-auto-mode-hook-depth t)
|
||
(remove-hook 'after-save-hook 'php-format-on-after-save-hook t)))
|
||
|
||
(provide 'php-format)
|
||
;;; php-format.el ends here
|