--- /dev/null
+;;; php-ts-mode.el --- Major mode for editing PHP files using tree-sitter -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Vincenzo Pupillo <v.pupillo@gmail.com>
+;; Maintainer: Vincenzo Pupillo <v.pupillo@gmail.com>
+;; Created: Jun 2024
+;; Keywords: PHP language tree-sitter
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This package provides `php-ts-mode' which is a major mode
+;; for editing PHP files with embedded HTML, JavaScript, CSS and phpdoc.
+;; Tree Sitter is used to parse each of these languages.
+;;
+;; Please note that this package requires `html-ts-mode', which
+;; registers itself as the major mode for editing HTML.
+;;
+;; This package is compatible and has been tested with the following
+;; tree-sitter grammars:
+;; * https://github.com/tree-sitter/tree-sitter-php
+;; * https://github.com/tree-sitter/tree-sitter-html
+;; * https://github.com/tree-sitter/tree-sitter-javascript
+;; * https://github.com/tree-sitter/tree-sitter-css
+;; * https://github.com/claytonrcarter/tree-sitter-phpdoc
+;;
+;; Features
+;;
+;; * Indent
+;; * IMenu
+;; * Navigation
+;; * Which-function
+;; * Flymake
+;; * Tree-sitter parser installation helper
+;; * PHP built-in server support
+;; * Shell interaction: execute PHP code in a inferior PHP process
+
+;;; Code:
+
+(require 'treesit)
+(require 'c-ts-common) ;; For comment indent and filling.
+(require 'css-mode) ;; for embed css into html
+(require 'js) ;; for embed javascript into html
+(require 'comint)
+
+(eval-when-compile
+ (require 'cl-lib)
+ (require 'rx)
+ (require 'subr-x))
+
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-node-end "treesit.c")
+(declare-function treesit-node-parent "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-node-string "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+(declare-function treesit-parser-add-notifier "treesit.c")
+(declare-function treesit-parser-buffer "treesit.c")
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-parser-included-ranges "treesit.c")
+(declare-function treesit-parser-list "treesit.c")
+(declare-function treesit-parser-language "treesit.c")
+
+;;; Install treesitter language parsers
+(defvar php-ts-mode--language-source-alist
+ '((php . ("https://github.com/tree-sitter/tree-sitter-php" "v0.22.5"))
+ (phpdoc . ("https://github.com/claytonrcarter/tree-sitter-phpdoc"))
+ (html . ("https://github.com/tree-sitter/tree-sitter-html" "v0.20.3"))
+ (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.21.2"))
+ (css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.21.0")))
+ "Treesitter language parsers required by `php-ts-mode'.
+You can customize this variable if you want to stick to a specific
+commit and/or use different parsers.")
+
+(defun php-ts-mode-install-parsers ()
+ "Install all the required treesitter parsers.
+`php-ts-mode--language-source-alist' defines which parsers to install."
+ (interactive)
+ (let ((treesit-language-source-alist php-ts-mode--language-source-alist))
+ (dolist (item php-ts-mode--language-source-alist)
+ (treesit-install-language-grammar (car item)))))
+
+;;; Custom variables
+
+(defgroup php-ts-mode nil
+ "Major mode for editing PHP files."
+ :prefix "php-ts-mode-"
+ :group 'languages)
+
+(defcustom php-ts-mode-indent-offset 4
+ "Number of spaces for each indentation step in `php-ts-mode'."
+ :tag "PHP indent offset"
+ :version "30.1"
+ :type 'integer
+ :safe 'integerp)
+
+(defcustom php-ts-mode-js-css-indent-offset 2
+ "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
+By default should have same value as `html-ts-mode-indent-offset'."
+ :tag "PHP javascript or css indent offset"
+ :version "30.1"
+ :type 'integer
+ :safe 'integerp)
+
+(defcustom php-ts-mode-php-executable (or (executable-find "php") "/usr/bin/php")
+ "The location of PHP executable."
+ :tag "PHP Executable"
+ :version "30.1"
+ :type 'file)
+
+(defcustom php-ts-mode-php-config nil
+ "The location of php.ini file.
+If nil the default one is used to run the embedded webserver or
+inferior PHP process."
+ :tag "PHP Init file"
+ :version "30.1"
+ :type 'file)
+
+(defcustom php-ts-mode-ws-hostname "localhost"
+ "The hostname that will be served by the PHP built-in webserver.
+If nil then `php-ts-mode-run-php-webserver' will ask you for the hostname.
+See `https://www.php.net/manual/en/features.commandline.webserver.php'."
+ :tag "PHP built-in web server hostname"
+ :version "30.1"
+ :type 'string
+ :safe 'stringp)
+
+(defcustom php-ts-mode-ws-port nil
+ "The port on which the PHP built-in webserver will listen.
+If nil `php-ts-mode-run-php-webserver' will ask you for the port number."
+ :tag "PHP built-in web server port"
+ :version "30.1"
+ :type 'integer
+ :safe 'integerp)
+
+(defcustom php-ts-mode-ws-document-root nil
+ "The root of the documents that the PHP built-in webserver will serve.
+If nil `php-ts-mode-run-php-webserver' will ask you for the document root."
+ :tag "PHP built-in web server document root"
+ :version "30.1"
+ :type 'directory)
+
+(defcustom php-ts-mode-ws-workers nil
+ "The number of workers the PHP built-in webserver will fork.
+Useful for testing code against multiple simultaneous requests."
+ :tag "PHP built-in number of workers"
+ :version "30.1"
+ :type 'integer
+ :safe 'integerp)
+
+(defcustom php-ts-mode-inferior-php-buffer "*PHP*"
+ "Name of the inferior PHP buffer."
+ :tag "PHP inferior process buffer name"
+ :version "30.1"
+ :type 'string
+ :safe 'stringp)
+
+(defcustom php-ts-mode-inferior-history nil
+ "File used to save command history of the inferior PHP process."
+ :tag "PHP inferior process history file."
+ :version "30.1"
+ :type '(choice (const :tag "None" nil) file)
+ :safe 'string-or-null-p)
+
+(defvar php-ts-mode--inferior-prompt "php >"
+ "Prompt used by PHP inferior process.")
+
+(defun php-ts-mode--indent-style-setter (sym val)
+ "Custom setter for `php-ts-mode-set-style'.
+
+Apart from setting the default value of SYM to VAL, also change
+the value of SYM in `php-ts-mode' buffers to VAL.
+SYM should be `php-ts-mode-indent-style', and VAL should be a style
+symbol."
+ (set-default sym val)
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (when (derived-mode-p 'php-ts-mode)
+ (php-ts-mode-set-style val)))))
+
+;; teken from c-ts-mode
+(defun php-ts-indent-style-safep (style)
+ "Non-nil if STYLE's value is safe for file-local variables."
+ (and (symbolp style) (not (functionp style))))
+
+(defcustom php-ts-mode-indent-style 'psr2
+ "Style used for indentation.
+The selected style could be one of:
+`PSR-2/PSR-12' - use PSR standards (PSR-2, PSR-12), thi is the default.
+`PEAR' - use coding styles preferred for PEAR code and modules.
+`Drupal' - use coding styles preferred for working with Drupal projects.
+`WordPress' - use coding styles preferred for working with WordPress projects.
+`Symfony' - use coding styles preferred for working with Symfony projects.
+`Zend' - use coding styles preferred for working with Zend projects.
+
+If one of the supplied styles doesn't suffice, a function could be
+set instead. This function is expected return a list that
+follows the form of `treesit-simple-indent-rules'."
+ :tag "PHP indent style"
+ :version "30.1"
+ :type '(choice (const :tag "PSR-2/PSR-12" psr2)
+ (const :tag "PEAR" pear)
+ (const :tag "Drupal" drupal)
+ (const :tag "WordPress" wordpress)
+ (const :tag "Symfony" symfony)
+ (const :tag "Zend" zend)
+ (function :tag "A function for user customized style" ignore))
+ :set #'php-ts-mode--indent-style-setter
+ :safe #'php-ts-indent-style-safep)
+
+\f
+;;; Flymake integration
+
+;; based on lua-ts-mode
+(defvar-local php-ts-mode--flymake-process nil
+ "Store the Flymake process.")
+
+;; TODO: add phpmd and phpcs
+(defun php-ts-mode-flymake-php (report-fn &rest _args)
+ "PHP backend for Flymake.
+Calls REPORT-FN directly."
+ (when (process-live-p php-ts-mode--flymake-process)
+ (kill-process php-ts-mode--flymake-process))
+ (let ((source (current-buffer))
+ (diagnostics-pattern (eval-when-compile
+ (rx bol (? "PHP ") ;; every dignostic line start with PHP
+ (group (or "Fatal" "Parse")) ;; 1: type
+ " error:" (+ (syntax whitespace))
+ (group (+? any)) ;; 2: msg
+ " in " (group (+? any)) ;; 3: file
+ " on line " (group (+ num)) ;; 4: line
+ eol))))
+ (save-restriction
+ (widen)
+ (setq php-ts-mode--flymake-process
+ (make-process
+ :name "php-ts-mode-flymake"
+ :noquery t
+ :connection-type 'pipe
+ :buffer (generate-new-buffer " *php-ts-mode-flymake*")
+ :command `(,php-ts-mode-php-executable
+ "-l" "-d" "display_errors=0")
+ :sentinel
+ (lambda (proc _event)
+ (when (eq 'exit (process-status proc))
+ (unwind-protect
+ (if (with-current-buffer source
+ (eq proc php-ts-mode--flymake-process))
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-min))
+ (let (diags)
+ (while (search-forward-regexp
+ diagnostics-pattern
+ nil t)
+ (let* ((beg
+ (car (flymake-diag-region
+ source
+ (string-to-number (match-string 4)))))
+ (end
+ (cdr (flymake-diag-region
+ source
+ (string-to-number (match-string 4)))))
+ (msg (match-string 2))
+ (type :error))
+ (push (flymake-make-diagnostic
+ source beg end type msg)
+ diags)))
+ (funcall report-fn diags)))
+ (flymake-log :warning "Canceling obsolete check %s" proc))
+ (kill-buffer (process-buffer proc)))))))
+ (process-send-region php-ts-mode--flymake-process (point-min) (point-max))
+ (process-send-eof php-ts-mode--flymake-process))))
+
+\f
+;;; Utils
+
+(defun php-ts-mode--get-indent-style ()
+ "Helper function to set indentation style.
+MODE can be `psr2', `pear', `drupal', `wordpress', `symfony', `zend'."
+ (let ((style
+ (if (functionp php-ts-mode-indent-style)
+ (funcall php-ts-mode-indent-style)
+ (cl-case php-ts-mode-indent-style
+ (psr2 (alist-get 'psr2 (php-ts-mode--indent-styles)))
+ (pear (alist-get 'pear (php-ts-mode--indent-styles)))
+ (drupal (alist-get 'drupal (php-ts-mode--indent-styles)))
+ (wordpress (alist-get 'wordpress (php-ts-mode--indent-styles)))
+ (symfony (alist-get 'symfony (php-ts-mode--indent-styles)))
+ (zend (alist-get 'zend (php-ts-mode--indent-styles)))
+ (t (alist-get 'psr2 (php-ts-mode--indent-styles)))))))
+ `((php ,@style))))
+
+(defun php-ts-mode--prompt-for-style ()
+ "Prompt for an indent style and return the symbol for it."
+ (intern
+ (completing-read
+ "Style: "
+ (mapcar #'car (php-ts-mode--indent-styles))
+ nil t nil nil "default")))
+
+(defun php-ts-mode-set-global-style (style)
+ "Set the indent style of PHP modes globally to STYLE.
+
+This changes the current indent style of every PHP buffer and
+the default PHP indent style for `php-ts-mode'
+in this Emacs session."
+ (interactive (list (php-ts-mode--prompt-for-style)))
+ (php-ts-mode--indent-style-setter 'php-ts-mode-indent-style style))
+
+(defun php-ts-mode--set-indent-property (style)
+ "Set the offset, tab, etc. according to STYLE."
+ (cl-case style
+ (psr2 (setq php-ts-mode-indent-offset 4
+ tab-width 4
+ indent-tabs-mode nil))
+ (pear (setq php-ts-mode-indent-offset 4
+ tab-width 4
+ indent-tabs-mode nil))
+ (drupal (setq php-ts-mode-indent-offset 2
+ tab-width 2
+ indent-tabs-mode nil))
+ (wordpress (setq php-ts-mode-indent-offset 4
+ tab-width 4
+ indent-tabs-mode t))
+ (symfony (setq php-ts-mode-indent-offset 4
+ tab-width 4
+ indent-tabs-mode nil))
+ (zend (setq php-ts-mode-indent-offset 4
+ tab-width 4
+ indent-tabs-mode nil))))
+
+(defun php-ts-mode-set-style (style)
+ "Set the PHP indent style of the current buffer to STYLE.
+To set the default indent style globally, use
+`php-ts-mode-set-global-style'."
+ (interactive (list (php-ts-mode--prompt-for-style)))
+ (cond
+ ((not (derived-mode-p 'php-ts-mode))
+ (user-error "The current buffer is not in `php-ts-mode'"))
+ ((equal php-ts-mode-indent-style style)
+ (message "The style is already %s" style));; nothing to do
+ (t (progn
+ (setq-local php-ts-mode-indent-style style)
+ (php-ts-mode--set-indent-property style)
+ (let ((rules (assq-delete-all 'php treesit-simple-indent-rules))
+ (new-style (car (treesit--indent-rules-optimize
+ (php-ts-mode--get-indent-style)))))
+ (setq treesit-simple-indent-rules (cons new-style rules))
+ (message "Switch to %s style" style))))))
+
+(defun php-ts-mode--get-parser-ranges ()
+ "Return the ranges covered by the parsers.
+
+`php-ts-mode' use five parsers, this function returns, for the
+current buffer, the ranges covered by each parser.
+Usefull for debugging."
+ (let ((ranges)
+ (parsers (treesit-parser-list nil nil t)))
+ (if (not parsers)
+ (message "At least one parser must be initialized"))
+ (cl-loop
+ for parser in parsers
+ do (push (list parser (treesit-parser-included-ranges parser)) ranges)
+ finally return ranges)))
+
+\f
+;;; Syntax table
+
+(defvar php-ts-mode--syntax-table
+ (let ((table (make-syntax-table)))
+ (modify-syntax-entry ?\\ "\\" table)
+ (modify-syntax-entry ?+ "." table)
+ (modify-syntax-entry ?- "." table)
+ (modify-syntax-entry ?= "." table)
+ (modify-syntax-entry ?% "." table)
+ (modify-syntax-entry ?< "." table)
+ (modify-syntax-entry ?> "." table)
+ (modify-syntax-entry ?& "." table)
+ (modify-syntax-entry ?| "." table)
+ (modify-syntax-entry ?\' "\"" table)
+ (modify-syntax-entry ?\240 "." table)
+ (modify-syntax-entry ?/ ". 124b" table)
+ (modify-syntax-entry ?* ". 23" table)
+ (modify-syntax-entry ?\n "> b" table)
+ (modify-syntax-entry ?\^m "> b" table)
+ ;; php specific syntax
+ (modify-syntax-entry ?_ "w" table)
+ (modify-syntax-entry ?` "\"" table)
+ (modify-syntax-entry ?\" "\"" table)
+ (modify-syntax-entry ?\r "> b" table)
+ (modify-syntax-entry ?# "< b" table)
+ (modify-syntax-entry ?$ "_" table)
+ table)
+ "Syntax table for `php-ts-mode'.")
+
+\f
+;;; Indent
+
+;; taken from c-ts-mode
+(defun php-ts-mode--else-heuristic (node parent bol &rest _)
+ "Heuristic matcher for when \"else\" is followed by a closing bracket.
+
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (and (null node)
+ (save-excursion
+ (forward-line -1)
+ (looking-at (rx (* whitespace) "else" (* whitespace) eol)))
+ (let ((next-node (treesit-node-first-child-for-pos parent bol)))
+ (equal (treesit-node-type next-node) "}"))))
+
+;; taken from c-ts-mode
+(defun php-ts-mode--first-sibling (node parent &rest _)
+ "Matches when NODE is the \"first sibling\".
+
+\"First sibling\" is defined as: the first child node of PARENT
+such that it's on its own line. NODE is the node to match and
+PARENT is its parent."
+ (let ((prev-sibling (treesit-node-prev-sibling node t)))
+ (or (null prev-sibling)
+ (save-excursion
+ (goto-char (treesit-node-start prev-sibling))
+ (<= (line-beginning-position)
+ (treesit-node-start parent)
+ (line-end-position))))))
+
+(defun php-ts-mode--js-css-tag-bol (node _parent &rest _)
+ "Find the first non-space caracters of html tags <script> or <style>.
+
+If NODE is nil return `line-beginning-position'. PARENT is ignored.
+NODE is the node to match and PARENT is its parent."
+ (if (null node)
+ (line-beginning-position)
+ (save-excursion
+ (goto-char (treesit-node-start node))
+ (re-search-backward "<script>\\|<style>" nil t))))
+
+(defun php-ts-mode--parent-eol (_node parent &rest _)
+ "Find the last non-space caracters of the PARENT of the current NODE.
+
+NODE is the node to match and PARENT is its parent."
+ (save-excursion
+ (goto-char (treesit-node-start parent))
+ (line-end-position)))
+
+(defun php-ts-mode--parent-html-bol (node parent _bol &rest _)
+ "Find the first non-space characters of the HTML tags before NODE.
+
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (save-excursion
+ (let ((html-node (treesit-search-forward node "text" t)))
+ (if html-node
+ (let ((end-html (treesit-node-end html-node)))
+ (goto-char end-html)
+ (backward-word)
+ (back-to-indentation)
+ (point))
+ (treesit-node-start parent)))))
+
+(defun php-ts-mode--parent-html-heuristic (node parent _bol &rest _)
+ "Returns position based on html indentation.
+
+Returns 0 if the NODE is after the </html>, otherwise returns the
+indentation point of the last word before the NODE, plus the
+indentation offset. If there is no HTML tag, it returns the beginning
+of the parent.
+It can be used when you want to indent PHP code relative to the HTML.
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (let ((html-node (treesit-search-forward node "text" t)))
+ (if html-node
+ (let ((end-html (treesit-node-end html-node)))
+ (save-excursion
+ (goto-char end-html)
+ (backward-word)
+ (back-to-indentation)
+ (if (search-forward "</html>" end-html t 1)
+ 0
+ (+ (point) php-ts-mode-indent-offset))))
+ ;; forse è meglio usare bol, leggi la documentazione!!!
+ (treesit-node-start parent))))
+
+(defun php-ts-mode--array-element-heuristic (_node parent _bol &rest _)
+ "Return of the position of the first element of the array.
+
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (let ((parent-start
+ (treesit-node-start parent))
+ (parent-first-child-start
+ (treesit-node-start (treesit-node-child parent 2))))
+ (if (equal
+ (line-number-at-pos parent-start)
+ (line-number-at-pos parent-first-child-start))
+ ;; if array_creation_expression and the first
+ ;; array_element_initializer are on the same same line
+ parent-first-child-start
+ ;; else return parent-bol plus the offset
+ (save-excursion
+ (goto-char (treesit-node-start parent))
+ (back-to-indentation)
+ (+ (point) php-ts-mode-indent-offset)))))
+
+
+(defun php-ts-mode--anchor-first-sibling (_node parent _bol &rest _)
+ "Return the start of the first child of a sibling of PARENT.
+
+If the fist sibling of PARENT and the first child of the sibling are
+on the same line return the start position of the firt child of the
+sibling. Otherwise return the start of the first sibling.
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (let ((first-sibling-start
+ (treesit-node-start (treesit-node-child parent 0)))
+ (first-sibling-child-start
+ (treesit-node-start (treesit-node-child parent 1))))
+ (if (equal
+ (line-number-at-pos first-sibling-start)
+ (line-number-at-pos first-sibling-child-start))
+ ;; if are on the same line return the child start
+ first-sibling-child-start
+ first-sibling-start)))
+
+;; adapted from c-ts-mode--anchor-prev-sibling
+(defun php-ts-mode--anchor-prev-sibling (node parent bol &rest _)
+ "Return the start of the previous named sibling of NODE.
+
+Return nil if a) there is no prev-sibling, or b) prev-sibling
+doesn't have a child.
+
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+ (when-let ((prev-sibling
+ (or (treesit-node-prev-sibling node t)
+ (treesit-node-prev-sibling
+ (treesit-node-first-child-for-pos parent bol) t)
+ (treesit-node-child parent -1 t)))
+ (continue t))
+ (save-excursion
+ (while (and prev-sibling continue)
+ (goto-char (treesit-node-start prev-sibling))
+ (if (looking-back (rx bol (* whitespace))
+ (line-beginning-position))
+ (setq continue nil)
+ (setq prev-sibling
+ (treesit-node-prev-sibling prev-sibling)))))
+ (treesit-node-start prev-sibling)))
+
+(defun php-ts-mode--indent-styles ()
+ "Indent rules supported by `php-ts-mode'."
+ (let ((common
+ `((php-ts-mode--else-heuristic prev-line php-ts-mode-indent-offset)
+
+ ((query "(ERROR (ERROR)) @indent") column-0 0)
+
+ ((node-is ")") parent-bol 0)
+ ((node-is "]") parent-bol 0)
+ ((node-is "else_clause") parent-bol 0)
+ ((node-is "case_statement") parent-bol php-ts-mode-indent-offset)
+ ((node-is "default_statement") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "default_statement") parent-bol php-ts-mode-indent-offset)
+ ((and
+ (parent-is "expression_statement")
+ (node-is ";"))
+ parent-bol 0)
+ ((parent-is "expression_statement") parent-bol php-ts-mode-indent-offset)
+ ;; `c-ts-common-looking-at-star' has to come before
+ ;; `c-ts-common-comment-2nd-line-matcher'.
+ ((and (parent-is "comment") c-ts-common-looking-at-star)
+ c-ts-common-comment-start-after-first-star -1)
+ (c-ts-common-comment-2nd-line-matcher
+ c-ts-common-comment-2nd-line-anchor
+ 1)
+ ((parent-is "comment") prev-adaptive-prefix 0)
+
+ ((parent-is "method_declaration") parent-bol 0)
+ ((node-is "class_interface_clause") parent-bol php-ts-mode-indent-offset)
+ ((query "(class_interface_clause (name) @indent)") php-ts-mode--parent-eol 1)
+ ((query "(class_interface_clause (qualified_name) @indent)")
+ parent-bol php-ts-mode-indent-offset)
+ ((parent-is "class_declaration") parent-bol 0)
+ ((parent-is "namespace_use_group") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "function_definition") parent-bol 0)
+ ((parent-is "member_call_expression") first-sibling php-ts-mode-indent-offset)
+ ((parent-is "conditional_expression") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "assignment_expression") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "array_creation_expression") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "parenthesized_expression") first-sibling 1)
+ ((parent-is "binary_expression") parent 0)
+ ((or (parent-is "arguments")
+ (parent-is "formal_parameters"))
+ parent-bol php-ts-mode-indent-offset)
+
+ ((query "(for_statement (assignment_expression left: (_)) @indent)")
+ parent-bol php-ts-mode-indent-offset)
+ ((query "(for_statement (binary_expression left: (_)) @indent)")
+ parent-bol php-ts-mode-indent-offset)
+ ((query "(for_statement (update_expression (_)) @indent)")
+ parent-bol php-ts-mode-indent-offset)
+ ((query "(function_call_expression arguments: (_) @indent)")
+ parent php-ts-mode-indent-offset)
+ ((query "(member_call_expression arguments: (_) @indent)")
+ parent php-ts-mode-indent-offset)
+ ((query "(scoped_call_expression name: (_) @indent)")
+ parent php-ts-mode-indent-offset)
+ ((parent-is "scoped_property_access_expression")
+ parent php-ts-mode-indent-offset)
+
+ ;; Closing bracket. Must stay here, the rule order matter.
+ ((node-is "}") standalone-parent 0)
+ ;; handle multiple single line comment that start at the and of a line
+ ((match "comment" "declaration_list") php-ts-mode--anchor-prev-sibling 0)
+ ((parent-is "declaration_list") column-0 php-ts-mode-indent-offset)
+
+ ((parent-is "initializer_list") parent-bol php-ts-mode-indent-offset)
+
+ ;; Statement in {} blocks.
+ ((or (and (parent-is "compound_statement")
+ ;; If the previous sibling(s) are not on their
+ ;; own line, indent as if this node is the first
+ ;; sibling
+ php-ts-mode--first-sibling)
+ (match null "compound_statement"))
+ standalone-parent php-ts-mode-indent-offset)
+ ((parent-is "compound_statement") parent-bol php-ts-mode-indent-offset)
+ ;; Opening bracket.
+ ((node-is "compound_statement") standalone-parent php-ts-mode-indent-offset)
+
+ ((parent-is "match_block") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "switch_block") parent-bol 0)
+
+ ;; These rules are for cases where the body is bracketless.
+ ((match "while" "do_statement") parent-bol 0)
+ ((or (parent-is "if_statement")
+ (parent-is "else_clause")
+ (parent-is "for_statement")
+ (parent-is "foreach_statement")
+ (parent-is "while_statement")
+ (parent-is "do_statement")
+ (parent-is "switch_statement")
+ (parent-is "case_statement")
+ (parent-is "empty_statement"))
+ parent-bol php-ts-mode-indent-offset))))
+ `((psr2
+ ((parent-is "program") parent-bol 0)
+ ((parent-is "text_interpolation") column-0 0)
+ ((parent-is "function_call_expression") parent-bol php-ts-mode-indent-offset)
+ ,@common)
+ (pear
+ ((parent-is "program") php-ts-mode--parent-html-heuristic 0)
+ ((parent-is "text_interpolation") php-ts-mode--parent-html-heuristic 0)
+ ((or (node-is "case_statement")
+ (node-is "default_statement"))
+ parent-bol 0)
+ ((parent-is "binary_expression") parent-bol php-ts-mode-indent-offset)
+ ,@common)
+ (drupal
+ ((parent-is "program") php-ts-mode--parent-html-heuristic 0)
+ ((parent-is "text_interpolation") php-ts-mode--parent-html-bol 0)
+ ((parent-is "if_statement") parent-bol 0)
+ ((parent-is "binary_expression") parent-bol php-ts-mode-indent-offset)
+ ((parent-is "function_call_expression") parent-bol php-ts-mode-indent-offset)
+ ,@common)
+ (symfony
+ ((parent-is "function_call_expression") parent-bol php-ts-mode-indent-offset)
+ ,@common)
+ (wordpress
+ ((parent-is "program") php-ts-mode--parent-html-bol 0)
+ ((parent-is "text_interpolation") php-ts-mode--parent-html-bol 0)
+ ,@common)
+ (zend
+ ((parent-is "class_interface_clause") php-ts-mode--anchor-first-sibling 0)
+ ((parent-is "function_call_expression") first-sibling 0)
+ ((parent-is "array_creation_expression") php-ts-mode--array-element-heuristic 0)
+ ,@common))))
+
+(defvar php-ts-mode--phpdoc-indent-rules
+ '((phpdoc
+ ((and (parent-is "document") c-ts-common-looking-at-star)
+ c-ts-common-comment-start-after-first-star -1)
+ (c-ts-common-comment-2nd-line-matcher
+ c-ts-common-comment-2nd-line-anchor
+ 1)))
+ "Tree-sitter indentation rules for for `phpdoc'.")
+
+\f
+;;; Font-lock
+
+(defconst php-ts-mode--keywords
+ '("abstract" "and" "array" "as" "break" "callable" "case" "catch"
+ "class" "clone" "const" "continue" "declare" "default" "do" "echo"
+ "else" "elseif" "enddeclare" "endfor" "endforeach" "endif"
+ "endswitch" "endwhile" "enum" "extends" "final" "finally" "fn"
+ "for" "foreach" "from" "function" "global" "goto" "if" "implements"
+ "include" "include_once" "instanceof" "insteadof" "interface"
+ "list" "match" "namespace" "new" "null" "or" "print" "private"
+ "protected" "public" "readonly" "require" "require_once" "return"
+ "static" "switch" "throw" "trait" "try" "unset" "use" "while" "xor"
+ "yield")
+ "PHP keywords for tree-sitter font-locking.")
+
+(defconst php-ts-mode--operators
+ '("--" "**=" "*=" "/=" "%=" "+=" "-=" ".=" "<<=" ">>=" "&=" "^="
+ "|=" "??" "??=" "||" "&&" "|" "^" "&" "==" "!=" "<>" "===" "!=="
+ "<" ">" "<=" ">=" "<=>" "<<" ">>" "+" "-" "." "*" "**" "/" "%"
+ "->" "?->")
+ "PHP operators for tree-sitter font-locking.")
+
+(defconst php-ts-mode--predefined-constant
+ '(;; predefined constant
+ "PHP_VERSION" "PHP_MAJOR_VERSION" "PHP_MINOR_VERSION"
+ "PHP_RELEASE_VERSION" "PHP_VERSION_ID" "PHP_EXTRA_VERSION"
+ "ZEND_THREAD_SAFE" "ZEND_DEBUG_BUILD" "PHP_ZTS" "PHP_DEBUG"
+ "PHP_MAXPATHLEN" "PHP_OS" "PHP_OS_FAMILY" "PHP_SAPI" "PHP_EOL"
+ "PHP_INT_MAX" "PHP_INT_MIN" "PHP_INT_SIZE" "PHP_FLOAT_DIG"
+ "PHP_FLOAT_EPSILON" "PHP_FLOAT_MIN" "PHP_FLOAT_MAX"
+ "PHP_WINDOWS_EVENT_CTRL_C" "PHP_WINDOWS_EVENT_CTRL_BREAK"
+ "DEFAULT_INCLUDE_PATH" "PEAR_INSTALL_DIR" "PEAR_EXTENSION_DIR"
+ "PHP_EXTENSION_DIR" "PHP_PREFIX" "PHP_BINDIR" "PHP_BINARY"
+ "PHP_MANDIR" "PHP_LIBDIR" "PHP_DATADIR" "PHP_SYSCONFDIR"
+ "PHP_LOCALSTATEDIR" "PHP_CONFIG_FILE_PATH" "PHP_CONFIG_FILE_SCAN_DIR"
+ "PHP_SHLIB_SUFFIX" "PHP_FD_SETSIZE" "E_ERROR" "E_WARNING" "E_PARSE"
+ "E_NOTICE" "E_CORE_ERROR" "E_CORE_WARNING" "E_COMPILE_ERROR"
+ "E_COMPILE_WARNING" "E_USER_ERROR" "E_USER_WARNING"
+ "E_USER_NOTICE" "E_USER_NOTICE" "E_DEPRECATED" "E_USER_DEPRECATED"
+ "E_ALL" "E_STRICT"
+ ;; magic constant
+ "__COMPILER_HALT_OFFSET__" "__CLASS__" "__DIR__" "__FILE__"
+ "__FUNCTION__" "__LINE__" "__METHOD__" "__NAMESPACE__" "__TRAIT__")
+ "PHP predefined constant.")
+
+(defun php-ts-mode--font-lock-settings ()
+ "Tree-sitter font-lock settings."
+ (treesit-font-lock-rules
+
+ :language 'php
+ :feature 'keyword
+ :override t
+ `([,@php-ts-mode--keywords] @font-lock-keyword-face)
+
+ :language 'php
+ :feature 'comment
+ :override t
+ '((comment) @font-lock-comment-face)
+
+ :language 'php
+ :feature 'constant
+ `((boolean) @font-lock-constant-face
+ (null) @font-lock-constant-face
+ ;; predefined constant or built in constant
+ ((name) @font-lock-builtin-face
+ (:match ,(rx-to-string
+ `(: bos (or ,@php-ts-mode--predefined-constant) eos))
+ @font-lock-builtin-face))
+ ;; user defined constant
+ ((name) @font-lock-constant-face
+ (:match "_?[A-Z][0-9A-Z_]+" @font-lock-constant-face))
+ (const_declaration
+ (const_element (name) @font-lock-constant-face))
+ (relative_scope "self") @font-lock-builtin-face
+ ;; declare directive
+ (declare_directive ["strict_types" "encoding" "ticks"] @font-lock-constant-face))
+
+ :language 'php
+ :feature 'name
+ `((goto_statement (name) @font-lock-constant-face)
+ (named_label_statement (name) @font-lock-constant-face)
+ (expression_statement (name) @font-lock-keyword-face
+ (:equal "exit" @font-lock-keyword-face)))
+
+ :language 'php
+ ;;:override t
+ :feature 'delimiter
+ `((["," ":" ";" "\\"]) @font-lock-delimiter-face)
+
+ :language 'php
+ :feature 'operator
+ `([,@php-ts-mode--operators] @font-lock-operator-face)
+
+ :language 'php
+ :feature 'variable-name
+ :override t
+ `(((name) @font-lock-keyword-face (:equal "this" @font-lock-keyword-face))
+ (variable_name (name) @font-lock-variable-name-face)
+ (dynamic_variable_name (name) @font-lock-variable-name-face)
+ (member_access_expression
+ name: (_) @font-lock-variable-name-face)
+ (scoped_property_access_expression
+ scope: (name) @font-lock-constant-face)
+ (error_suppression_expression (name) @font-lock-variable-name-face))
+
+ :language 'php
+ :feature 'string
+ ;;:override t
+ `(("\"") @font-lock-string-face
+ (encapsed_string) @font-lock-string-face
+ (string_content) @font-lock-string-face
+ (string) @font-lock-string-face)
+
+ :language 'php
+ :feature 'literal
+ '((integer) @font-lock-number-face
+ (float) @font-lock-number-face
+ (heredoc identifier: (heredoc_start) @font-lock-constant-face)
+ (heredoc_body (string_content) @font-lock-string-face)
+ (heredoc end_tag: (heredoc_end) @font-lock-constant-face)
+ (nowdoc identifier: (heredoc_start) @font-lock-constant-face)
+ (nowdoc_body (nowdoc_string) @font-lock-string-face)
+ (nowdoc end_tag: (heredoc_end) @font-lock-constant-face)
+ (shell_command_expression) @font-lock-string-face)
+
+ :language 'php
+ :feature 'type
+ :override t
+ '((union_type) @font-lock-type-face
+ (bottom_type) @font-lock-type-face
+ (primitive_type) @font-lock-type-face
+ (cast_type) @font-lock-type-face
+ (named_type) @font-lock-type-face
+ (optional_type) @font-lock-type-face)
+
+ :language 'php
+ :feature 'definition
+ :override t
+ '((php_tag) @font-lock-preprocessor-face
+ ("?>") @font-lock-preprocessor-face
+ ;; Highlights identifiers in declarations.
+ (class_declaration
+ name: (_) @font-lock-type-face)
+ (class_interface_clause (name) @font-lock-type-face)
+ (interface_declaration
+ name: (_) @font-lock-type-face)
+ (trait_declaration
+ name: (_) @font-lock-type-face)
+ (property_declaration
+ (visibility_modifier) @font-lock-keyword-face)
+ (property_declaration
+ (var_modifier) @font-lock-keyword-face)
+ (enum_declaration
+ name: (_) @font-lock-type-face)
+ (function_definition
+ name: (_) @font-lock-function-name-face)
+ (method_declaration
+ name: (_) @font-lock-function-name-face)
+ ("=>") @font-lock-keyword-face
+ (object_creation_expression
+ (name) @font-lock-type-face)
+ (namespace_name_as_prefix (namespace_name (name)) @font-lock-type-face)
+ (namespace_use_clause (name) @font-lock-property-use-face)
+ (namespace_aliasing_clause (name) @font-lock-type-face)
+ (namespace_name (name) @font-lock-type-face)
+ (use_declaration (name) @font-lock-property-use-face))
+
+ :language 'php
+ :feature 'function-scope
+ :override t
+ '((relative_scope) @font-lock-constant-face
+ (scoped_call_expression
+ scope: (name) @font-lock-constant-face)
+ (class_constant_access_expression (name) @font-lock-constant-face))
+
+ :language 'php
+ :feature 'function-call
+ :override t
+ '((function_call_expression
+ function: (name) @font-lock-function-call-face)
+ (scoped_call_expression
+ name: (_) @font-lock-function-name-face)
+ (member_call_expression
+ name: (_) @font-lock-function-name-face)
+ (nullsafe_member_call_expression
+ name: (_) @font-lock-constant-face))
+
+ :language 'php
+ :feature 'argument
+ '((argument
+ name: (_) @font-lock-constant-face))
+
+ :language 'php
+ :feature 'escape-sequence
+ :override t
+ '((string (escape_sequence) @font-lock-escape-face)
+ (encapsed_string (escape_sequence) @font-lock-escape-face)
+ (heredoc_body (escape_sequence) @font-lock-escape-face))
+
+ :language 'php
+ :feature 'base-clause
+ :override t
+ '((base_clause (name) @font-lock-type-face)
+ (use_as_clause (name) @font-lock-property-use-face)
+ (qualified_name (name) @font-lock-constant-face))
+
+ :language 'php
+ :feature 'property
+ '((enum_case
+ name: (_) @font-lock-type-face))
+
+ :language 'php
+ :feature 'attribute
+ '((((attribute (_) @attribute_name) @font-lock-preprocessor-face)
+ (:equal "Deprecated" @attribute_name))
+ (attribute_group (attribute (name) @font-lock-constant-face)))
+
+ :language 'php
+ :feature 'bracket
+ '((["(" ")" "[" "]" "{" "}"]) @font-lock-bracket-face)
+
+ :language 'php
+ :feature 'error
+ :override t
+ '((ERROR) @php-ts-mode--fontify-error)))
+
+\f
+;;; Font-lock helpers
+
+(defconst php-ts-mode--custom-html-font-lock-settings
+ (treesit-font-lock-rules
+ :language 'html
+ :override t
+ :feature 'comment
+ `((comment) @font-lock-comment-face
+ ;; handle shebang path and others type of comment
+ (document (text) @font-lock-comment-face))
+
+ :language 'html
+ :override t
+ :feature 'keyword
+ `("doctype" @font-lock-keyword-face)
+
+ :language 'html
+ :override t
+ :feature 'definition
+ `((tag_name) @font-lock-function-name-face)
+
+ :language 'html
+ :override 'append
+ :feature 'string
+ `((quoted_attribute_value) @font-lock-string-face)
+
+ :language 'html
+ :override t
+ :feature 'property
+ `((attribute_name) @font-lock-variable-name-face))
+ "Tree-sitter font-lock settings for `php-html-ts-mode'.")
+
+(defvar php-ts-mode--phpdoc-font-lock-settings
+ (treesit-font-lock-rules
+ :language 'phpdoc
+ :feature 'document
+ :override t
+ '((document) @font-lock-doc-face)
+
+ :language 'phpdoc
+ :feature 'type
+ :override t
+ '((union_type
+ [(array_type) (primitive_type) (named_type) (optional_type)] @font-lock-type-face)
+ ([(array_type) (primitive_type) (named_type) (optional_type)] @font-lock-type-face)
+ (fqsen (name) @font-lock-function-name-face))
+
+ :language 'phpdoc
+ :feature 'attribute
+ :override t
+ `((tag_name) @font-lock-constant-face
+ (uri) @font-lock-doc-markup-face
+ (tag
+ [(version) (email_address)] @font-lock-doc-markup-face)
+ (tag (author_name) @font-lock-property-name-face))
+
+ :language 'phpdoc
+ :feature 'variable
+ :override t
+ '((variable_name (name) @font-lock-variable-name-face)))
+ "Tree-sitter font-lock settings for phpdoc.")
+
+(defun php-ts-mode--fontify-error (node override start end &rest _)
+ "Fontify the error nodes.
+For NODE, OVERRIDE, START, and END, see `treesit-font-lock-rules'."
+ (treesit-fontify-with-override
+ (treesit-node-start node) (treesit-node-end node)
+ 'font-lock-warning-face
+ override start end))
+
+(defun php-ts-mode--html-language-at-point (point)
+ "Return the language at POINT assuming the point is within a HTML region."
+ (let* ((node (treesit-node-at point 'html))
+ (parent (treesit-node-parent node))
+ (node-query (format "(%s (%s))"
+ (treesit-node-type parent)
+ (treesit-node-type node))))
+ (cond
+ ((string-equal "(script_element (raw_text))" node-query) 'javascript)
+ ((string-equal "(style_element (raw_text))" node-query) 'css)
+ (t 'html))))
+
+(defun php-ts-mode--language-at-point (point)
+ "Return the language at POINT."
+ (let* ((node (treesit-node-at point 'php))
+ (node-type (treesit-node-type node))
+ (parent (treesit-node-parent node))
+ (node-query (format "(%s (%s))" (treesit-node-type parent) node-type)))
+ (save-excursion
+ (goto-char (treesit-node-start node))
+ (cond
+ ((not (member node-query '("(program (text))"
+ "(text_interpolation (text))")))
+ 'php)
+ (t (php-ts-mode--html-language-at-point point))))))
+
+\f
+;;; Imenu
+
+(defun php-ts-mode--parent-object (node)
+ "Return the name of the object that own NODE."
+ (treesit-parent-until
+ node
+ (lambda (n)
+ (member (treesit-node-type n)
+ '("class_declaration"
+ "enum_declaration"
+ "function_definition"
+ "interface_declaration"
+ "method_declaration"
+ "namespace_definition"
+ "trait_declaration")))))
+
+(defun php-ts-mode--defun-name-separator (node)
+ "Return a separator to connect object name, based on NODE type."
+ (let ((node-type (treesit-node-type node)))
+ (cond ((member node-type '("function_definition" "method_declaration"))
+ "()::")
+ ((member node-type '("class_declaration" "enum_declaration" "trait_declaration"))
+ "::")
+ (t "\\"))))
+
+(defun php-ts-mode--defun-object-name (node node-text)
+ "Compose the full name of a NODE that is a PHP variable, method, class etc.
+If the NODE has a parent, it recursively concat the parent names with NODE-TEXT,
+otherwise it returns NODE-TEXT."
+ (let* ((parent-node (php-ts-mode--parent-object node))
+ (parent-node-text
+ (treesit-node-text
+ (treesit-node-child-by-field-name parent-node "name") t))
+ (parent-node-separator (php-ts-mode--defun-name-separator parent-node)))
+ (if parent-node
+ (progn
+ (setq parent-node-text
+ (php-ts-mode--defun-object-name
+ parent-node
+ parent-node-text))
+ (concat parent-node-text parent-node-separator node-text))
+ node-text)))
+
+(defun php-ts-mode--defun-name (node)
+ "Return the defun name of NODE.
+Return nil if the NODE has no field “name” or if NODE is not a defun node."
+ (let ((child (treesit-node-child-by-field-name node "name")))
+ (cl-case (intern (treesit-node-type node))
+ (class_declaration (treesit-node-text child t))
+ (trait_declaration (treesit-node-text child t))
+ (interface_declaration (treesit-node-text child t))
+ (namespace_definition (treesit-node-text child t))
+ (enum_declaration (treesit-node-text child t))
+ (function_definition (treesit-node-text child t))
+ (method_declaration
+ (php-ts-mode--defun-object-name node (treesit-node-text child t)))
+ (variable_name
+ (php-ts-mode--defun-object-name node (treesit-node-text node t)))
+ (const_element
+ (php-ts-mode--defun-object-name
+ node
+ (treesit-node-text (treesit-node-child node 0) t))))))
+
+\f
+;;; Defun navigation
+
+(defun php-ts-mode--indent-defun ()
+ "Indent the current top-level declaration syntactically.
+`treesit-defun-type-regexp' defines what constructs to indent."
+ (interactive "*")
+ (when-let ((orig-point (point-marker))
+ (node (treesit-defun-at-point)))
+ (indent-region (treesit-node-start node)
+ (treesit-node-end node))
+ (goto-char orig-point)))
+
+(defun php-ts-mode--defun-valid-p (node)
+ "Return non-nil if NODE is a valid defun node.
+Ie, NODE is not nested."
+ (not (and (member (treesit-node-type node)
+ '("variable_name"
+ "const_element"
+ "enum_declaration"
+ "union_declaration"
+ "declaration"))
+ ;; If NODE's type is one of the above, make sure it is
+ ;; top-level.
+ (treesit-node-top-level
+ node (rx (or "variable_name"
+ "const_element"
+ "function_definition"
+ "enum_declaration"
+ "union_declaration"
+ "declaration"))))))
+
+\f
+;;; Filling
+
+(defun php-ts-mode--comment-indent-new-line (&optional soft)
+ "Break line at point and indent, continuing comment if within one.
+Like `c-ts-common-comment-indent-new-line', but handle the
+less common PHP-style # comment. SOFT works the same as in
+`comment-indent-new-line'."
+ (if (save-excursion
+ ;; Line start with # or ## or ###...
+ (beginning-of-line)
+ (re-search-forward
+ (rx "#" (group (* (any "#")) (* " ")))
+ (line-end-position)
+ t nil))
+ (let ((offset (- (match-beginning 0) (line-beginning-position)))
+ (comment-prefix (match-string 0)))
+ (if soft (insert-and-inherit ?\n) (newline 1))
+ (delete-region (line-beginning-position) (point))
+ (insert
+ (make-string offset ?\s)
+ comment-prefix))
+ ;; other style of comments
+ (c-ts-common-comment-indent-new-line soft)))
+
+(defun php-ts-mode-comment-setup ()
+ "Set up local variables for PHP comment.
+Depends on `c-ts-common-comment-setup'."
+ (c-ts-common-comment-setup)
+ (setq-local c-ts-common--comment-regexp "comment"
+ comment-line-break-function #'php-ts-mode--comment-indent-new-line
+ comment-style 'extra-line
+ comment-start-skip (rx (or (seq "#" (not (any "[")))
+ (seq "/" (+ "/"))
+ (seq "/" (+ "*")))
+ (* (syntax whitespace)))))
+
+\f
+;;; Modes
+
+(defun php-ts-mode-set-comment-style ()
+ "Set a different comment style."
+ (interactive)
+ (setq-local comment-start
+ (completing-read
+ "Choose comment style: "
+ '("/**" "//" "/*" "#") nil t nil nil "// "))
+ (cond
+ ((equal comment-start "/*") (setq-local comment-end "*/"))
+ ((equal comment-start "//") (setq-local comment-end ""))
+ ((equal comment-start "#") (setq-local comment-end ""))
+ ((equal comment-start "/**") (setq-local comment-end "*/"))))
+
+(defvar-keymap php-ts-mode-map
+ :doc "Keymap for `php-ts-mode' buffers."
+ :parent prog-mode-map
+ "C-c C-q" #'php-ts-mode--indent-defun
+ "C-c ." #'php-ts-mode-set-style
+ "C-c C-k" #'php-ts-mode-set-comment-style
+ "C-c C-n" #'run-php
+ "C-c C-c" #'php-ts-mode-send-buffer
+ "C-c C-l" #'php-ts-mode-send-file
+ "C-c C-r" #'php-ts-mode-send-region)
+
+(easy-menu-define php-ts-mode-menu php-ts-mode-map
+ "Menu bar entry for `php-ts-mode'."
+ `("PHP"
+ ["Comment Out Region" comment-region
+ :enable mark-active
+ :help "Comment out the region between the mark and point"]
+ ["Uncomment Region" (comment-region (region-beginning)
+ (region-end) '(4))
+ :enable mark-active
+ :help "Uncomment the region between the mark and point"]
+ ["Indent Top-level Expression" php-ts-mode--indent-defun
+ :help "Indent/reindent top-level function, class, etc."]
+ ["Indent Line or Region" indent-for-tab-command
+ :help "Indent current line or region, or insert a tab"]
+ ["Forward Expression" forward-sexp
+ :help "Move forward across one balanced expression"]
+ ["Backward Expression" backward-sexp
+ :help "Move back across one balanced expression"]
+ ("Style..."
+ ["Set Indentation Style..." php-ts-mode-set-style
+ :help "Set PHP indentation style for current buffer"]
+ ["Show Current Style Name"(message "Indentation Style: %s"
+ php-ts-mode-indent-style)
+ :help "Show the name of the PHP indentation style for current buffer"]
+ ["Set Comment Style" php-ts-mode-set-comment-style
+ :help "Choose PHP comment style between block and line comments"])
+ "--"
+ ["Start interpreter" run-php
+ :help "Run inferior PHP process in a separate buffer"]
+ ["Show interpreter buffer" php-ts-mode-show-process-buffer]
+ ["Hide interpreter buffer" php-ts-mode-hide-process-buffer]
+ ["Kill interpreter process" php-ts-mode-kill-process]
+ ["Evaluate buffer" php-ts-mode-send-buffer]
+ ["Evaluate file" php-ts-mode-send-file]
+ ["Evaluate region" php-ts-mode-send-region]
+ "--"
+ ["Start built-in webserver" php-ts-mode-run-php-webserver
+ :help "Run the built-in PHP webserver"]
+ "--"
+ ["Customize" (lambda () (interactive) (customize-group "php-ts"))]))
+
+(defvar php-ts-mode--feature-list
+ '((;; common
+ comment definition spell
+ ;; CSS specific
+ query selector
+ ;; HTML specific
+ text
+ ;; PHPDOC specific
+ document
+ phpdoc-error)
+ (keyword string type name)
+ (;; common
+ attribute assignment constant escape-sequence function-scope
+ base-clause literal variable-name variable
+ ;; Javascript specific
+ jsx number pattern string-interpolation)
+ (;; common
+ argument bracket delimiter error function-call operator property
+ ;; Javascript specific
+ function)))
+
+;;;###autoload
+(define-derived-mode php-ts-mode prog-mode "PHP"
+ "Major mode for editing PHP, powered by tree-sitter."
+ :syntax-table php-ts-mode--syntax-table
+
+ (if (not (and
+ (treesit-ready-p 'php)
+ (treesit-ready-p 'phpdoc)
+ (treesit-ready-p 'html)
+ (treesit-ready-p 'javascript)
+ (treesit-ready-p 'css)))
+ (error "Tree-sitter for PHP isn't
+ available. You can install the parsers with M-x
+ `php-ts-mode-install-parsers'")
+
+ ;; Require html-ts-mode only when we load php-ts-mode
+ ;; so that we don't get a tree-sitter compilation warning for
+ ;; php-ts-mode.
+ (defvar html-ts-mode--indent-rules)
+ (require 'html-ts-mode)
+ ;; For embed html
+
+ ;; phpdoc is a local parser, don't create a parser fot it
+ (treesit-parser-create 'html)
+ (treesit-parser-create 'css)
+ (treesit-parser-create 'javascript)
+
+ ;; define the injected parser ranges
+ (setq-local treesit-range-settings
+ (treesit-range-rules
+ :embed 'phpdoc
+ :host 'php
+ :local t
+ '(((comment) @cap
+ (:match "/\\*\\*" @cap)))
+
+ :embed 'html
+ :host 'php
+ '((program (text) @cap)
+ (text_interpolation (text) @cap))
+
+ :embed 'javascript
+ :host 'html
+ :offset '(1 . -1)
+ '((script_element
+ (start_tag (tag_name))
+ (raw_text) @cap))
+
+ :embed 'css
+ :host 'html
+ :offset '(1 . -1)
+ '((style_element
+ (start_tag (tag_name))
+ (raw_text) @cap))))
+
+ (setq-local treesit-language-at-point-function #'php-ts-mode--language-at-point)
+
+ ;; Navigation.
+ (setq-local treesit-defun-type-regexp
+ (regexp-opt '("class_declaration"
+ "enum_declaration"
+ "function_definition"
+ "interface_declaration"
+ "method_declaration"
+ "namespace_definition"
+ "trait_declaration")))
+
+ (setq-local treesit-defun-name-function #'php-ts-mode--defun-name)
+
+ (setq-local treesit-thing-settings
+ `((php
+ (defun ,treesit-defun-type-regexp)
+ (sexp (not ,(rx (or "{" "}" "[" "]" "(" ")" ","))))
+ (sentence ,(regexp-opt
+ '("break_statement"
+ "case_statement"
+ "continue_statement"
+ "declaration"
+ "default_statement"
+ "do_statement"
+ "expression_statement"
+ "for_statement"
+ "if_statement"
+ "return_statement"
+ "switch_statement"
+ "while_statement"
+ "statement")))
+ (text ,(regexp-opt '("comment" "text"))))))
+
+ ;; Nodes like struct/enum/union_specifier can appear in
+ ;; function_definitions, so we need to find the top-level node.
+ (setq-local treesit-defun-prefer-top-level t)
+
+ ;; Indent.
+ (when (eq php-ts-mode-indent-style 'wordpress)
+ (setq-local indent-tabs-mode t))
+
+ (setq-local c-ts-common-indent-offset 'php-ts-mode-indent-offset)
+ (setq-local treesit-simple-indent-rules (php-ts-mode--get-indent-style))
+ (setq-local treesit-simple-indent-rules
+ (append treesit-simple-indent-rules
+ php-ts-mode--phpdoc-indent-rules
+ html-ts-mode--indent-rules
+ ;; Extended rules for js and css, to
+ ;; indent appropriately when injected
+ ;; into html
+ `((javascript ((parent-is "program")
+ php-ts-mode--js-css-tag-bol
+ php-ts-mode-js-css-indent-offset)
+ ,@(cdr (car js--treesit-indent-rules))))
+ `((css ((parent-is "stylesheet")
+ php-ts-mode--js-css-tag-bol
+ php-ts-mode-js-css-indent-offset)
+ ,@(cdr (car css--treesit-indent-rules))))))
+
+ ;; Comment
+ (php-ts-mode-comment-setup)
+
+ ;; PHP vars are case-sensitive
+ (setq-local case-fold-search t)
+
+ ;; Electric
+ (setq-local electric-indent-chars
+ (append "{}():;," electric-indent-chars))
+
+ ;; Imenu/Which-function/Outline
+ (setq-local treesit-simple-imenu-settings
+ '(("Class" "\\`class_declaration\\'" nil nil)
+ ("Enum" "\\`enum_declaration\\'" nil nil)
+ ("Function" "\\`function_definition\\'" nil nil)
+ ("Interface" "\\`interface_declaration\\'" nil nil)
+ ("Method" "\\`method_declaration\\'" nil nil)
+ ("Namespace" "\\`namespace_definition\\'" nil nil)
+ ("Trait" "\\`trait_declaration\\'" nil nil)
+ ("Variable" "\\`variable_name\\'" nil nil)
+ ("Constant" "\\`const_element\\'" nil nil)))
+
+ ;; Font-lock.
+ (setq-local treesit-font-lock-settings (php-ts-mode--font-lock-settings))
+ (setq-local treesit-font-lock-settings
+ (append treesit-font-lock-settings
+ php-ts-mode--custom-html-font-lock-settings
+ js--treesit-font-lock-settings
+ css--treesit-settings
+ php-ts-mode--phpdoc-font-lock-settings))
+
+ (setq-local treesit-font-lock-feature-list php-ts-mode--feature-list)
+
+ ;; Align.
+ (setq-local align-indent-before-aligning t)
+
+ ;; should be the last one
+ (setq-local treesit-primary-parser (treesit-parser-create 'php))
+ (treesit-font-lock-recompute-features)
+ (treesit-major-mode-setup)
+ (add-hook 'flymake-diagnostic-functions #'php-ts-mode-flymake-php nil 'local)))
+
+\f
+;;;###autoload
+(defun php-ts-mode-run-php-webserver (&optional port hostname document-root
+ router-script num-of-workers)
+ "Run PHP built-in web server.
+
+PORT: Port number of built-in web server, default `php-ts-mode-ws-port'.
+Prompt for the port if the default value is nil.
+HOSTNAME: Hostname or IP address of Built-in web server,
+default `php-ts-mode-ws-hostname'. Prompt for the hostname if the
+default value is nil.
+DOCUMENT-ROOT: Path to Document root, default `php-ts-mode-ws-document-root'.
+Prompt for the document-root if the default value is nil.
+ROUTER-SCRIPT: Path of the router PHP script,
+see `https://www.php.net/manual/en/features.commandline.webserver.php'
+NUM-OF-WORKERS: Before run the web server set the
+PHP_CLI_SERVER_WORKERS env variable useful for testing code against
+multiple simultaneous requests.
+
+Interactively, when invoked with prefix argument, always prompt
+for PORT, HOSTNAME, DOCUMENT-ROOT and ROUTER-SCRIPT."
+ (interactive (when current-prefix-arg
+ (php-ts-mode--webserver-read-args)))
+ (let* ((port (or
+ port
+ php-ts-mode-ws-port
+ (php-ts-mode--webserver-read-args 'port)))
+ (hostname (or
+ hostname
+ php-ts-mode-ws-hostname
+ (php-ts-mode--webserver-read-args 'hostname)))
+ (document-root (or
+ document-root
+ php-ts-mode-ws-document-root
+ (php-ts-mode--webserver-read-args 'document-root)))
+ (host (format "%s:%d" hostname port))
+ (name (format "PHP web server on: %s" host))
+ (buf-name (format "*%s*" name))
+ (args (delq
+ nil
+ (list "-S" host
+ "-t" document-root
+ router-script)))
+ (process-environment
+ (cons (cond
+ (num-of-workers (format "PHP_CLI_SERVER_WORKERS=%d" num-of-workers))
+ (php-ts-mode-ws-workers (format "PHP_CLI_SERVER_WORKERS=%d" php-ts-mode-ws-workers)))
+ process-environment)))
+ (if (get-buffer buf-name)
+ (message "Switch to already running web server into buffer %s" buf-name)
+ (message "Run PHP built-in web server with args %s into buffer %s"
+ (string-join args " ")
+ buf-name)
+ (apply #'make-comint name php-ts-mode-php-executable nil args))
+ (funcall
+ (if (called-interactively-p 'interactive) #'display-buffer #'get-buffer)
+ buf-name)))
+
+(derived-mode-add-parents 'php-ts-mode '(php-mode))
+
+(defun php-ts-mode--webserver-read-args (&optional type)
+ "Helper for php-ts-mode-run-php-webserver.
+The optional TYPE can be the symbol \"port\", \"hostname\", \"document-root\" or
+\"router-script\", otherwise it requires all of them."
+ (let ((ask-port (lambda ()
+ (read-number "Port: " 3000)))
+ (ask-hostname (lambda ()
+ (read-string "Hostname: " "localhost")))
+ (ask-document-root (lambda ()
+ (expand-file-name
+ (read-directory-name "Document root: "
+ (file-name-directory (buffer-file-name))))))
+ (ask-router-script (lambda ()
+ (expand-file-name
+ (read-file-name "Router script: "
+ (file-name-directory (buffer-file-name)))))))
+ (cl-case type
+ (port (funcall ask-port))
+ (hostname (funcall ask-hostname))
+ (document-root (funcall ask-document-root))
+ (router-script (funcall ask-router-script))
+ (t (list
+ (funcall ask-port)
+ (funcall ask-hostname)
+ (funcall ask-document-root)
+ (funcall ask-router-script))))))
+
+(define-derived-mode inferior-php-ts-mode comint-mode "Inferior PHP"
+ "Major mode for PHP inferior process."
+ (setq-local scroll-conservatively 1
+ comint-input-ring-file-name php-ts-mode-inferior-history
+ comint-input-ignoredups t
+ comint-prompt-read-only t
+ comint-use-prompt-regexp t
+ comint-prompt-regexp (concat "^" php-ts-mode--inferior-prompt " "))
+ (comint-read-input-ring t))
+
+\f
+;;; Inferior PHP process.
+
+(defvar php-ts-mode--inferior-php-process nil
+ "The PHP inferior process associated to `php-ts-mode-inferior-php-buffer'.")
+
+;;;###autoload
+(defun run-php (&optional cmd config)
+ "Run an PHP interpreter as a inferior process.
+
+Argumens CMD an CONFIG, default to `php-ts-mode-php-executable'
+and `php-ts-mode-php-config' respectively, control which PHP interpreter is run.
+Prompt for CMD if `php-ts-mode-php-executable' is nil.
+Optional CONFIG, if supplied, is the php.ini file to use."
+ (interactive (when current-prefix-arg
+ (list
+ (read-string "Run PHP: " php-ts-mode-php-executable)
+ (expand-file-name
+ (read-file-name "With config: " php-ts-mode-php-config)))))
+ (let ((buffer (get-buffer-create php-ts-mode-inferior-php-buffer))
+ (cmd (or
+ cmd
+ php-ts-mode-php-executable
+ (read-string "Run PHP: " php-ts-mode-php-executable)))
+ (config (or
+ config
+ (and php-ts-mode-php-config
+ (expand-file-name php-ts-mode-php-config)))))
+ (unless (comint-check-proc buffer)
+ (with-current-buffer buffer
+ (inferior-php-ts-mode-startup cmd config)
+ (inferior-php-ts-mode)))
+ (when buffer
+ (pop-to-buffer buffer))))
+
+(defun inferior-php-ts-mode-startup (cmd &optional config)
+ "Start an inferior PHP process with command CMD and init file CONFIG.
+CMD is the command to run. Optional CONFIG, if supplied, is the php.ini
+file to use."
+ (setq-local php-ts-mode--inferior-php-process
+ (apply #'make-comint-in-buffer
+ (string-replace "*" "" php-ts-mode-inferior-php-buffer)
+ php-ts-mode-inferior-php-buffer
+ cmd
+ nil
+ (delq
+ nil
+ (list
+ (when config
+ (format "-c %s" config))
+ "-a"))))
+ (add-hook 'comint-preoutput-filter-functions
+ (lambda (string)
+ (let ((prompt (concat php-ts-mode--inferior-prompt " ")))
+ (if (member
+ string
+ (list prompt "php { " "php ( " "/* > " "Interactive shell\n\n"))
+ string
+ (let (;; Filter out prompts characters that accumulate when sending
+ ;; regions to the inferior process.
+ (clean-string
+ (replace-regexp-in-string
+ (rx-to-string `(or
+ (+ "php >" (opt space))
+ (+ "php {" (opt space))
+ (+ "php (" (opt space))
+ (+ "/*" (1+ space) (1+ ">") (opt space))))
+ "" string)))
+ ;; Re-add the prompt for the next line, if isn't empty.
+ (if (string= clean-string "")
+ ""
+ (concat (string-chop-newline clean-string) "\n" prompt))))))
+ nil t)
+ (when php-ts-mode-inferior-history
+ (set-process-sentinel
+ (get-buffer-process php-ts-mode-inferior-php-buffer)
+ 'php-ts-mode-inferior--write-history)))
+
+;; taken and adapted from lua-ts-mode
+(defun php-ts-mode-inferior--write-history (process _)
+ "Write history file for inferior PHP PROCESS."
+ ;; Depending on how the process is killed the buffer may not be
+ ;; around anymore; e.g. `kill-buffer'.
+ (when-let* ((buffer (process-buffer process))
+ ((buffer-live-p (process-buffer process))))
+ (with-current-buffer buffer (comint-write-input-ring))))
+
+(defun php-ts-mode-send-region (beg end)
+ "Send region between BEG and END to the inferior PHP process."
+ (interactive "r")
+ (if (buffer-live-p php-ts-mode--inferior-php-process)
+ (progn
+ (php-ts-mode-show-process-buffer)
+ (comint-send-string php-ts-mode--inferior-php-process "\n")
+ (comint-send-string
+ php-ts-mode--inferior-php-process
+ (buffer-substring-no-properties beg end))
+ (comint-send-string php-ts-mode--inferior-php-process "\n"))
+ (message "Invoke run-php first!")))
+
+(defun php-ts-mode-send-buffer ()
+ "Send current buffer to the inferior PHP process."
+ (interactive)
+ (save-excursion
+ (goto-char (point-min))
+ (search-forward "<?php" nil t)
+ (php-ts-mode-send-region (point) (point-max))))
+
+(defun php-ts-mode-send-file (file)
+ "Send contents of FILE to the inferior PHP process."
+ (interactive "f")
+ (with-temp-buffer
+ (insert-file-contents-literally file)
+ (search-forward "<?php" nil t)
+ (php-ts-mode-send-region (point) (point-max))))
+
+(defun php-ts-mode-show-process-buffer ()
+ "Show the inferior PHP process buffer."
+ (interactive)
+ (display-buffer php-ts-mode-inferior-php-buffer))
+
+(defun php-ts-mode-hide-process-buffer ()
+ "Hide the inferior PHP process buffer."
+ (interactive)
+ (delete-windows-on php-ts-mode-inferior-php-buffer))
+
+(defun php-ts-mode-kill-process ()
+ "Kill the inferior PHP process."
+ (interactive)
+ (with-current-buffer php-ts-mode-inferior-php-buffer
+ (kill-buffer-and-window)))
+
+(when (treesit-ready-p 'php)
+ (add-to-list
+ 'auto-mode-alist '("\\.\\(?:php[s345]?\\|phtml\\)\\'" . php-ts-mode))
+ (add-to-list
+ 'auto-mode-alist '("\\.\\(?:php\\|inc\\|stub\\)\\'" . php-ts-mode))
+ (add-to-list
+ 'auto-mode-alist '("/\\.php_cs\\(?:\\.dist\\)?\\'" . php-ts-mode))
+ (add-to-list
+ 'interpreter-mode-alist
+ (cons "php\\(?:-?[34578]\\(?:\\.[0-9]+\\)*\\)?" 'php-ts-mode)))
+
+(provide 'php-ts-mode)
+;;; php-ts-mode.el ends here