;;; phpinspect-meta.el --- PHP parsing and completion package -*- lexical-binding: t; -*- ;; Copyright (C) 2021-2023 Free Software Foundation, Inc ;; Author: Hugo Thunnissen ;; Keywords: php, languages, tools, convenience ;; Version: 0 ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; ;; IMPLEMENTATION CONTEXT ;; ;; This file contains code for the storing and reading of metadata related to ;; the tokens in a parsed syntax tree. ;; ;; Phpinspect's parser uses basic lists as datastructure for its parsing ;; result. The simplicity of the datastructure makes it performant as the amount ;; of GC's triggered by simple lists is not as large as when using more complex ;; datastructures. ;; ;; A drawback of this simplicity is that attaching additional metadata to the ;; parsed tokens is nontrivial. A list is a list: There is no way to store ;; additional metadata in a separate slot that is not part of its overall ;; members. This is fine when parsing entire files and indexing their ;; contents. Efficiently maintaining the state of code in a buffer, however, is ;; a more involved process. It requires the storage of more metadata than just ;; the parsed tokens. ;; ;; For this reason, phpinspect uses a metadata tree for live buffers. The ;; metadata tree can be interacted with through functions prefixed with ;; "phpinspect-meta". It is an n-ary tree that can be navigated and searched ;; through using the start position of parsed tokens. Each metadata object ;; stores its direct children in a binary search tree (see phpinspect-splayt.el). ;; ;; The metadata tree makes it easy to dig down in a buffer's syntax tree and ;; determine the context from which a user is interacting with the PHP code in ;; it. ;; ;;; Code: (require 'phpinspect-splayt) (define-inline phpinspect-make-meta (parent start end whitespace-before token &optional overlay children parent-offset) (inline-quote (list 'meta ,parent ,start ,end ,whitespace-before ,token ,overlay ;;,children ,parent-offset))) (or ,children (phpinspect-make-splayt)) ,parent-offset))) (define-inline phpinspect-meta-parent (meta) (inline-quote (cadr ,meta))) (define-inline phpinspect-meta-children (meta) (inline-quote (car (nthcdr 7 ,meta)))) (define-inline phpinspect-meta-parent-offset (meta) (inline-quote (car (nthcdr 8 ,meta)))) (define-inline phpinspect-meta-overlay (meta) (inline-quote (car (nthcdr 6 ,meta)))) (define-inline phpinspect-meta-token (meta) (inline-quote (car (nthcdr 5 ,meta)))) (define-inline phpinspect-meta-absolute-end (meta) (inline-quote (cadddr ,meta))) (define-inline phpinspect-meta-whitespace-before (meta) (inline-quote (car (cddddr ,meta)))) (define-inline phpinspect-meta-parent-start (meta) "Calculate parent start position iteratively based on parent offsets." (inline-letevals (meta) (inline-quote (let ((start (or (phpinspect-meta-parent-offset ,meta) 0)) (current ,meta)) (while (phpinspect-meta-parent current) (setq current (phpinspect-meta-parent current) start (+ start (or (phpinspect-meta-parent-offset current) 0)))) (+ (phpinspect-meta-absolute-start current) start))))) (define-inline phpinspect-meta-start (meta) "Calculate the start position of META." (inline-quote (if (phpinspect-meta-parent ,meta) (+ (phpinspect-meta-parent-start (phpinspect-meta-parent ,meta)) (phpinspect-meta-parent-offset ,meta)) (phpinspect-meta-absolute-start ,meta)))) (define-inline phpinspect-meta-width (meta) (inline-letevals (meta) (inline-quote (- (phpinspect-meta-absolute-end ,meta) (phpinspect-meta-absolute-start ,meta))))) (define-inline phpinspect-meta-end (meta) (inline-letevals (meta) (inline-quote (+ (phpinspect-meta-start ,meta) (phpinspect-meta-width ,meta))))) (defsubst phpinspect-meta-find-root (meta) (while (phpinspect-meta-parent meta) (setq meta (phpinspect-meta-parent meta))) meta) (defun phpinspect-meta-sort-width (meta1 meta2) (< (phpinspect-meta-width meta1) (phpinspect-meta-width meta2))) (defun phpinspect-meta-sort-start (meta1 meta2) (< (phpinspect-meta-start meta1) (phpinspect-meta-start meta2))) (define-inline phpinspect-meta-absolute-start (meta) (inline-quote (caddr ,meta))) (define-inline phpinspect-meta-overlaps-point (meta point) "Check if META's region overlaps POINT." (inline-letevals (point meta) (inline-quote (and (> (phpinspect-meta-end ,meta) ,point) (<= (phpinspect-meta-start ,meta) ,point))))) (defun phpinspect-meta-find-parent-matching-token (meta predicate) "Find a parent metadata node of META, the token of which matches PREDICATE." (if (funcall predicate (phpinspect-meta-token meta)) meta (catch 'found (while (phpinspect-meta-parent meta) (setq meta (phpinspect-meta-parent meta)) (when (funcall predicate (phpinspect-meta-token meta)) (throw 'found meta)))))) (define-inline phpinspect-meta-set-parent (meta parent) (inline-letevals (meta parent) (inline-quote (progn (when ,parent (setf (phpinspect-meta-parent-offset ,meta) (- (phpinspect-meta-start ,meta) (phpinspect-meta-start ,parent))) (phpinspect-meta-add-child ,parent ,meta)) (setcar (cdr ,meta) ,parent) ,meta)))) ;; Note: using defsubst here causes a byte code overflow (defun phpinspect-meta-add-child (meta child) (phpinspect-splayt-insert (phpinspect-meta-children meta) (phpinspect-meta-parent-offset child) child)) (define-inline phpinspect-meta-detach-parent (meta) (inline-letevals (meta) (inline-quote (when (phpinspect-meta-parent ,meta) ;; Update absolute start and end (setf (phpinspect-meta-absolute-end ,meta) (phpinspect-meta-end ,meta)) ;; Note: start should always be updated after end, as end is derived from ;; it. (setf (phpinspect-meta-absolute-start ,meta) (phpinspect-meta-start ,meta)) (setf (phpinspect-meta-parent ,meta) nil))))) (defun phpinspect-meta-shift (meta delta) (setf (phpinspect-meta-absolute-start meta) (+ (phpinspect-meta-start meta) delta)) (setf (phpinspect-meta-absolute-end meta) (+ (phpinspect-meta-end meta) delta))) (defun phpinspect-meta-right-siblings (meta) (sort (phpinspect-splayt-find-all-after (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)) #'phpinspect-meta-sort-start)) (defun phpinspect-meta-left-sibling-tokens (meta) (let* ((tokens (cons nil nil)) (rear tokens)) (dolist (sibling (phpinspect-meta-left-siblings meta)) (setq rear (setcdr rear (cons (phpinspect-meta-token sibling) nil)))) (cdr tokens))) (defun phpinspect-meta-token-with-left-siblings (meta) (nconc (phpinspect-meta-left-sibling-tokens meta) (list (phpinspect-meta-token meta)))) (defun phpinspect-meta-left-siblings (meta) (sort (phpinspect-splayt-find-all-before (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)) #'phpinspect-meta-sort-start)) (defun phpinspect-meta-wrap-token-pred (predicate) (lambda (meta) (funcall predicate (phpinspect-meta-token meta)))) (define-inline phpinspect-meta--point-offset (meta point) (inline-quote (- ,point (phpinspect-meta-start ,meta)))) (cl-defmethod phpinspect-meta-find-left-sibling ((meta (head meta))) (when (phpinspect-meta-parent meta) (phpinspect-splayt-find-largest-before (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)))) (cl-defmethod phpinspect-meta-find-right-sibling ((meta (head meta))) (when (phpinspect-meta-parent meta) (phpinspect-splayt-find-smallest-after (phpinspect-meta-children (phpinspect-meta-parent meta)) (phpinspect-meta-parent-offset meta)))) (cl-defmethod phpinspect-meta-find-overlapping-child ((meta (head meta)) (point integer)) (let ((child (phpinspect-splayt-find-largest-before (phpinspect-meta-children meta) ;; Use point +1 as a child starting at point still overlaps (+ (phpinspect-meta--point-offset meta point) 1)))) (when (and child (phpinspect-meta-overlaps-point child point)) child))) (cl-defmethod phpinspect-meta-find-overlapping-children ((meta (head meta)) (point integer)) (let ((child meta) children) (while (and (setq child (phpinspect-meta-find-overlapping-child child point)) (phpinspect-meta-overlaps-point child point)) (push child children)) children)) (cl-defmethod phpinspect-meta-find-child-starting-at ((meta (head meta)) (point integer)) (phpinspect-splayt-find (phpinspect-meta-children meta) (phpinspect-meta--point-offset meta point))) (cl-defmethod phpinspect-meta-find-child-starting-at-recursively ((meta (head meta)) (point integer)) (let ((child (phpinspect-meta-find-child-starting-at meta point))) (if child child (setq child (phpinspect-meta-find-overlapping-child meta point)) (when child (phpinspect-meta-find-child-starting-at-recursively child point))))) (cl-defmethod phpinspect-meta-find-child-before ((meta (head meta)) (point integer)) (phpinspect-splayt-find-largest-before (phpinspect-meta-children meta) (phpinspect-meta--point-offset meta point))) (cl-defmethod phpinspect-meta-find-child-after ((meta (head meta)) (point integer)) (phpinspect-splayt-find-smallest-after (phpinspect-meta-children meta) (phpinspect-meta--point-offset meta point))) (cl-defmethod phpinspect-meta-find-child-before-recursively ((meta (head meta)) (point integer)) (let ((child meta) last) (while (setq child (phpinspect-meta-find-child-before child point)) (setq last child)) last)) (cl-defmethod phpinspect-meta-find-children-after ((meta (head meta)) (point integer)) (sort (phpinspect-splayt-find-all-after (phpinspect-meta-children meta) (phpinspect-meta--point-offset meta point)) #'phpinspect-meta-sort-start)) (cl-defmethod phpinspect-meta-find-children-before ((meta (head meta)) (point integer)) (sort (phpinspect-splayt-find-all-before (phpinspect-meta-children meta) (phpinspect-meta--point-offset meta point)) #'phpinspect-meta-sort-start)) (cl-defmethod phpinspect-meta-find-first-child-matching ((meta (head meta)) predicate) (catch 'return (phpinspect-splayt-traverse-lr (child (phpinspect-meta-children meta)) (when (funcall predicate child) (throw 'return child))))) (cl-defmethod phpinspect-meta-find-first-child-matching-token ((meta (head meta)) predicate) (catch 'return (phpinspect-splayt-traverse-lr (child (phpinspect-meta-children meta)) (when (funcall predicate (phpinspect-meta-token child)) (throw 'return child))))) (cl-defmethod phpinspect-meta-last-child ((meta (head meta))) (phpinspect-meta-find-child-before meta (phpinspect-meta-end meta))) (cl-defmethod phpinspect-meta-first-child ((meta (head meta))) (phpinspect-meta-find-child-after meta (- (phpinspect-meta-start meta) 1))) (defun phpinspect-meta-string (meta) (if meta (format "[start: %d, end: %d, token: %s]" (phpinspect-meta-start meta) (phpinspect-meta-end meta) (phpinspect-meta-token meta)) "[nil]")) (provide 'phpinspect-meta) ;;; phpinspect-meta.el ends here