;;; treemacs.el --- A tree style file viewer package -*- lexical-binding: t -*- ;; Copyright (C) 2021 Alexander Miller ;; 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: ;; Basically this: https://github.com/Alexander-Miller/treemacs/issues/143. ;;; Code: (require 'ht) (require 'dash) (require 's) (eval-when-compile (require 'cl-lib) (require 'inline) (require 'treemacs-macros)) (eval-when-compile (cl-declaim (optimize (speed 3) (safety 0)))) (defvar-local treemacs-dom nil) (cl-defstruct (treemacs-dom-node (:conc-name treemacs-dom-node->) (:constructor treemacs-dom-node->create!)) key parent children reentry-nodes position refresh-flag collapse-keys) ;; needed because simple declare-function for pos slot in core-utils wont properly expand via setf (define-inline treemacs-dom-node->set-position! (self value) "Set `position' field of SELF to VALUE. SELF: Dom Node Struct VALUE: Marker" (inline-letevals (self value) (inline-quote (setf (treemacs-dom-node->position ,self) ,value)))) (defun treemacs--reset-dom () "Reset the dom." (setf treemacs-dom (make-hash-table :size 1000 :test 'equal))) (define-inline treemacs-find-in-dom (key) "Get node with KEY, if any. KEY: Node Path" (declare (side-effect-free t)) (inline-letevals (key) (inline-quote (ht-get treemacs-dom ,key)))) (define-inline treemacs-dom-node->insert-into-dom! (self) "Insert SELF into the dom. SELF: Dom Node Struct" (inline-letevals (self) (inline-quote (ht-set! treemacs-dom (treemacs-dom-node->key ,self) ,self)))) (define-inline treemacs-dom-node->add-child! (self child) "Add CHILD to to the children of SELF." (inline-letevals (self child) (inline-quote (setf (treemacs-dom-node->children ,self) (cons ,child (treemacs-dom-node->children ,self)))))) (define-inline treemacs-dom-node->remove-from-dom! (self) "Remove SELF from the dom. SELF: Dom Node Struct" (inline-letevals (self) (inline-quote (progn (ht-remove! treemacs-dom (treemacs-dom-node->key ,self)) (let ((parent (treemacs-dom-node->parent ,self))) (setf (treemacs-dom-node->children parent) (delete ,self (treemacs-dom-node->children parent)))) (dolist (key (treemacs-dom-node->collapse-keys ,self)) (ht-remove! treemacs-dom key)))))) (define-inline treemacs-dom-node->remove-collapse-keys! (self keys) "Remove the given collapse KEYS from both SELF and the dom." (inline-letevals (self keys) (inline-quote (progn (dolist (key ,keys) (ht-remove! treemacs-dom key)) (setf (treemacs-dom-node->collapse-keys ,self) (--reject (member it ,keys) (treemacs-dom-node->collapse-keys ,self))))))) (define-inline treemacs-dom-node->all-parents (self) "Get all parent nodes of SELF. List will be sorted top to bottom. SELF: Dom Node Struct" (declare (side-effect-free t)) (inline-letevals (self) (inline-quote (let ((parent (treemacs-dom-node->parent ,self)) (ret)) (while parent (push parent ret) (setf parent (treemacs-dom-node->parent parent))) ret)))) (define-inline treemacs-on-expand (key pos) "Re-arrange the dom when node at KEY with POS is expanded. KEY: Node Path POS: Marker" (inline-letevals (key pos) (inline-quote (-if-let (dom-node (treemacs-find-in-dom ,key)) (progn (setf (treemacs-dom-node->position dom-node) ,pos) (dolist (collapse-key (treemacs-dom-node->collapse-keys dom-node)) (setf (treemacs-dom-node->position (treemacs-find-in-dom collapse-key)) ,pos)) (-when-let (parent-dom-node (treemacs-dom-node->parent dom-node)) (setf (treemacs-dom-node->reentry-nodes parent-dom-node) (cons dom-node (treemacs-dom-node->reentry-nodes parent-dom-node))))) ;; expansion of root (setf dom-node (treemacs-dom-node->create! :key ,key :position ,pos)) (treemacs-dom-node->insert-into-dom! dom-node))))) (define-inline treemacs-on-collapse (key &optional purge) "Re-arrange the dom when node at KEY was collapsed. Will remove NODE's parent/child link and invalidate the position and refresh data of NODE and all its children. When PURGE is non-nil will instead remove NODE and its children from the dom. KEY: Node Path Purge: Boolean" (inline-letevals (key purge) (inline-quote (let* ((dom-node (treemacs-find-in-dom ,key)) (children (treemacs-dom-node->children dom-node))) (-when-let (parent-dom-node (treemacs-dom-node->parent dom-node)) (setf (treemacs-dom-node->reentry-nodes parent-dom-node) (delete dom-node (treemacs-dom-node->reentry-nodes parent-dom-node)))) (cond (,purge (treemacs--on-purged-collapse dom-node)) (children (treemacs--on-collapse-of-node-with-children dom-node)) (t (treemacs--on-collapse-of-node-without-children dom-node))))))) (define-inline treemacs--on-purged-collapse (dom-node) "Run when a DOM-NODE is collapsed with a purge (prefix) argument. Will remove all the children of DOM-NODE from the dom. DOM-NODE: Dom Node Struct" (inline-letevals (dom-node) (inline-quote (progn (treemacs-walk-dom-exclusive ,dom-node (lambda (it) (treemacs-dom-node->remove-from-dom! it))) (setf (treemacs-dom-node->children ,dom-node) nil (treemacs-dom-node->reentry-nodes ,dom-node) nil))))) (define-inline treemacs--on-collapse-of-node-without-children (dom-node) "Run when a DOM-NODE without any children is collapsed. Will remove DOm-NODE from its parent's reentry list. DOM-NODE: Dom Node Struct" (inline-letevals (dom-node) (inline-quote (let ((parent-dom-node (treemacs-dom-node->parent ,dom-node))) (when parent-dom-node (setf (treemacs-dom-node->reentry-nodes parent-dom-node) (delete ,dom-node (treemacs-dom-node->reentry-nodes parent-dom-node)))))))) (define-inline treemacs--on-collapse-of-node-with-children (dom-node) "Run when a DOM-NODE with children is collapsed. Will remove all entries below the one collapsed from the dom. DOM-NODE: Dom Node Struct" (inline-letevals (dom-node) (inline-quote (progn (treemacs-walk-dom-exclusive ,dom-node (lambda (it) (treemacs-dom-node->remove-from-dom! it) (setf (treemacs-dom-node->children it) nil))) (setf (treemacs-dom-node->children ,dom-node) nil))))) (defun treemacs--on-rename (old-name new-name dont-rename-initial) "Renames dom entries after a file was renamed from OLD-NAME to NEW-NAME. Renames the initial dom entry (the one backing the file that was actually renamed) only if DONT-RENAME-INITIAL is nil in case the entry is required for filewatch-mode to work. OLD-NAME: File Path | Tag Path NEW-NAME: File Path | Tag Path DONT-RENAME-INITIAL: Boolean" (-when-let (dom-node (treemacs-find-in-dom old-name)) (-let [migrate-keys (lambda (it) (let* ((old-key (treemacs-dom-node->key it)) (new-key (cond ((stringp old-key) (s-replace old-name new-name old-key)) ((and (consp old-key) (stringp (car old-key))) (cons (s-replace old-name new-name (car old-key)) (cdr old-key)))))) (when new-key (ht-remove! treemacs-dom old-key) (ht-set! treemacs-dom new-key it) (setf (treemacs-dom-node->key it) new-key))))] ;; when filewatch is enabled the acutally renamed file needs to keep ;; its dom entry until refresh actually runs so it can be deleted properly (if dont-rename-initial (progn (treemacs-walk-reentry-dom-exclusive dom-node migrate-keys) (treemacs-walk-dom-exclusive dom-node migrate-keys)) (treemacs-walk-dom dom-node migrate-keys) (treemacs-walk-reentry-dom dom-node migrate-keys))))) (defun treemacs-walk-dom (node fn) "Recursively walk the dom starting at NODE. Calls FN on every node encountered in a depth-first pattern, starting with the deepest. This assures that FN may destructively modify the dom, at least on levels the one currently visiting. NODE: Dom Node Struct FN: (Dom Node) -> Any" (declare (indent 1)) (-let [children (treemacs-dom-node->children node)] (funcall fn node) (dolist (it children) (treemacs-walk-dom it fn)))) (defun treemacs-walk-dom-exclusive (node fn) "Same as `treemacs-walk-dom', but start NODE will not be passed to FN. NODE: Dom Node Struct FN: (Dom Node) -> Any" (declare (indent 1)) (dolist (it (treemacs-dom-node->children node)) (treemacs-walk-dom it fn))) (defun treemacs-walk-reentry-dom (node fn) "Recursively walk the dom starting at NODE. Unlike `treemacs-walk-dom' only expanded nodes are selected. Calls FN on every node encountered in a depth-first pattern, starting with the deepest. This assures that FN may destructively modify the dom, at least on levels the one currently visiting. NODE: Dom Node Struct FN: (Dom Node) -> Any" (declare (indent 1)) (funcall fn node) (dolist (it (treemacs-dom-node->reentry-nodes node)) (treemacs-walk-reentry-dom it fn))) (defun treemacs-walk-reentry-dom-exclusive (node fn) "Same as `treemacs-walk-reentry-dom', but start NODE will not be passed to FN. NODE: Dom Node Struct FN: (Dom Node) -> Any" (declare (indent 1)) (dolist (it (treemacs-dom-node->reentry-nodes node)) (treemacs-walk-reentry-dom it fn))) (provide 'treemacs-dom) ;;; treemacs-dom.el ends here