unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
@ 2024-06-05 13:59 Vincenzo Pupillo
  2024-06-06  6:58 ` Eli Zaretskii
                   ` (2 more replies)
  0 siblings, 3 replies; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-05 13:59 UTC (permalink / raw)
  To: 71380

[-- Attachment #1: Type: text/plain, Size: 578 bytes --]

Hi, 
I would like to submit php-ts-mode. 
This major mode this major mode, in addition to font-lock for PHP implements the following features:
* font-lock for html, javascript, css and phpdoc.
* six different indentation styles (PSR, PEAR, Zend, Drupal, Wordpress, Symfony).
* Imenu
* Flymake
* Which-function
* a helper function to simplify the installation of parsers, in versions used to develop major-mode
* PHP built-in server support
* Shell interaction: execute PHP code in an inferior PHP process.


I completed the assignment process in March 2023.
Thank you.

Vincenzo

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 63410 bytes --]

From 430382668698b299040699785ba9ea0279f85a5f Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Wed, 5 Jun 2024 15:54:37 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1640 +++++++++++++++++++++++++++++++++
 2 files changed, 1645 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 922721f143c..72abedb043a 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1926,6 +1926,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..bb036fa6dad
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1640 @@
+;;; php-ts-mode.el --- Major mode PHP using tree-sitter -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: 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.
+;;
+;; 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 'html-ts-mode) ;; For embed html
+(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 parser.
+`php-ts-mode--language-source-alist' define 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 (default) in `php-ts-mode'."
+  :tag "PHP indent offset"
+  :version "30.1"
+  :type 'integer
+  :safe 'integerp)
+
+(defcustom php-ts-mode-js-css-indent-offset html-ts-mode-indent-offset
+  "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
+By default, the value is the same 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 'string
+  :safe 'stringp)
+
+(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 'string
+  :safe 'stringp)
+
+(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 'string
+  :safe 'stringp)
+
+(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)
+  (named-let loop ((res nil)
+		   (buffers (buffer-list)))
+    (if (null buffers)
+	(mapc (lambda (b)
+		(with-current-buffer b
+		  (php-ts-mode-set-style val)))
+	      res)
+      (let ((buffer (car buffers)))
+	(with-current-buffer buffer
+	  (if (derived-mode-p 'php-ts-mode)
+	      (loop (append res (list buffer)) (cdr buffers))
+	    (loop res (cdr buffers))))))))
+
+(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 'c-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)))
+    ;; Taken from the cc-langs version
+    (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)
+
+   :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)
+     (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 "*/")))
+  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
+  (force-mode-line-update))
+
+(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.
+
+\\{php-ts-mode-map}"
+  :syntax-table php-ts-mode--syntax-table
+
+  (unless (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'"))
+
+  ;; 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 (port hostname document-root
+					   &optional router-script num-of-workers)
+  "Run the PHP Built-in web-server on a specified PORT.
+
+PORT: Port number of built-in web server, default `php-ts-mode-ws-port'.
+If a default value is nil, the value is prompted.
+HOSTNAME: Hostname or IP address of Built-in web server,
+default `php-ts-mode-ws-hostname'.  If a default value is nil,
+the value is prompted.
+DOCUMENT-ROOT: Path to Document root, default `php-ts-mode-ws-document-root'.
+If a default value is nil, the value is prompted.
+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.
+
+When called with \\[universal-argument] it requires 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))))
+    (cond (num-of-workers (setenv "PHP_CLI_SERVER_WORKERS" num-of-workers))
+	  (php-ts-mode-ws-workers
+	   (setenv "PHP_CLI_SERVER_WORKERS" php-ts-mode-ws-workers)))
+    (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)))
+
+(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 a PHP interpreter as a inferior process.
+
+Argumens CMD an CONFIG, defaults to `php-ts-mode-php-executable'
+and `php-ts-mode-php-config' respectively, control which PHP interpreter is run.
+If CONFIG is nil the intepreter run with the default php.ini.
+if `php-ts-mode-php-executable' is not defined the user is prompted."
+  (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 config)
+  "Start an inferior PHP process.
+CMD is the command to run, CONFIG, if not nil, is a 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)))
+
+(derived-mode-add-parents 'php-ts-mode '(php-mode))
+
+(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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-05 13:59 bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php Vincenzo Pupillo
@ 2024-06-06  6:58 ` Eli Zaretskii
  2024-06-07 10:45   ` Vincenzo Pupillo
  2024-06-06 14:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-06-06 14:54 ` Andrea Corallo
  2 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-06  6:58 UTC (permalink / raw)
  To: Vincenzo Pupillo, Stefan Monnier, Philip Kaludercic, Yuan Fu; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Date: Wed, 05 Jun 2024 15:59:20 +0200
> 
> I would like to submit php-ts-mode. 
> This major mode this major mode, in addition to font-lock for PHP implements the following features:
> * font-lock for html, javascript, css and phpdoc.
> * six different indentation styles (PSR, PEAR, Zend, Drupal, Wordpress, Symfony).
> * Imenu
> * Flymake
> * Which-function
> * a helper function to simplify the installation of parsers, in versions used to develop major-mode
> * PHP built-in server support
> * Shell interaction: execute PHP code in an inferior PHP process.

Thanks, I added Stefan, Philip and Yuan to the discussion, in case they have
comments.

> +---
> +*** New major mode 'php-ts-mode'.
> +A major mode based on the tree-sitter library for editing

This seems to be an incomplete sentence.

Also, I think we should add PHP to the list of modes in the "Program
Modes" node of the Emacs user manual.

> +(defun php-ts-mode-install-parsers ()
> +  "Install all the required treesitter parser.
                                          ^^^^^^
"parsers", in plural.

> +`php-ts-mode--language-source-alist' define which parsers to install."
                                        ^^^^^^
"defines".

> +(defcustom php-ts-mode-indent-offset 4
> +  "Number of spaces for each indentation step (default) in `php-ts-mode'."
                ^^^^^^
"columns", I guess?

And what does "(default)" mean here?

> +(defcustom php-ts-mode-js-css-indent-offset html-ts-mode-indent-offset
> +  "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
> +By default, the value is the same as `html-ts-mode-indent-offset'"
                                                                    ^
Period missing there at the end of the sentence.

> +(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 'string
           ^^^^^^^
Should this be 'file instead?

> +(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 'string

Likewise here.

> +(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 'string

And this one perhaps should be 'directory?

> +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> +  "Return of the position of the first element of the array.

The "of" part should be deleted here, I think.

> +(defun php-ts-mode-run-php-webserver (port hostname document-root
> +					   &optional router-script num-of-workers)
> +  "Run the PHP Built-in web-server on a specified PORT.

This should mention the mandatory arguments.  How about

  Run PHP built-in web server on HOSTNAME:PORT to serve DOCUMENT-ROOT.

> +PORT: Port number of built-in web server, default `php-ts-mode-ws-port'.
> +If a default value is nil, the value is prompted.

Please avoid passive voice as much as possible.  In this case, I'd use

  Prompt for the port if the default value is nil.

Btw, it will prompt also if the value of the argument is nil, right?
So the above should say that as well.

> +HOSTNAME: Hostname or IP address of Built-in web server,
> +default `php-ts-mode-ws-hostname'.  If a default value is nil,
> +the value is prompted.
> +DOCUMENT-ROOT: Path to Document root, default `php-ts-mode-ws-document-root'.
> +If a default value is nil, the value is prompted.

Same comments for these two arguments.

> +When called with \\[universal-argument] it requires PORT,
> +HOSTNAME, DOCUMENT-ROOT and ROUTER-SCRIPT."

Our style of saying that is like this:

  Interactively, when invoked with prefix argument, always prompt
  for PORT, HOSTNAME, DOCUMENT-ROOT and ROUTER-SCRIPT.

> +    (cond (num-of-workers (setenv "PHP_CLI_SERVER_WORKERS" num-of-workers))
> +	  (php-ts-mode-ws-workers
> +	   (setenv "PHP_CLI_SERVER_WORKERS" php-ts-mode-ws-workers)))

setenv modifies process-environment for the entire duration of this
Emacs session.  If we only want to affect the following invocation of
make-comint, then perhaps let-binding process-environment around the
call to make-comint is a better way?

> +(defun run-php (&optional cmd config)
> +  "Run a PHP interpreter as a inferior process.
                               ^
"an"

> +Argumens CMD an CONFIG, defaults to `php-ts-mode-php-executable'
                           ^^^^^^^^
"default", in plural.  Also, I think it is better to say "which
default", given the rest of the sentence.

> +If CONFIG is nil the intepreter run with the default php.ini.
                                   ^^^
"runs", singular.

> +if `php-ts-mode-php-executable' is not defined the user is prompted."
   ^^                                             ^^^^^^^^^^^^^^^^^^^^
"If", capitalized.  And please use "prompt the user" to avoid the
passive voice.

> +(defun inferior-php-ts-mode-startup (cmd config)
> +  "Start an inferior PHP process.
> +CMD is the command to run, CONFIG, if not nil, is a php.ini file to use."

Again, please try to mention the mandatory arguments in the first
line of the doc string.  For example:

  Start an inferior PHP process with command CMD and init file CONFIG.





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-05 13:59 bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php Vincenzo Pupillo
  2024-06-06  6:58 ` Eli Zaretskii
@ 2024-06-06 14:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-06-07  9:04   ` Vincenzo Pupillo
  2024-06-06 14:54 ` Andrea Corallo
  2 siblings, 1 reply; 29+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-06-06 14:06 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

I have not read the whole code, but just noticed the things below:

> +  (named-let loop ((res nil)
> +		   (buffers (buffer-list)))
> +    (if (null buffers)
> +	(mapc (lambda (b)
> +		(with-current-buffer b
> +		  (php-ts-mode-set-style val)))
> +	      res)
> +      (let ((buffer (car buffers)))
> +	(with-current-buffer buffer
> +	  (if (derived-mode-p 'php-ts-mode)
> +	      (loop (append res (list buffer)) (cdr buffers))
> +	    (loop res (cdr buffers))))))))

These `loop` calls are not in tail-position, so this will eat up the
stack (because of the surrounding `with-current-buffer`).
Also, I don't understand why you do it in such a complicated way.
Isn't this better written:

    (dolist (buffer (buffer-list))
      (with-current-buffer buffer
        (when (derived-mode-p 'php-ts-mode)
          (php-ts-mode-set-style val))))

?

> +(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 'c-ts-indent-style-safep)

The :safe arg is also a function, so please #' it as well.

> +(defvar php-ts-mode--syntax-table
> +  (let ((table (make-syntax-table)))
> +    ;; Taken from the cc-langs version

Does this mean it comes from "the cc-mode-based `php-mode.el`" or from
`cc-langs.el` (and if so, which part, exactly)?

> +;; taken from c-ts-mode
[...]
> +;; taken from c-ts-mode

Are these literal copies?
Maybe we should consolidate the code with that of `c-ts-mode` to avoid
the code duplication?

> +  (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 "*/")))
> +  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
> +  (force-mode-line-update))

Is `comment-start` important enough to merit being part of the mode name?

> +(define-derived-mode php-ts-mode prog-mode "PHP"
> +  "Major mode for editing PHP, powered by tree-sitter.
> +
> +\\{php-ts-mode-map}"

\\{php-ts-mode-map} is not necessary because `define-derived-mode` adds
for you anyway.

> +(derived-mode-add-parents 'php-ts-mode '(php-mode))

I'd move it next to the `define-derived-mode`.


        Stefan






^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-05 13:59 bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php Vincenzo Pupillo
  2024-06-06  6:58 ` Eli Zaretskii
  2024-06-06 14:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-06-06 14:54 ` Andrea Corallo
  2024-06-07  8:36   ` Vincenzo Pupillo
  2 siblings, 1 reply; 29+ messages in thread
From: Andrea Corallo @ 2024-06-06 14:54 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

Vincenzo Pupillo <v.pupillo@gmail.com> writes:

> Hi, 
> I would like to submit php-ts-mode. 
> This major mode this major mode, in addition to font-lock for PHP implements the following features:
> * font-lock for html, javascript, css and phpdoc.
> * six different indentation styles (PSR, PEAR, Zend, Drupal, Wordpress, Symfony).
> * Imenu
> * Flymake
> * Which-function
> * a helper function to simplify the installation of parsers, in versions used to develop major-mode
> * PHP built-in server support
> * Shell interaction: execute PHP code in an inferior PHP process.
>
>
> I completed the assignment process in March 2023.
> Thank you.
>
> Vincenzo

Ciao Vincenzo,

applying the patch to the tree and building I see this warning is
introduced on my machine.

'Warning (treesit): Cannot activate tree-sitter, because language grammar for html is unavailable (not-found):'

Also, maybe you want to add ";; Maintainer:" as well with your name so
we know who to bother in case? :)

  Andrea





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-06 14:54 ` Andrea Corallo
@ 2024-06-07  8:36   ` Vincenzo Pupillo
  2024-06-07 13:39     ` Andrea Corallo
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07  8:36 UTC (permalink / raw)
  To: Andrea Corallo; +Cc: 71380

Ciao Andrea, 
I think the warning is due to html-ts-mode. It checks whether or not the parser exists, but even if it does not exist it tries to create it:
...

 (unless (treesit-ready-p 'html)
   (error “Tree-sitter for HTML isn't available”))

  (treesit-parser-create 'html)

....

I fixed it by replacing “unless” with “if” ...
Do I open another bug for this patch?

Ok for the ";; Maintainer". 
Go ahead and bother me! :D

V.

In data giovedì 6 giugno 2024 16:54:12 CEST, Andrea Corallo ha scritto:
> Vincenzo Pupillo <v.pupillo@gmail.com> writes:
> 
> > Hi, 
> > I would like to submit php-ts-mode. 
> > This major mode this major mode, in addition to font-lock for PHP implements the following features:
> > * font-lock for html, javascript, css and phpdoc.
> > * six different indentation styles (PSR, PEAR, Zend, Drupal, Wordpress, Symfony).
> > * Imenu
> > * Flymake
> > * Which-function
> > * a helper function to simplify the installation of parsers, in versions used to develop major-mode
> > * PHP built-in server support
> > * Shell interaction: execute PHP code in an inferior PHP process.
> >
> >
> > I completed the assignment process in March 2023.
> > Thank you.
> >
> > Vincenzo
> 
> Ciao Vincenzo,
> 
> applying the patch to the tree and building I see this warning is
> introduced on my machine.
> 
> 'Warning (treesit): Cannot activate tree-sitter, because language grammar for html is unavailable (not-found):'
> 
> Also, maybe you want to add ";; Maintainer:" as well with your name so
> we know who to bother in case? :)
> 
>   Andrea
> 








^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-06 14:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-06-07  9:04   ` Vincenzo Pupillo
  2024-06-07 12:53     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07  9:04 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 71380

Hi Stefan

In data giovedì 6 giugno 2024 16:06:21 CEST, Stefan Monnier ha scritto:
> I have not read the whole code, but just noticed the things below:
> 
> > +  (named-let loop ((res nil)
> > +		   (buffers (buffer-list)))
> > +    (if (null buffers)
> > +	(mapc (lambda (b)
> > +		(with-current-buffer b
> > +		  (php-ts-mode-set-style val)))
> > +	      res)
> > +      (let ((buffer (car buffers)))
> > +	(with-current-buffer buffer
> > +	  (if (derived-mode-p 'php-ts-mode)
> > +	      (loop (append res (list buffer)) (cdr buffers))
> > +	    (loop res (cdr buffers))))))))
> 
> These `loop` calls are not in tail-position, so this will eat up the
> stack (because of the surrounding `with-current-buffer`).
> Also, I don't understand why you do it in such a complicated way.
> Isn't this better written:
> 
>     (dolist (buffer (buffer-list))
>       (with-current-buffer buffer
>         (when (derived-mode-p 'php-ts-mode)
>           (php-ts-mode-set-style val))))
> 
> ?
Yes is better. The code above is a copy of c-ts-mode--indent-style-setter. 
It seemed too complicated to me too, but since it had been used there 
I thought there was some reason.

> 
> > +(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 'c-ts-indent-style-safep)
> 
> The :safe arg is also a function, so please #' it as well.
> 
Done

> > +(defvar php-ts-mode--syntax-table
> > +  (let ((table (make-syntax-table)))
> > +    ;; Taken from the cc-langs version
> 
> Does this mean it comes from "the cc-mode-based `php-mode.el`" or from
> `cc-langs.el` (and if so, which part, exactly)?
> 
> > +;; taken from c-ts-mode
> [...]
> > +;; taken from c-ts-mode
> 
> Are these literal copies?
> Maybe we should consolidate the code with that of `c-ts-mode` to avoid
> the code duplication?
> 
Yes, the first part is a literal copy of c-ts-mode--syntax-table. 
java-ts-mode does exactly the same thing, so I thought it best 
to avoid depending on c-ts-mode--syntax-table.


> > +  (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 "*/")))
> > +  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
> > +  (force-mode-line-update))
> 
> Is `comment-start` important enough to merit being part of the mode name?
> 

Sorry. I didn't understand.
Could you please clarify?

> > +(define-derived-mode php-ts-mode prog-mode "PHP"
> > +  "Major mode for editing PHP, powered by tree-sitter.
> > +
> > +\\{php-ts-mode-map}"
> 
> \\{php-ts-mode-map} is not necessary because `define-derived-mode` adds
> for you anyway.
> 
Ok, done.

> > +(derived-mode-add-parents 'php-ts-mode '(php-mode))
> 
> I'd move it next to the `define-derived-mode`.
> 
> 
Ok, done.
>         Stefan
> 
> 
Thank you Stefan.

Vincenzo







^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-06  6:58 ` Eli Zaretskii
@ 2024-06-07 10:45   ` Vincenzo Pupillo
  2024-06-07 11:12     ` Eli Zaretskii
  2024-06-09 13:53     ` Eli Zaretskii
  0 siblings, 2 replies; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 10:45 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 7353 bytes --]

Hi Eli, Thank you.

In data giovedì 6 giugno 2024 08:58:11 CEST, hai scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Date: Wed, 05 Jun 2024 15:59:20 +0200
> > 
> > I would like to submit php-ts-mode.
> > This major mode this major mode, in addition to font-lock for PHP
> > implements the following features: * font-lock for html, javascript, css
> > and phpdoc.
> > * six different indentation styles (PSR, PEAR, Zend, Drupal, Wordpress,
> > Symfony). * Imenu
> > * Flymake
> > * Which-function
> > * a helper function to simplify the installation of parsers, in versions
> > used to develop major-mode * PHP built-in server support
> > * Shell interaction: execute PHP code in an inferior PHP process.
> 
> Thanks, I added Stefan, Philip and Yuan to the discussion, in case they have
> comments.
> 
> > +---
> > +*** New major mode 'php-ts-mode'.
> > +A major mode based on the tree-sitter library for editing
> 
> This seems to be an incomplete sentence.
Done

> 
> Also, I think we should add PHP to the list of modes in the "Program
> Modes" node of the Emacs user manual.
> 
> > +(defun php-ts-mode-install-parsers ()
> > +  "Install all the required treesitter parser.
> 
>                                           ^^^^^^
> "parsers", in plural.
>
Done 
> > +`php-ts-mode--language-source-alist' define which parsers to install."
> 
>                                         ^^^^^^
> "defines".
> 
Done
> > +(defcustom php-ts-mode-indent-offset 4
> > +  "Number of spaces for each indentation step (default) in
> > `php-ts-mode'."
> 
>                 ^^^^^^
> "columns", I guess?
> 
> And what does "(default)" mean here?
> 
The comment is the same as c-ts-mode-indent-offset except for “(default)” , 
which I deleted

> > +(defcustom php-ts-mode-js-css-indent-offset html-ts-mode-indent-offset
> > +  "JavaScript and CSS indent spaces related to the <script> and <style>
> > HTML tags. +By default, the value is the same as
> > `html-ts-mode-indent-offset'"
>                                                                     ^
> Period missing there at the end of the sentence.
> 
Done 
> > +(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 'string
> 
>            ^^^^^^^
> Should this be 'file instead?
> 
Done

> > +(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 'string
> 
> Likewise here.
>
Done
 
> > +(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 'string
> 
> And this one perhaps should be 'directory?
> 
Done

> > +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> > +  "Return of the position of the first element of the array.
> 
> The "of" part should be deleted here, I think.
>
I'm not sure how to explain it. Different indentation styles indent the 
elements of an array differently when written on multiple rows. For example.
in PSR2 it is like this:
$a = array("a" => 1,
     "b" => 2,
     "c" => 3);
while with Zend it is like this:
$a = array("a" => 1,
           "b" => 2,
           "c" => 3);
What do you suggest?

> > +(defun php-ts-mode-run-php-webserver (port hostname document-root
> > +					   &optional router-script num-
of-workers)
> > +  "Run the PHP Built-in web-server on a specified PORT.
> 
> This should mention the mandatory arguments.  How about
> 
>   Run PHP built-in web server on HOSTNAME:PORT to serve DOCUMENT-ROOT.
> 
Sorry, an error while merge my branch. all parameters are optional.

> > +PORT: Port number of built-in web server, default `php-ts-mode-ws-port'.
> > +If a default value is nil, the value is prompted.
> 
> Please avoid passive voice as much as possible.  In this case, I'd use
> 
>   Prompt for the port if the default value is nil.
> 
> Btw, it will prompt also if the value of the argument is nil, right?
> So the above should say that as well.
Yes, done.

> 
> > +HOSTNAME: Hostname or IP address of Built-in web server,
> > +default `php-ts-mode-ws-hostname'.  If a default value is nil,
> > +the value is prompted.
> > +DOCUMENT-ROOT: Path to Document root, default
> > `php-ts-mode-ws-document-root'. +If a default value is nil, the value is
> > prompted.
> 
> Same comments for these two arguments.
>
Ok, done.
 
> > +When called with \\[universal-argument] it requires PORT,
> > +HOSTNAME, DOCUMENT-ROOT and ROUTER-SCRIPT."
> 
> Our style of saying that is like this:
> 
>   Interactively, when invoked with prefix argument, always prompt
>   for PORT, HOSTNAME, DOCUMENT-ROOT and ROUTER-SCRIPT.
> 
Done

> > +    (cond (num-of-workers (setenv "PHP_CLI_SERVER_WORKERS"
> > num-of-workers)) +	  (php-ts-mode-ws-workers
> > +	   (setenv "PHP_CLI_SERVER_WORKERS" php-ts-mode-ws-workers)))
> 
> setenv modifies process-environment for the entire duration of this
> Emacs session.  If we only want to affect the following invocation of
> make-comint, then perhaps let-binding process-environment around the
> call to make-comint is a better way?
> 
Yes is better. I rewrote as a let variable:
(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))

PHP accepts that environment variable only if > 1, 
otherwise it issues in warning.


> > +(defun run-php (&optional cmd config)
> > +  "Run a PHP interpreter as a inferior process.
> 
>                                ^
> "an"
> 
Done

> > +Argumens CMD an CONFIG, defaults to `php-ts-mode-php-executable'
> 
>                            ^^^^^^^^
> "default", in plural.  Also, I think it is better to say "which
> default", given the rest of the sentence.
> 
> > +If CONFIG is nil the intepreter run with the default php.ini.
> 
>                                    ^^^
> "runs", singular.
> 
Done

> > +if `php-ts-mode-php-executable' is not defined the user is prompted."
> 
>    ^^                                             ^^^^^^^^^^^^^^^^^^^^
> "If", capitalized.  And please use "prompt the user" to avoid the
> passive voice.
> 
> > +(defun inferior-php-ts-mode-startup (cmd config)
> > +  "Start an inferior PHP process.
> > +CMD is the command to run, CONFIG, if not nil, is a php.ini file to use."
> 
> Again, please try to mention the mandatory arguments in the first
> line of the doc string.  For example:
> 
>   Start an inferior PHP process with command CMD and init file CONFIG

Done


As I wrote to Andrea, the compilation warning seems to be caused 
by html-ts-mode. I have prepared another patch that fixes the problem.

Thank you.

Vincenzo

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69600 bytes --]

From d328f9d7cd2a20811c1ad1ebdee8083f38db8b51 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Fri, 7 Jun 2024 12:39:03 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1637 +++++++++++++++++++++++++++++++++
 2 files changed, 1642 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 808cd0562db..067963b7a26 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1933,6 +1933,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..ac441cf7c99
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1637 @@
+;;; php-ts-mode.el --- Major mode PHP 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.
+;;
+;; 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 'html-ts-mode) ;; For embed html
+(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 html-ts-mode-indent-offset
+  "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
+By default, the value is the same 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)))
+    ;; Taken from the cc-langs version
+    (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)
+
+   :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)
+     (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 "*/")))
+  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
+  (force-mode-line-update))
+
+(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'")
+
+    ;; 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
-- 
2.45.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-Do-not-run-treesit-parser-create-if-HTML-parser-is-u.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Do-not-run-treesit-parser-create-if-HTML-parser-is-u.patch", Size: 4166 bytes --]

From c1c40dea72c0ea68c259fb82749ddc6e7bf41890 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Fri, 7 Jun 2024 12:29:25 +0200
Subject: [PATCH] Do not run treesit-parser-create if HTML parser is
 unavailable.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* lisp/textmodes/html-ts-mode.el: replaced ‘unless’ with ‘if'.
---
 lisp/textmodes/html-ts-mode.el | 88 +++++++++++++++++-----------------
 1 file changed, 44 insertions(+), 44 deletions(-)

diff --git a/lisp/textmodes/html-ts-mode.el b/lisp/textmodes/html-ts-mode.el
index 235e1055fa9..4c8c6e77f8a 100644
--- a/lisp/textmodes/html-ts-mode.el
+++ b/lisp/textmodes/html-ts-mode.el
@@ -89,50 +89,50 @@ html-ts-mode
   "Major mode for editing Html, powered by tree-sitter."
   :group 'html
 
-  (unless (treesit-ready-p 'html)
-    (error "Tree-sitter for HTML isn't available"))
-
-  (treesit-parser-create 'html)
-
-  ;; Indent.
-  (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules)
-
-  ;; Navigation.
-  (setq-local treesit-defun-type-regexp "element")
-
-  (setq-local treesit-defun-name-function #'html-ts-mode--defun-name)
-
-  (setq-local treesit-thing-settings
-              `((html
-                 (sexp ,(regexp-opt '("element"
-                                      "text"
-                                      "attribute"
-                                      "value")))
-                 (sentence "tag")
-                 (text ,(regexp-opt '("comment" "text"))))))
-
-  ;; Font-lock.
-  (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings)
-  (setq-local treesit-font-lock-feature-list
-              '((comment keyword definition)
-                (property string)
-                () ()))
-
-  ;; Imenu.
-  (setq-local treesit-simple-imenu-settings
-              '(("Element" "\\`tag_name\\'" nil nil)))
-
-  ;; Outline minor mode.
-  (setq-local treesit-outline-predicate "\\`element\\'")
-  ;; `html-ts-mode' inherits from `html-mode' that sets
-  ;; regexp-based outline variables.  So need to restore
-  ;; the default values of outline variables to be able
-  ;; to use `treesit-outline-predicate' above.
-  (kill-local-variable 'outline-regexp)
-  (kill-local-variable 'outline-heading-end-regexp)
-  (kill-local-variable 'outline-level)
-
-  (treesit-major-mode-setup))
+  (if (treesit-ready-p 'html)
+      (error "Tree-sitter for HTML isn't available")
+
+    (treesit-parser-create 'html)
+
+    ;; Indent.
+    (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules)
+
+    ;; Navigation.
+    (setq-local treesit-defun-type-regexp "element")
+
+    (setq-local treesit-defun-name-function #'html-ts-mode--defun-name)
+
+    (setq-local treesit-thing-settings
+                `((html
+                   (sexp ,(regexp-opt '("element"
+                                        "text"
+                                        "attribute"
+                                        "value")))
+                   (sentence "tag")
+                   (text ,(regexp-opt '("comment" "text"))))))
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '((comment keyword definition)
+                  (property string)
+                  () ()))
+
+    ;; Imenu.
+    (setq-local treesit-simple-imenu-settings
+                '(("Element" "\\`tag_name\\'" nil nil)))
+
+    ;; Outline minor mode.
+    (setq-local treesit-outline-predicate "\\`element\\'")
+    ;; `html-ts-mode' inherits from `html-mode' that sets
+    ;; regexp-based outline variables.  So need to restore
+    ;; the default values of outline variables to be able
+    ;; to use `treesit-outline-predicate' above.
+    (kill-local-variable 'outline-regexp)
+    (kill-local-variable 'outline-heading-end-regexp)
+    (kill-local-variable 'outline-level)
+
+    (treesit-major-mode-setup)))
 
 (derived-mode-add-parents 'html-ts-mode '(html-mode))
 
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 10:45   ` Vincenzo Pupillo
@ 2024-06-07 11:12     ` Eli Zaretskii
  2024-06-07 12:50       ` Vincenzo Pupillo
  2024-06-09 13:53     ` Eli Zaretskii
  1 sibling, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-07 11:12 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Fri, 07 Jun 2024 12:45:05 +0200
> 
> > > +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> > > +  "Return of the position of the first element of the array.
> > 
> > The "of" part should be deleted here, I think.
> >
> I'm not sure how to explain it. Different indentation styles indent the 
> elements of an array differently when written on multiple rows. For example.
> in PSR2 it is like this:
> $a = array("a" => 1,
>      "b" => 2,
>      "c" => 3);
> while with Zend it is like this:
> $a = array("a" => 1,
>            "b" => 2,
>            "c" => 3);
> What do you suggest?

What does the function return in each of these two cases?





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 11:12     ` Eli Zaretskii
@ 2024-06-07 12:50       ` Vincenzo Pupillo
  2024-06-07 13:44         ` Eli Zaretskii
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 12:50 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

In data venerdì 7 giugno 2024 13:12:25 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Cc: 71380@debbugs.gnu.org
> > Date: Fri, 07 Jun 2024 12:45:05 +0200
> > 
> > > > +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> > > > +  "Return of the position of the first element of the array.
> > > 
> > > The "of" part should be deleted here, I think.
> > >
> > I'm not sure how to explain it. Different indentation styles indent the 
> > elements of an array differently when written on multiple rows. For example.
> > in PSR2 it is like this:
> > $a = array("a" => 1,
> >      "b" => 2,
> >      "c" => 3);
> > while with Zend it is like this:
> > $a = array("a" => 1,
> >            "b" => 2,
> >            "c" => 3);
> > What do you suggest?
> 
> What does the function return in each of these two cases?
> 
If '$a = array(' is on the same line as '“a” => 1,' it returns the initial position of '“a”, 
otherwise the starting position of 'array(', like PSR2. 
In terms of tree-sitter-php the first case is:
(treesit-node-start (treesit-node-child parent 2))
while the second is: parent indentation + offset.

Intricate, but can handle nested array declarations in styles like Zend. 
It is currently used only for Zend but I am starting to look at another style named PER (https://www.php-fig.org/per/coding-style/). 

Thank you.
Vincenzo







^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07  9:04   ` Vincenzo Pupillo
@ 2024-06-07 12:53     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-06-07 13:25       ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-06-07 12:53 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

>>     (dolist (buffer (buffer-list))
>>       (with-current-buffer buffer
>>         (when (derived-mode-p 'php-ts-mode)
>>           (php-ts-mode-set-style val))))
>> 
>> ?
> Yes is better. The code above is a copy of c-ts-mode--indent-style-setter. 
> It seemed too complicated to me too, but since it had been used there 
> I thought there was some reason.

Aha!  Thanks, I guess we should check the
`c-ts-mode--indent-style-setter` situation, as well, then.

>> > +(defvar php-ts-mode--syntax-table
>> > +  (let ((table (make-syntax-table)))
>> > +    ;; Taken from the cc-langs version
>> 
>> Does this mean it comes from "the cc-mode-based `php-mode.el`" or from
>> `cc-langs.el` (and if so, which part, exactly)?
>> 
>> > +;; taken from c-ts-mode
>> [...]
>> > +;; taken from c-ts-mode
>> 
>> Are these literal copies?
>> Maybe we should consolidate the code with that of `c-ts-mode` to avoid
>> the code duplication?
>> 
> Yes, the first part is a literal copy of c-ts-mode--syntax-table.
> java-ts-mode does exactly the same thing, so I thought it best
> to avoid depending on c-ts-mode--syntax-table.

Hmm... that makes the comment hard to understand.

>> > +  (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 "*/")))
>> > +  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
>> > +  (force-mode-line-update))
>> Is `comment-start` important enough to merit being part of the mode name?
> Sorry. I didn't understand.  Could you please clarify?

You have:

    (setq mode-name (concat "PHP" (string-trim-right comment-start)))

which means the mode-line will display `comment-start` (along with the
usual other things).  Is it really a good idea, given how the mode-line
is already often "too full"?  Which other major mode does that?
What's special about `comment-start` to make it deserve this honor?


        Stefan






^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 12:53     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-06-07 13:25       ` Vincenzo Pupillo
  2024-06-07 13:49         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 13:25 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 71380


In data venerdì 7 giugno 2024 14:53:04 CEST, Stefan Monnier ha scritto:
> >>     (dolist (buffer (buffer-list))
> >>       (with-current-buffer buffer
> >>         (when (derived-mode-p 'php-ts-mode)
> >>           (php-ts-mode-set-style val))))
> >> 
> >> ?
> > Yes is better. The code above is a copy of c-ts-mode--indent-style-setter. 
> > It seemed too complicated to me too, but since it had been used there 
> > I thought there was some reason.
> 
> Aha!  Thanks, I guess we should check the
> `c-ts-mode--indent-style-setter` situation, as well, then.
> 
> >> > +(defvar php-ts-mode--syntax-table
> >> > +  (let ((table (make-syntax-table)))
> >> > +    ;; Taken from the cc-langs version
> >> 
> >> Does this mean it comes from "the cc-mode-based `php-mode.el`" or from
> >> `cc-langs.el` (and if so, which part, exactly)?
> >> 
> >> > +;; taken from c-ts-mode
> >> [...]
> >> > +;; taken from c-ts-mode
> >> 
> >> Are these literal copies?
> >> Maybe we should consolidate the code with that of `c-ts-mode` to avoid
> >> the code duplication?
> >> 
> > Yes, the first part is a literal copy of c-ts-mode--syntax-table.
> > java-ts-mode does exactly the same thing, so I thought it best
> > to avoid depending on c-ts-mode--syntax-table.
> 
> Hmm... that makes the comment hard to understand.
> 
Yes. Perhaps it would be better to remove it.

> >> > +  (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 "*/")))
> >> > +  (setq mode-name (concat "PHP" (string-trim-right comment-start)))
> >> > +  (force-mode-line-update))
> >> Is `comment-start` important enough to merit being part of the mode name?
> > Sorry. I didn't understand.  Could you please clarify?
> 
> You have:
> 
>     (setq mode-name (concat "PHP" (string-trim-right comment-start)))
> 
> which means the mode-line will display `comment-start` (along with the
> usual other things).  Is it really a good idea, given how the mode-line
> is already often "too full"?  Which other major mode does that?
> What's special about `comment-start` to make it deserve this honor?
> 
> 
c-ts-mode-set-modeline does the same, which is why I did it too. 


>         Stefan
> 
> 

Thanks

Vincenzo








^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07  8:36   ` Vincenzo Pupillo
@ 2024-06-07 13:39     ` Andrea Corallo
  2024-06-07 17:02       ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Andrea Corallo @ 2024-06-07 13:39 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

Vincenzo Pupillo <v.pupillo@gmail.com> writes:

> Ciao Andrea, 
> I think the warning is due to html-ts-mode. It checks whether or not the parser exists, but even if it does not exist it tries to create it:
> ...
>
>  (unless (treesit-ready-p 'html)
>    (error “Tree-sitter for HTML isn't available”))
>
>   (treesit-parser-create 'html)

What is not clear to me is why we should run this while compiling php-ts-mode.

> ....
>
> I fixed it by replacing “unless” with “if” ...
> Do I open another bug for this patch?

But in that case one could activate html-ts-mode even with no parser? 🤔

> Ok for the ";; Maintainer". 
> Go ahead and bother me! :D

:D

Thanks

  Andrea





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 12:50       ` Vincenzo Pupillo
@ 2024-06-07 13:44         ` Eli Zaretskii
  2024-06-07 15:05           ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-07 13:44 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Fri, 07 Jun 2024 14:50:24 +0200
> 
> In data venerdì 7 giugno 2024 13:12:25 CEST, Eli Zaretskii ha scritto:
> 
> > > > > +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> > > > > +  "Return of the position of the first element of the array.
> > > > 
> > > > The "of" part should be deleted here, I think.
> > > >
> > > I'm not sure how to explain it. Different indentation styles indent the 
> > > elements of an array differently when written on multiple rows. For example.
> > > in PSR2 it is like this:
> > > $a = array("a" => 1,
> > >      "b" => 2,
> > >      "c" => 3);
> > > while with Zend it is like this:
> > > $a = array("a" => 1,
> > >            "b" => 2,
> > >            "c" => 3);
> > > What do you suggest?
> > 
> > What does the function return in each of these two cases?
> > 
> If '$a = array(' is on the same line as '“a” => 1,' it returns the initial position of '“a”, 
> otherwise the starting position of 'array(', like PSR2. 
> In terms of tree-sitter-php the first case is:
> (treesit-node-start (treesit-node-child parent 2))
> while the second is: parent indentation + offset.

Does it return a buffer position or a column?  You seem to say that
sometimes it returns the former and sometimes the latter.





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 13:25       ` Vincenzo Pupillo
@ 2024-06-07 13:49         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2024-06-07 14:37           ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2024-06-07 13:49 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> c-ts-mode-set-modeline does the same, which is why I did it too. 

I see.  FWIW, I consider such things as errors: major mode should
provide  the functionality that depends on the language, but there's
nothing special about C comments that makes `comment-start` more
deserving of being in the mode-line in a C mode than in any other mode.

IOW, it seems like this is a case where the major mode author imposed
his own preference.  If you want `comment-start` in the mode-line in C,
then most likely you also want it in many/most other modes and so you
should implement a minor mode for that which would be usable in
any mode.


        Stefan






^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 13:49         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2024-06-07 14:37           ` Vincenzo Pupillo
  0 siblings, 0 replies; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 14:37 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 71380

> > c-ts-mode-set-modeline does the same, which is why I did it too. 
> 
> I see.  FWIW, I consider such things as errors: major mode should
> provide  the functionality that depends on the language, but there's
> nothing special about C comments that makes `comment-start` more
> deserving of being in the mode-line in a C mode than in any other mode.
> 
> IOW, it seems like this is a case where the major mode author imposed
> his own preference.  If you want `comment-start` in the mode-line in C,
> then most likely you also want it in many/most other modes and so you
> should implement a minor mode for that which would be usable in
> any mode.

Agreed. Deleted.

> 
> 
>         Stefan
> 
> 

Thank you Stefan.

Vincenzo









^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 13:44         ` Eli Zaretskii
@ 2024-06-07 15:05           ` Vincenzo Pupillo
  2024-06-08  9:31             ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 15:05 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

In data venerdì 7 giugno 2024 15:44:44 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Cc: 71380@debbugs.gnu.org
> > Date: Fri, 07 Jun 2024 14:50:24 +0200
> > 
> > In data venerdì 7 giugno 2024 13:12:25 CEST, Eli Zaretskii ha scritto:
> > 
> > > > > > +(defun php-ts-mode--array-element-heuristic (node parent bol &rest _)
> > > > > > +  "Return of the position of the first element of the array.
> > > > > 
> > > > > The "of" part should be deleted here, I think.
> > > > >
> > > > I'm not sure how to explain it. Different indentation styles indent the 
> > > > elements of an array differently when written on multiple rows. For example.
> > > > in PSR2 it is like this:
> > > > $a = array("a" => 1,
> > > >      "b" => 2,
> > > >      "c" => 3);
> > > > while with Zend it is like this:
> > > > $a = array("a" => 1,
> > > >            "b" => 2,
> > > >            "c" => 3);
> > > > What do you suggest?
> > > 
> > > What does the function return in each of these two cases?
> > > 
> > If '$a = array(' is on the same line as '“a” => 1,' it returns the initial position of '“a”, 
> > otherwise the starting position of 'array(', like PSR2. 
> > In terms of tree-sitter-php the first case is:
> > (treesit-node-start (treesit-node-child parent 2))
> > while the second is: parent indentation + offset.
> 
> Does it return a buffer position or a column?  You seem to say that
> sometimes it returns the former and sometimes the latter.
> 
The point in both cases. 

Vincenzo








^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 13:39     ` Andrea Corallo
@ 2024-06-07 17:02       ` Vincenzo Pupillo
  0 siblings, 0 replies; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-07 17:02 UTC (permalink / raw)
  To: Andrea Corallo; +Cc: 71380

In data venerdì 7 giugno 2024 15:39:04 CEST, Andrea Corallo ha scritto:
> Vincenzo Pupillo <v.pupillo@gmail.com> writes:
> > Ciao Andrea,
> > I think the warning is due to html-ts-mode. It checks whether or not the
> > parser exists, but even if it does not exist it tries to create it: ...
> > 
> >  (unless (treesit-ready-p 'html)
> >  
> >    (error “Tree-sitter for HTML isn't available”))
> >   
> >   (treesit-parser-create 'html)
> 
> What is not clear to me is why we should run this while compiling
> php-ts-mode.
I don't know. Maybe because there is a '(require 'html-ts-mode)' ?

> > ....
> > 
> > I fixed it by replacing “unless” with “if” ...
> > Do I open another bug for this patch?
> 
> But in that case one could activate html-ts-mode even with no parser? 🤔
> 
I don't think so. Because if the parser is not there, the rest of the code is 
not executed. I don't know the details, but all '*-ts modes' do this.

> > Ok for the ";; Maintainer".
> > Go ahead and bother me! :D
> :
> :D
> 
> Thanks
> 
>   Andrea

Ciao
Vincenzo







^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 15:05           ` Vincenzo Pupillo
@ 2024-06-08  9:31             ` Vincenzo Pupillo
  2024-06-08 10:45               ` Eli Zaretskii
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-08  9:31 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 1972 bytes --]

Hi Eli, 
this is the patch update. I followed Stefan's advice and removed the 
modification of the 'mode-line' and the comment in the 'php-ts-mode--syntax-
table'. 
Thanks!

Vincenzo

In data venerdì 7 giugno 2024 17:05:58 CEST, Vincenzo Pupillo ha scritto:
> In data venerdì 7 giugno 2024 15:44:44 CEST, Eli Zaretskii ha scritto:
> > > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > > Cc: 71380@debbugs.gnu.org
> > > Date: Fri, 07 Jun 2024 14:50:24 +0200
> > > 
> > > In data venerdì 7 giugno 2024 13:12:25 CEST, Eli Zaretskii ha scritto:
> > > > > > > +(defun php-ts-mode--array-element-heuristic (node parent bol
> > > > > > > &rest _)
> > > > > > > +  "Return of the position of the first element of the array.
> > > > > > 
> > > > > > The "of" part should be deleted here, I think.
> > > > > 
> > > > > I'm not sure how to explain it. Different indentation styles indent
> > > > > the
> > > > > elements of an array differently when written on multiple rows. For
> > > > > example. in PSR2 it is like this:
> > > > > $a = array("a" => 1,
> > > > > 
> > > > >      "b" => 2,
> > > > >      "c" => 3);
> > > > > 
> > > > > while with Zend it is like this:
> > > > > $a = array("a" => 1,
> > > > > 
> > > > >            "b" => 2,
> > > > >            "c" => 3);
> > > > > 
> > > > > What do you suggest?
> > > > 
> > > > What does the function return in each of these two cases?
> > > 
> > > If '$a = array(' is on the same line as '“a” => 1,' it returns the
> > > initial position of '“a”, otherwise the starting position of 'array(',
> > > like PSR2.
> > > In terms of tree-sitter-php the first case is:
> > > (treesit-node-start (treesit-node-child parent 2))
> > > while the second is: parent indentation + offset.
> > 
> > Does it return a buffer position or a column?  You seem to say that
> > sometimes it returns the former and sometimes the latter.
> 
> The point in both cases.
> 
> Vincenzo


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69460 bytes --]

From 8830b49bb5a654ceff9d07cd30bd67c3e498b37e Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Fri, 7 Jun 2024 12:39:03 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1634 +++++++++++++++++++++++++++++++++
 2 files changed, 1639 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 808cd0562db..067963b7a26 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1933,6 +1933,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..ce36208ac66
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1634 @@
+;;; php-ts-mode.el --- Major mode PHP 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.
+;;
+;; 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 'html-ts-mode) ;; For embed html
+(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 html-ts-mode-indent-offset
+  "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
+By default, the value is the same 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)
+
+   :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)
+     (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'")
+
+    ;; 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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-08  9:31             ` Vincenzo Pupillo
@ 2024-06-08 10:45               ` Eli Zaretskii
  2024-06-08 11:15                 ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-08 10:45 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Sat, 08 Jun 2024 11:31:05 +0200
> 
> this is the patch update. I followed Stefan's advice and removed the 
> modification of the 'mode-line' and the comment in the 'php-ts-mode--syntax-
> table'. 

Thanks, but does this replace both patches you sent in a previous
message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
Or does this replace only one of the two, and the other one is still
needed?





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-08 10:45               ` Eli Zaretskii
@ 2024-06-08 11:15                 ` Vincenzo Pupillo
  2024-06-09 13:54                   ` Eli Zaretskii
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-08 11:15 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

In data sabato 8 giugno 2024 12:45:08 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Cc: 71380@debbugs.gnu.org
> > Date: Sat, 08 Jun 2024 11:31:05 +0200
> > 
> > this is the patch update. I followed Stefan's advice and removed the
> > modification of the 'mode-line' and the comment in the
> > 'php-ts-mode--syntax- table'.
> 
> Thanks, but does this replace both patches you sent in a previous
> message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> Or does this replace only one of the two, and the other one is still
> needed?
Replace only one. The other is still needed. I made a separate patch, waiting 
to hear if I should open another bug for html-ts-mode. 

Thanks

Vincenzo







^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-07 10:45   ` Vincenzo Pupillo
  2024-06-07 11:12     ` Eli Zaretskii
@ 2024-06-09 13:53     ` Eli Zaretskii
  1 sibling, 0 replies; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-09 13:53 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Fri, 07 Jun 2024 12:45:05 +0200
> 
> +  (if (treesit-ready-p 'html)
> +      (error "Tree-sitter for HTML isn't available")

Is this really correct? we signal an error if the HTML parser is
available?





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-08 11:15                 ` Vincenzo Pupillo
@ 2024-06-09 13:54                   ` Eli Zaretskii
  2024-06-09 17:23                     ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-09 13:54 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Sat, 08 Jun 2024 13:15:58 +0200
> 
> In data sabato 8 giugno 2024 12:45:08 CEST, Eli Zaretskii ha scritto:
> > > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > > Cc: 71380@debbugs.gnu.org
> > > Date: Sat, 08 Jun 2024 11:31:05 +0200
> > > 
> > > this is the patch update. I followed Stefan's advice and removed the
> > > modification of the 'mode-line' and the comment in the
> > > 'php-ts-mode--syntax- table'.
> > 
> > Thanks, but does this replace both patches you sent in a previous
> > message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > Or does this replace only one of the two, and the other one is still
> > needed?
> Replace only one. The other is still needed. I made a separate patch, waiting 
> to hear if I should open another bug for html-ts-mode. 

Would you mind posting both patches together, updated so that I could
install them both?  And note that I just found a strange mistake(?) in
the second patch.

Thanks.





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-09 13:54                   ` Eli Zaretskii
@ 2024-06-09 17:23                     ` Vincenzo Pupillo
  2024-06-09 17:49                       ` Eli Zaretskii
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-09 17:23 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 1684 bytes --]

In data domenica 9 giugno 2024 15:54:38 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Cc: 71380@debbugs.gnu.org
> > Date: Sat, 08 Jun 2024 13:15:58 +0200
> > 
> > In data sabato 8 giugno 2024 12:45:08 CEST, Eli Zaretskii ha scritto:
> > > > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > > > Cc: 71380@debbugs.gnu.org
> > > > Date: Sat, 08 Jun 2024 11:31:05 +0200
> > > > 
> > > > this is the patch update. I followed Stefan's advice and removed the
> > > > modification of the 'mode-line' and the comment in the
> > > > 'php-ts-mode--syntax- table'.
> > > 
> > > Thanks, but does this replace both patches you sent in a previous
> > > message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > > Or does this replace only one of the two, and the other one is still
> > > needed?
> > 
> > Replace only one. The other is still needed. I made a separate patch,
> > waiting to hear if I should open another bug for html-ts-mode.
> 
> Would you mind posting both patches together, updated so that I could
> install them both?  And note that I just found a strange mistake(?) in
> the second patch.
> 
> Thanks.
Hi Eli, 
sorry. I made a mistake with the second patch.
The warning Andrea reported was actually due to the (require 'html-ts-mode) at 
the beginning of php-ts-mode.
I then use a solution similar to elixir-ts-mode, the (require 'html-ts-mode) 
is done only after checking that the parser exists.
There is one problem I don't know how to solve: enabling php-ts-mode, because 
of the require html-ts-mode, the major mode for html changes from mhtml-mode 
to html-ts-mode. 
Can you tell me how to fix this?

Thanks.
Vincenzo

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69756 bytes --]

From 230f3f6da0e3d46ba6b83f71c545d7f1c2cbac16 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Sun, 9 Jun 2024 19:04:13 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1642 +++++++++++++++++++++++++++++++++
 2 files changed, 1647 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 60df9760aa4..a30b0744a9c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1967,6 +1967,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..da3f039eb32
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1642 @@
+;;; php-ts-mode.el --- Major mode PHP 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.
+;;
+;; 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)
+     (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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-09 17:23                     ` Vincenzo Pupillo
@ 2024-06-09 17:49                       ` Eli Zaretskii
  2024-06-09 19:37                         ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-09 17:49 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380

> From: Vincenzo Pupillo <v.pupillo@gmail.com>
> Cc: 71380@debbugs.gnu.org
> Date: Sun, 09 Jun 2024 19:23:05 +0200
> 
> > > > Thanks, but does this replace both patches you sent in a previous
> > > > message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > > > Or does this replace only one of the two, and the other one is still
> > > > needed?
> > > 
> > > Replace only one. The other is still needed. I made a separate patch,
> > > waiting to hear if I should open another bug for html-ts-mode.
> > 
> > Would you mind posting both patches together, updated so that I could
> > install them both?  And note that I just found a strange mistake(?) in
> > the second patch.
> > 
> > Thanks.
> Hi Eli, 
> sorry. I made a mistake with the second patch.
> The warning Andrea reported was actually due to the (require 'html-ts-mode) at 
> the beginning of php-ts-mode.
> I then use a solution similar to elixir-ts-mode, the (require 'html-ts-mode) 
> is done only after checking that the parser exists.

But the patch you posted now doesn't include the HTML part, does it?

is the patch self-contained, or does it still need the second patch?

> There is one problem I don't know how to solve: enabling php-ts-mode, because 
> of the require html-ts-mode, the major mode for html changes from mhtml-mode 
> to html-ts-mode. 
> Can you tell me how to fix this?

Just document this side effect, so that users know.





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-09 17:49                       ` Eli Zaretskii
@ 2024-06-09 19:37                         ` Vincenzo Pupillo
  2024-06-09 20:36                           ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-09 19:37 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 1688 bytes --]

In data domenica 9 giugno 2024 19:49:24 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > Cc: 71380@debbugs.gnu.org
> > Date: Sun, 09 Jun 2024 19:23:05 +0200
> > 
> > > > > Thanks, but does this replace both patches you sent in a previous
> > > > > message?  See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > > > > Or does this replace only one of the two, and the other one is still
> > > > > needed?
> > > > 
> > > > Replace only one. The other is still needed. I made a separate patch,
> > > > waiting to hear if I should open another bug for html-ts-mode.
> > > 
> > > Would you mind posting both patches together, updated so that I could
> > > install them both?  And note that I just found a strange mistake(?) in
> > > the second patch.
> > > 
> > > Thanks.
> > 
> > Hi Eli,
> > sorry. I made a mistake with the second patch.
> > The warning Andrea reported was actually due to the (require
> > 'html-ts-mode) at the beginning of php-ts-mode.
> > I then use a solution similar to elixir-ts-mode, the (require
> > 'html-ts-mode) is done only after checking that the parser exists.
> 
> But the patch you posted now doesn't include the HTML part, does it?
> 
> is the patch self-contained, or does it still need the second patch?

It is self-contained. You can delete the patch for html-ts-mode, it is no 
longer needed

> 
> > There is one problem I don't know how to solve: enabling php-ts-mode,
> > because of the require html-ts-mode, the major mode for html changes from
> > mhtml-mode to html-ts-mode.
> > Can you tell me how to fix this?
> 
> Just document this side effect, so that users know.

Ok done. 

Thank you.
Vincenzo


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69756 bytes --]

From 230f3f6da0e3d46ba6b83f71c545d7f1c2cbac16 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Sun, 9 Jun 2024 19:04:13 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1642 +++++++++++++++++++++++++++++++++
 2 files changed, 1647 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 60df9760aa4..a30b0744a9c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1967,6 +1967,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..da3f039eb32
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1642 @@
+;;; php-ts-mode.el --- Major mode PHP 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.
+;;
+;; 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)
+     (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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-09 19:37                         ` Vincenzo Pupillo
@ 2024-06-09 20:36                           ` Vincenzo Pupillo
  2024-06-12  9:25                             ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-09 20:36 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 2028 bytes --]

Sorry. The correct patch is this one. I had forgotten to extend the commit by 
adding documentation about the side effect.
Thanks. 
Vincenzo

In data domenica 9 giugno 2024 21:37:40 CEST, Vincenzo Pupillo ha scritto:
> In data domenica 9 giugno 2024 19:49:24 CEST, Eli Zaretskii ha scritto:
> > > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > > Cc: 71380@debbugs.gnu.org
> > > Date: Sun, 09 Jun 2024 19:23:05 +0200
> > > 
> > > > > > Thanks, but does this replace both patches you sent in a previous
> > > > > > message?  See
> > > > > > https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > > > > > Or does this replace only one of the two, and the other one is
> > > > > > still
> > > > > > needed?
> > > > > 
> > > > > Replace only one. The other is still needed. I made a separate
> > > > > patch,
> > > > > waiting to hear if I should open another bug for html-ts-mode.
> > > > 
> > > > Would you mind posting both patches together, updated so that I could
> > > > install them both?  And note that I just found a strange mistake(?) in
> > > > the second patch.
> > > > 
> > > > Thanks.
> > > 
> > > Hi Eli,
> > > sorry. I made a mistake with the second patch.
> > > The warning Andrea reported was actually due to the (require
> > > 'html-ts-mode) at the beginning of php-ts-mode.
> > > I then use a solution similar to elixir-ts-mode, the (require
> > > 'html-ts-mode) is done only after checking that the parser exists.
> > 
> > But the patch you posted now doesn't include the HTML part, does it?
> > 
> > is the patch self-contained, or does it still need the second patch?
> 
> It is self-contained. You can delete the patch for html-ts-mode, it is no
> longer needed
> 
> > > There is one problem I don't know how to solve: enabling php-ts-mode,
> > > because of the require html-ts-mode, the major mode for html changes
> > > from
> > > mhtml-mode to html-ts-mode.
> > > Can you tell me how to fix this?
> > 
> > Just document this side effect, so that users know.
> 
> Ok done.
> 
> Thank you.
> Vincenzo


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69885 bytes --]

From 23bbaacd3f904349aa4ae81d45f1bfdb1ddbff41 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Sun, 9 Jun 2024 19:04:13 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1645 +++++++++++++++++++++++++++++++++
 2 files changed, 1650 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 60df9760aa4..a30b0744a9c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1967,6 +1967,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..8d28f910416
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1645 @@
+;;; php-ts-mode.el --- Major mode PHP 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)
+     (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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-09 20:36                           ` Vincenzo Pupillo
@ 2024-06-12  9:25                             ` Vincenzo Pupillo
  2024-06-12 18:27                               ` Eli Zaretskii
  0 siblings, 1 reply; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-12  9:25 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: 71380

[-- Attachment #1: Type: text/plain, Size: 2302 bytes --]

Hi Eli, this updated patch add the support for PHP var_modifier. 

Thanks.
Vincenzo


In data domenica 9 giugno 2024 22:36:35 CEST, Vincenzo Pupillo ha scritto:
> Sorry. The correct patch is this one. I had forgotten to extend the commit by 
> adding documentation about the side effect.
> Thanks. 
> Vincenzo
> 
> In data domenica 9 giugno 2024 21:37:40 CEST, Vincenzo Pupillo ha scritto:
> > In data domenica 9 giugno 2024 19:49:24 CEST, Eli Zaretskii ha scritto:
> > > > From: Vincenzo Pupillo <v.pupillo@gmail.com>
> > > > Cc: 71380@debbugs.gnu.org
> > > > Date: Sun, 09 Jun 2024 19:23:05 +0200
> > > > 
> > > > > > > Thanks, but does this replace both patches you sent in a previous
> > > > > > > message?  See
> > > > > > > https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71380#23
> > > > > > > Or does this replace only one of the two, and the other one is
> > > > > > > still
> > > > > > > needed?
> > > > > > 
> > > > > > Replace only one. The other is still needed. I made a separate
> > > > > > patch,
> > > > > > waiting to hear if I should open another bug for html-ts-mode.
> > > > > 
> > > > > Would you mind posting both patches together, updated so that I could
> > > > > install them both?  And note that I just found a strange mistake(?) in
> > > > > the second patch.
> > > > > 
> > > > > Thanks.
> > > > 
> > > > Hi Eli,
> > > > sorry. I made a mistake with the second patch.
> > > > The warning Andrea reported was actually due to the (require
> > > > 'html-ts-mode) at the beginning of php-ts-mode.
> > > > I then use a solution similar to elixir-ts-mode, the (require
> > > > 'html-ts-mode) is done only after checking that the parser exists.
> > > 
> > > But the patch you posted now doesn't include the HTML part, does it?
> > > 
> > > is the patch self-contained, or does it still need the second patch?
> > 
> > It is self-contained. You can delete the patch for html-ts-mode, it is no
> > longer needed
> > 
> > > > There is one problem I don't know how to solve: enabling php-ts-mode,
> > > > because of the require html-ts-mode, the major mode for html changes
> > > > from
> > > > mhtml-mode to html-ts-mode.
> > > > Can you tell me how to fix this?
> > > 
> > > Just document this side effect, so that users know.
> > 
> > Ok done.
> > 
> > Thank you.
> > Vincenzo
> 
> 

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-php-ts-mode.patch --]
[-- Type: text/x-patch; charset="x-UTF_8J"; name="0001-Add-php-ts-mode.patch", Size: 69980 bytes --]

From d15a81a383660ede80b7fef46e573b0147e4261d Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <v.pupillo@gmail.com>
Date: Fri, 7 Jun 2024 12:39:03 +0200
Subject: [PATCH] Add php-ts-mode

* etc/NEWS: Mention the new mode.
* lisp/progmodes/php-ts-mode.el: New file.
---
 etc/NEWS                      |    5 +
 lisp/progmodes/php-ts-mode.el | 1647 +++++++++++++++++++++++++++++++++
 2 files changed, 1652 insertions(+)
 create mode 100644 lisp/progmodes/php-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 808cd0562db..067963b7a26 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1933,6 +1933,11 @@ A major mode based on the tree-sitter library for editing Elixir files.
 *** New major mode 'lua-ts-mode'.
 A major mode based on the tree-sitter library for editing Lua files.
 
+---
+*** New major mode 'php-ts-mode'.
+A major mode based on the tree-sitter library for editing PHP files.
+
+
 ** Minibuffer and Completions
 
 +++
diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
new file mode 100644
index 00000000000..3473057edd4
--- /dev/null
+++ b/lisp/progmodes/php-ts-mode.el
@@ -0,0 +1,1647 @@
+;;; 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
-- 
2.45.2


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-12  9:25                             ` Vincenzo Pupillo
@ 2024-06-12 18:27                               ` Eli Zaretskii
  2024-06-12 20:44                                 ` Vincenzo Pupillo
  0 siblings, 1 reply; 29+ messages in thread
From: Eli Zaretskii @ 2024-06-12 18:27 UTC (permalink / raw)
  To: Vincenzo Pupillo; +Cc: 71380-done

> From: Vincenzo Pupillo <vincenzo.pupillo@lpsd.it>
> Cc: 71380@debbugs.gnu.org
> Date: Wed, 12 Jun 2024 11:25:20 +0200
> 
> Hi Eli, this updated patch add the support for PHP var_modifier. 

Thanks, installed, and closing the bug.

I wonder if you'd like to add some tests for this new mode?





^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php
  2024-06-12 18:27                               ` Eli Zaretskii
@ 2024-06-12 20:44                                 ` Vincenzo Pupillo
  0 siblings, 0 replies; 29+ messages in thread
From: Vincenzo Pupillo @ 2024-06-12 20:44 UTC (permalink / raw)
  To: 71380, eliz, v.pupillo

In data mercoledì 12 giugno 2024 20:27:14 CEST, Eli Zaretskii ha scritto:
> > From: Vincenzo Pupillo <vincenzo.pupillo@lpsd.it>
> > Cc: 71380@debbugs.gnu.org
> > Date: Wed, 12 Jun 2024 11:25:20 +0200
> > 
> > Hi Eli, this updated patch add the support for PHP var_modifier.
> 
> Thanks, installed, and closing the bug.
> 
> I wonder if you'd like to add some tests for this new mode?
Yes, I will write some tests in the next few weeks.

I'm currently preparing a patch to color inline css exactly like with css-ts-
mode.
Thank you.

Vincenzo








^ permalink raw reply	[flat|nested] 29+ messages in thread

end of thread, other threads:[~2024-06-12 20:44 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-06-05 13:59 bug#71380: 30.0.50; Submitting php-ts-mode, new major mode for php Vincenzo Pupillo
2024-06-06  6:58 ` Eli Zaretskii
2024-06-07 10:45   ` Vincenzo Pupillo
2024-06-07 11:12     ` Eli Zaretskii
2024-06-07 12:50       ` Vincenzo Pupillo
2024-06-07 13:44         ` Eli Zaretskii
2024-06-07 15:05           ` Vincenzo Pupillo
2024-06-08  9:31             ` Vincenzo Pupillo
2024-06-08 10:45               ` Eli Zaretskii
2024-06-08 11:15                 ` Vincenzo Pupillo
2024-06-09 13:54                   ` Eli Zaretskii
2024-06-09 17:23                     ` Vincenzo Pupillo
2024-06-09 17:49                       ` Eli Zaretskii
2024-06-09 19:37                         ` Vincenzo Pupillo
2024-06-09 20:36                           ` Vincenzo Pupillo
2024-06-12  9:25                             ` Vincenzo Pupillo
2024-06-12 18:27                               ` Eli Zaretskii
2024-06-12 20:44                                 ` Vincenzo Pupillo
2024-06-09 13:53     ` Eli Zaretskii
2024-06-06 14:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-07  9:04   ` Vincenzo Pupillo
2024-06-07 12:53     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-07 13:25       ` Vincenzo Pupillo
2024-06-07 13:49         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-07 14:37           ` Vincenzo Pupillo
2024-06-06 14:54 ` Andrea Corallo
2024-06-07  8:36   ` Vincenzo Pupillo
2024-06-07 13:39     ` Andrea Corallo
2024-06-07 17:02       ` Vincenzo Pupillo

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).