diff --git a/CHANGELOG.md b/CHANGELOG.md index de0f8d11..21fa2d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes of the PHP Mode 1.19.1 release series are documented in this * **Net feature**: `php-format` ([#731]) * Add `php-format-project` and `php-format-this-buffer-file` commands * Add `php-format-auto-mode` minor mode + * **Experimental feature: `php-ide`** ([#709]) + * Add `php-ide-phpactor` as simple IDE feature without LSP clients + * Add `php-ide-mode` minor mode for binding IDE-like features ### Fixed @@ -19,6 +22,7 @@ All notable changes of the PHP Mode 1.19.1 release series are documented in this * No longer highlights `'link` in PHPDoc ([#724]) * Please use `goto-address-prog-mode` minor mode +[#709]: https://github.com/emacs-php/php-mode/pull/709 [#724]: https://github.com/emacs-php/php-mode/pull/724 [#726]: https://github.com/emacs-php/php-mode/pull/726 [#731]: https://github.com/emacs-php/php-mode/pull/731 diff --git a/Cask b/Cask index 1d85fe49..a52d38fa 100644 --- a/Cask +++ b/Cask @@ -11,9 +11,13 @@ "lisp/php-format.el" "lisp/php-project.el" "lisp/php-local-manual.el" + "lisp/php-ide-phpactor.el" + "lisp/php-ide.el" "lisp/php-mode-debug.el") (development + ;;(depends-on "lsp-mode") + (depends-on "phpactor") (depends-on "pkg-info") (depends-on "projectile") (depends-on "smart-jump") diff --git a/Makefile b/Makefile index 12aadcda..77474be1 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,8 @@ ELS += lisp/php-defs.el ELS += lisp/php-face.el ELS += lisp/php-flymake.el ELS += lisp/php-format.el +ELS += lisp/php-ide-phpactor.el +ELS += lisp/php-ide.el ELS += lisp/php-local-manual.el ELS += lisp/php-mode-debug.el ELS += lisp/php-mode.el diff --git a/lisp/php-ide-phpactor.el b/lisp/php-ide-phpactor.el new file mode 100644 index 00000000..3f73c80e --- /dev/null +++ b/lisp/php-ide-phpactor.el @@ -0,0 +1,127 @@ +;;; php-ide-phpactor.el --- PHP-IDE feature using Phpactor RPC -*- 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.24.0 +;; 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-IDE implementation to integrate Phpactor (phpactor.el). +;; This feature depends on . + +;;; Code: +(require 'phpactor nil t) +(require 'popup nil t) +(require 'smart-jump nil t) +(eval-when-compile + (require 'cl-lib)) + +(defvar-local php-ide-phpactor-buffer nil) +(defvar-local php-ide-phpactor-hover-last-pos nil) +(defvar-local php-ide-phpactor-hover-last-msg nil) + +(declare-function phpactor--command-argments "ext:phpactor" (&rest arg-keys)) +(declare-function phpactor--parse-json "ext:phpactor" (buffer)) +(declare-function phpactor--rpc-async "ext:phpactor" (action arguments callback)) +(declare-function phpactor-goto-definition "ext:phpactor" ()) +(declare-function popup-tip "ext:popup" (string)) +(declare-function smart-jump-back "ext:smart-jump" ()) +(declare-function smart-jump-go "ext:smart-jump" (&optional smart-list continue)) +(declare-function smart-jump-references "ext:smart-jump" (&optional smart-list continue)) + +(defgroup php-ide-phpactor nil + "UI support for PHP developing." + :tag "PHP-IDE Phpactor" + :prefix "php-ide-phpactor-" + :group 'php-ide) + +(defcustom php-ide-phpactor-activate-features '(all) + "A set of Phpactor features you want to enable." + :tag "PHP-IDE Phpactor Activate Features" + :type '(set (const all :tag "All") + (const hover) + (const navigation)) + :safe (lambda (v) (and (listp v))) + :group 'php-ide-phpactor) + +(defvar php-ide-phpactor-timer nil + "Timer object for execute Phpactor and display hover message.") + +(defvar php-ide-phpactor-disable-hover-at-point-functions + '(php-in-string-or-comment-p)) + +(defun php-ide-phpactor--disable-hover-at-point-p () + "Return non-NIL if any function return non-NIL for disable to hover at point." + (cl-loop for f in php-ide-phpactor-disable-hover-at-point-functions + never (not (funcall f)))) + +(defun php-ide-phpactor-hover () + "Show brief information about the symbol underneath the cursor." + (interactive) + (when (and php-ide-phpactor-buffer (not (php-ide-phpactor--disable-hover-at-point-p))) + (if (eq (point) php-ide-phpactor-hover-last-pos) + (when php-ide-phpactor-hover-last-msg + (let ((msg php-ide-phpactor-hover-last-msg)) + (setq php-ide-phpactor-hover-last-msg nil) + (popup-tip msg))) + (setq php-ide-phpactor-hover-last-pos (point)) + (let ((main-buffer (current-buffer))) + (phpactor--rpc-async "hover" (phpactor--command-argments :source :offset) + (lambda (proc) + (let* ((response (phpactor--parse-json (process-buffer proc))) + (msg (plist-get (plist-get response :parameters) :message))) + (with-current-buffer main-buffer + (setq php-ide-phpactor-hover-last-msg msg))))))))) + +(defsubst php-ide-phpactor--feature-activated-p (feature) + "Is FEATURE activated in `php-ide-phpactor-activate-features'." + (or (memq 'all php-ide-phpactor-activate-features) + (memq feature php-ide-phpactor-activate-features))) + +;;;###autoload +(defun php-ide-phpactor-activate () + "Activate PHP-IDE using phpactor.el." + (interactive) + (when (php-ide-phpactor--feature-activated-p 'navigation) + (if (not (bound-and-true-p phpactor-smart-jump-initialized)) + (local-set-key [remap xref-find-definitions] #'phpactor-goto-definition) + (local-set-key [remap xref-find-definitions] #'smart-jump-go) + (local-set-key [remap xref-pop-marker-stack] #'smart-jump-back) + (local-set-key [remap xref-find-references] #'smart-jump-references))) + (when (php-ide-phpactor--feature-activated-p 'hover) + (unless php-ide-phpactor-timer + (setq php-ide-phpactor-timer (run-with-timer 0.8 0.8 #'php-ide-phpactor-hover)))) + (setq php-ide-phpactor-buffer t)) + +;;;###autoload +(defun php-ide-phpactor-deactivate () + "Dectivate PHP-IDE using phpactor.el." + (interactive) + (local-unset-key [remap xref-find-definitions]) + (local-unset-key [remap xref-pop-marker-stack]) + (local-unset-key [remap xref-find-references]) + + (when php-ide-phpactor-timer + (cancel-timer php-ide-phpactor-timer) + (setq php-ide-phpactor-timer nil)) + (setq php-ide-phpactor-buffer nil)) + +(provide 'php-ide-phpactor) +;;; php-ide-phpactor.el ends here diff --git a/lisp/php-ide.el b/lisp/php-ide.el new file mode 100644 index 00000000..bd61e6ad --- /dev/null +++ b/lisp/php-ide.el @@ -0,0 +1,240 @@ +;;; 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.24.0 +;; 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) + +(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-loop for function in functions + always (functionp function))))) + +;;;###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