;;; oni-php.el --- PHP Configuration -*- lexical-binding: t; -*- ;; Copyright (C) 2019 Tom Willemse ;; Author: Tom Willemse ;; Keywords: local ;; Version: 2026.0514.120954 ;; Package-Requires: (oni-yasnippet oni-flycheck oni-hydra oni-corfu ggtags fic-mode rainbow-delimiters rainbow-identifiers) ;; 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: ;; My PHP mode configuration. Includes rules for aligning variables according to ;; the WordPress coding standards. ;;; Code: (require 'align) (require 'hydra) (require 'map) (require 'project) (require 'whitespace) (require 'yasnippet) (defconst oni-php-root (file-name-directory (or load-file-name (buffer-file-name))) "The directory where ‘oni-php’ was loaded from.") (defconst oni-php-snippets-dir (expand-file-name "snippets" oni-php-root) "The directory where ‘oni-php’ stores its snippets.") (defconst oni-php-scripts-dir (expand-file-name "scripts" oni-php-root) "The directory where ‘oni-php’ stores its scripts.") (defun oni-php-snippets-initialize () "Initialize the snippets for ‘oni-php’." (when (boundp 'yas-snippet-dirs) (add-to-list 'yas-snippet-dirs oni-php-snippets-dir)) (yas-load-directory oni-php-snippets-dir)) (defun oni-php--set-require-final-newline () "Set `require-final-newline' to t. This is necessary because the PHP mode configuration sets this to nil for some reason." (setq require-final-newline t)) (defun oni-php--whitespace-mode () "Enable whitespace mode with only tabs showing." (setq-local whitespace-style '(face tabs)) (whitespace-mode)) (defun oni-php--auto-fill-mode () "Enable ‘auto-fill-mode’ only for comments." (setq-local comment-auto-fill-only-comments t) (auto-fill-mode)) (defun oni-php-set-rainbow-identifier-faces () "Set the faces to override for ‘rainbow-identifiers-mode’." (setq-local rainbow-identifiers-faces-to-override '(font-lock-type-face php-property-name php-function-call-traditional php-doc-variable-sigil php-method-call-traditional php-variable-sigil php-variable-name))) (defun oni-php-add-use (class) "Try to add a use statement for CLASS under point." (interactive (list (thing-at-point 'symbol))) (let* ((default-directory (project-root (project-current))) (classes (read (shell-command-to-string (concat (expand-file-name "find-php-class" oni-php-scripts-dir) " " class)))) (class (cond ((null classes) (user-error "Class ‘%s’ not found" class)) ((> (length classes) 1) (completing-read "Use: " classes nil t)) (t (car classes))))) (save-excursion (goto-char (point-min)) (if (search-forward "use " nil t) (forward-paragraph) (search-forward "namespace ") (forward-line) (insert "\n")) (unless (re-search-backward (rx "use " (literal class) ";") nil t) (let ((start (point))) (insert "use " class ";\n") (pulse-momentary-highlight-region start (point))))))) (defun oni-php--syntax-in-string-p (syntax) "Does SYNTAX indicate point is inside a string?" (nth 3 syntax)) (defun oni-php--syntax-in-comment-p (syntax) "Does SYNTAX indicate point is inside a comment?" (nth 4 syntax)) (defun oni-php--in-string-or-comment-p () "Return whether or not point is within a string or comment." (let ((syntax (syntax-ppss))) (or (oni-php--syntax-in-string-p syntax) (oni-php--syntax-in-comment-p syntax)))) (defun oni-php-insert-dot-dwim (N) "Insert either a concatenation or access operator depending on context. Do the insert N times." (interactive "p") (if (or (oni-php--in-string-or-comment-p) (save-excursion (skip-syntax-backward " ") (nth 3 (syntax-ppss (1- (point))))) (save-excursion (skip-syntax-forward " ") (nth 3 (syntax-ppss (1+ (point)))))) (self-insert-command N) (let ((op (if (looking-back (rx (or "$" ")") (zero-or-more (any whitespace "\n" alnum "(" ")" "->"))) (save-excursion (backward-paragraph) (point))) "->" "::"))) (dotimes (_ N) (insert op))))) (defun oni-php-doc-comment () "Insert a PHP documentation comment at point." (interactive) (let ((start (point))) (insert "/**\n * ") (let ((insert-marker (point-marker))) (insert "\n */") (indent-region start (point)) (goto-char insert-marker)))) (defun oni-php-doc-use-comment () "Insert a PHP documentation comment for a use statement." (interactive) (let ((start (point)) (class (save-excursion (save-match-data (search-forward-regexp (rx "use " (group (minimal-match (zero-or-more alnum))) ";")) (match-string 1))))) (insert "/**\n * @use " class) (let ((insert-marker (point-marker))) (insert "\n */") (indent-region start (point)) (goto-char insert-marker)))) (defun oni-php-comment-dwim (func &rest args) "See if a PHP documentation comment should be added and add it. Otherwise call FUNC with ARGS. This is meant as advice around ‘comment-dwim’ to make it smarter for PHP code." (cond ((looking-at (rx (minimal-match (zero-or-more (any whitespace "\n"))) "use ")) (oni-php-doc-use-comment)) ((and (derived-mode-p 'php-mode) (not (region-active-p)) (looking-back (rx (minimal-match (zero-or-more blank))) (line-beginning-position)) (looking-at (rx (minimal-match (zero-or-more (any whitespace "\n"))) (or (regexp php-beginning-of-defun-regexp) (regexp php--re-classlike-pattern))))) (oni-php-doc-comment)) (t (apply func args)))) (defun oni-php-generate-namespace () (string-join (mapcar (lambda (s) (let ((case-fold-search nil)) (if (string-match-p (rx upper-case) s) s (capitalize s)))) (cdr (split-string (directory-file-name (file-name-directory (file-relative-name buffer-file-name (project-root (project-current))))) "/"))) "\\")) (defun oni-php-set-dabbrev-settings () "Set any settings for dabbrev that make sense for PHP code." (setq-local dabbrev-abbrev-skip-leading-regexp (rx "$"))) (defun oni-php-ignore-error-at-point (error-type) (interactive (list (let* ((error-types (seq-uniq (mapcar (lambda (e) (let ((str (flycheck-error-message e))) (string-match (rx "🪪" (one-or-more whitespace) (group (one-or-more (any alnum ".")))) str) (match-string 1 str))) (seq-filter (lambda (e) (not (flycheck-error-checker e))) (flycheck-overlay-errors-at (point)))) 'string=))) (if (> 1 (length error-types)) (completing-read "Ignore type: " error-types) (car error-types))))) (save-excursion (goto-char (line-beginning-position)) (indent-for-tab-command) (insert "/** @phpstan-ignore " error-type " */\n") (indent-for-tab-command))) (defhydra php-mode-hydra (:color blue) ("a" align-current "Align current selection")) (advice-add 'comment-dwim :around #'oni-php-comment-dwim) (add-hook 'php-mode-hook #'oni-php--set-require-final-newline) (add-hook 'php-mode-hook #'oni-php--whitespace-mode) (add-hook 'php-mode-hook 'display-fill-column-indicator-mode) (add-hook 'php-mode-hook 'electric-indent-local-mode) (add-hook 'php-mode-hook 'fic-mode) (add-hook 'php-mode-hook 'flycheck-mode) (add-hook 'php-mode-hook 'ggtags-mode) (add-hook 'php-mode-hook 'oni-php--auto-fill-mode) (add-hook 'php-mode-hook 'oni-php-set-dabbrev-settings) (add-hook 'php-mode-hook 'oni-php-set-rainbow-identifier-faces) (add-hook 'php-mode-hook 'rainbow-delimiters-mode) (add-hook 'php-mode-hook 'rainbow-identifiers-mode) (add-hook 'php-mode-hook 'smartparens-mode) (add-hook 'php-mode-hook 'subword-mode) (add-hook 'php-mode-hook 'yas-minor-mode) (add-hook 'php-mode-hook 'corfu-mode) (define-key php-mode-map (kbd "C-c m") #'php-mode-hydra/body) (define-key php-mode-map (kbd ".") #'oni-php-insert-dot-dwim) ;; In PHP code it's nice to have any ~=>~ aligned. ;; 'bar', ;; 'frob' => 'baz' ;; ); ;; ?> (add-to-list 'align-rules-list `(php-array-arrow (regexp . ,(rx any (group (zero-or-more whitespace)) "=>" any)) (group . (1)) (modes . '(php-mode web-mode php-mode)) (repeat . t))) ;; The WordPress coding standards specify that multiple assignments ;; should have their assignment operators aligned. ;; _ "\n" "}\n") (map-elt auto-insert-alist (rx "Test.php" eos)) '(nil " "use RefreshDatabase;\n" > "use WithFaker;\n" "\n" > "/** @test */\n" > "public function " (replace-regexp-in-string (rx whitespace) "_" (downcase (skeleton-read "Test function name: "))) "(): void\n" "{" > "\n" > _ "\n" "}" > "\n" "}"))) (defun oni-php-in-expression-context-p () (not (oni-php-in-static-call-context-p))) (defun oni-php-in-test-file () (string-suffix-p "Test.php" buffer-file-name)) (defun oni-php-in-static-call-context-p () (looking-back "::\\w+" (- (point) (line-beginning-position)))) (defun oni-php-grep-symbol (symbol) "Use ‘rgrep’ to find SYMBOL. If there is an active region when this command is called interactively SYMBOL will be the text in the region. Otherwise it's the symbol at point." (interactive (list (if (region-active-p) (buffer-substring-no-properties (region-beginning) (region-end)) (thing-at-point 'symbol)))) (rgrep (rx (literal symbol)) (map-elt grep-files-aliases "php") (project-root (project-current)))) (define-key php-mode-map (kbd "C-c .") nil t) (define-key php-mode-map (kbd "C-c . g") '("Search for symbol at point" . oni-php-grep-symbol)) (define-key php-mode-map (kbd "C-c . a") '("Import symbol at point" . oni-php-add-use)) (define-key php-mode-map (kbd "C-c . i") '("Ignore error at point" . oni-php-ignore-error-at-point)) (provide 'oni-php) ;;; oni-php.el ends here