;;; php-ide.el --- IDE-like UI support for PHP development -*- lexical-binding: t; -*- ;; Copyright (C) 2023 Friends of Emacs-PHP development ;; Author: USAMI Kenta ;; Keywords: tools, files ;; URL: https://github.com/emacs-php/php-mode ;; Version: 1.26.1 ;; 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 . ;;; Commentary: ;; PHP Mode integrates LSP Mode (lsp-mode), Phpactor (phpactor.el) and IDE-like tools. ;; ;; **Note**: ;; This feature is under development and experimental. ;; All of these functions, modes and terms are subject to change without notice. ;; ;; ## Motivations ;; ;; There are some IDE-like features / packages for PHP development. ;; PHP-IDE bridges projects and their IDE-like features. ;; ;; ## IDE Features ;; ;; We don't recommend features, but bundle some feature bridges. ;; They are sorted alphabetically except "none." ;; ;; - none ;; Does not launch any IDE features. ;; - eglot ;; https://github.com/joaotavora/eglot ;; - lsp-bridge ;; https://github.com/manateelazycat/lsp-bridge ;; - lsp-mode ;; https://emacs-lsp.github.io/lsp-mode/ ;; https://github.com/emacs-lsp/lsp-mode ;; - phpactor ;; https://phpactor.readthedocs.io/ ;; https://github.com/phpactor/phpactor ;; https://github.com/emacs-php/phpactor.el ;; ;; ## Configuration ;; ;; Put follows code into your .emacs (~/.emacs.d/init.el) file: ;; ;; (defun init-php-mode-setup () ;; (add-hook 'hack-local-variables-hook #'php-ide-mode t t)) ;; ;; (defun init-php-ide-mode-setup (feature activate) ;; (pcase feature ;; (`lsp-bridge ;; (if activate ;; (progn (yas-minor-mode +1) ;; (corfu-mode -1)) ;; (yas-minor-mode -1) ;; (corfu-mode +1))))) ;; ;; (with-eval-after-load 'php-ide ;; (custom-set-variables ;; '(php-ide-features . 'eglot) ;; and/or 'none, 'phpactor, 'lsp-mode ;; '(php-ide-eglot-executable "psalm-language-server") ;; or "intelephense", '("php" "vendor/bin/path/to/server") ;; ;; If you want to hide php-ide-mode from the mode line, set an empty string ;; '(php-ide-mode-lighter "")) ;; ;; (add-hook 'php-mode-hook #'init-php-mode-setup) ;; (add-hook 'php-ide-mode-functions #'init-php-ide-mode-setup)) ;; ;; If you don't enable IDE support by default, set '(php-ide-feature 'none) ;; ;; ### For per project configuration ;; ;; Put follows code into .dir-locals.el in project directory: ;; ;; ((nil (php-project-root . git) ;; (php-ide-features . (lsp-mode)))) ;; ;; If you can't put .dir-locals.el in your project directory, consider the sidecar-locals package. ;; https://melpa.org/#/sidecar-locals ;; https://codeberg.org/ideasman42/emacs-sidecar-locals ;; ;;; Code: (require 'cl-lib) (require 'php-project) (eval-when-compile (require 'php-ide-phpactor) (defvar eglot-server-programs) (declare-function lsp-bridge-mode "ext:lsp-bridge" ()) (declare-function eglot-ensure "ext:eglot" ()) (declare-function eglot--managed-mode-off "ext:eglot" ()) (declare-function phpactor--find-executable "ext:phpactor" ())) (defvar php-ide-feature-alist '((none :test (lambda () t) :activate (lambda () t) :deactivate (lambda () t)) (phpactor :test (lambda () (and (require 'phpactor nil t) (featurep 'phpactor))) :activate php-ide-phpactor-activate :deactivate php-ide-phpactor-activate) (eglot :test (lambda () (and (require 'eglot nil t) (featurep 'eglot))) :activate eglot-ensure :deactivate eglot--managed-mode-off) (lsp-bridge :test (lambda () (and (require 'lsp-bridge nil t) (featurep 'lsp-bridge))) :activate (lambda () (lsp-bridge-mode +1)) :deactivate (lambda () (lsp-bridge-mode -1))) (lsp-mode :test (lambda () (and (require 'lsp nil t) (featurep 'lsp))) :activate lsp :deactivate lsp-workspace-shutdown))) (defvar php-ide-lsp-command-alist '((intelephense "intelephense" "--stdio") (phpactor . (lambda () (list (if (fboundp 'phpactor--find-executable) (phpactor--find-executable) "phpactor") "language-server"))))) (defgroup php-ide nil "IDE-like support for PHP developing." :tag "PHP-IDE" :prefix "php-ide-" :group 'php) ;;;###autoload (defcustom php-ide-features nil "A set of PHP-IDE features symbol." :tag "PHP-IDE Feature" :group 'php-ide :type `(set ,@(mapcar (lambda (feature) (list 'const (car feature))) php-ide-feature-alist) symbol) :safe (lambda (v) (cl-loop for feature in (if (listp v) v (list v)) always (symbolp feature)))) ;;;###autoload (defcustom php-ide-eglot-executable nil "Command name or path to the command of Eglot LSP executable." :tag "PHP-IDE Eglot Executable" :group 'php-ide :type '(choice (const intelephense) (const phpactor) string (repeat string)) :safe (lambda (v) (cond ((stringp v) (file-exists-p v)) ((listp v) (cl-every #'stringp v)) ((assq v php-ide-lsp-command-alist))))) ;;;###autoload (defun php-ide-eglot-server-program () "Return a list of command to execute LSP Server." (cond ((stringp php-ide-eglot-executable) (list php-ide-eglot-executable)) ((listp php-ide-eglot-executable) php-ide-eglot-executable) ((when-let* ((command (assq php-ide-eglot-executable php-ide-lsp-command-alist))) (cond ((functionp command) (funcall command)) ((listp command) command)))))) (defcustom php-ide-mode-lighter " PHP-IDE" "A symbol of PHP-IDE feature." :tag "PHP-IDE Mode Lighter" :group 'php-ide :type 'string :safe #'stringp) ;;;###autoload (defcustom php-ide-mode-functions nil "Hook functions called when before activating or deactivating PHP-IDE. Notice that two arguments (FEATURE ACTIVATE) are given. FEATURE: A symbol, like \\='lsp-mode. ACTIVATE: T is given when activeting, NIL when deactivating PHP-IDE." :tag "PHP-IDE Mode Functions" :group 'php-ide :type '(repeat function) :safe (lambda (functions) (and (listp functions) (cl-every #'functionp functions)))) ;;;###autoload (define-minor-mode php-ide-mode "Minor mode for integrate IDE-like tools." :lighter php-ide-mode-lighter (let ((ide-features php-ide-features)) (when-let* ((unavailable-features (cl-loop for feature in ide-features unless (assq feature php-ide-feature-alist) collect feature))) (user-error "%s includes unavailable PHP-IDE features. (available features are: %s)" ide-features (mapconcat (lambda (feature) (concat "'" (symbol-name feature))) (php-ide--avilable-features) ", "))) (cl-loop for feature in ide-features for ide-plist = (cdr-safe (assq feature php-ide-feature-alist)) do (if (null ide-plist) (message "Please set `php-ide-feature' variable in .dir-locals.el or custom variable") (run-hook-with-args 'php-ide-mode-functions feature php-ide-mode) (if php-ide-mode (php-ide--activate-buffer feature ide-plist) (php-ide--deactivate-buffer ide-plist)))))) ;;;###autoload (defun php-ide-turn-on () "Turn on PHP IDE-FEATURES and execute `php-ide-mode'." (unless php-ide-features (user-error "No PHP-IDE feature is installed. Install the lsp-mode, lsp-bridge, eglot or phpactor package")) (php-ide-mode +1)) (defun php-ide--activate-buffer (name ide-plist) "Activate php-ide implementation by NAME and IDE-PLIST." (unless (funcall (plist-get ide-plist :test)) (user-error "PHP-IDE feature `%s' is not available" name)) (funcall (plist-get ide-plist :activate))) (defun php-ide--deactivate-buffer (ide-plist) "Deactivate php-ide implementation by IDE-PLIST." (funcall (plist-get ide-plist :deactivate))) (defun php-ide--avilable-features () "Return list of available PHP-IDE features." (cl-loop for (ide . plist) in php-ide-feature-alist if (funcall (plist-get plist :test)) collect ide)) (provide 'php-ide) ;;; php-ide.el ends here