Files
emacs/lisp/treemacs/treemacs-scope.el
2025-11-25 19:52:03 +01:00

279 lines
10 KiB
EmacsLisp

;;; treemacs.el --- A tree style file viewer package -*- lexical-binding: t -*-
;; Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Module that handles uniquely associating treemacs buffers with a
;; certain scope, like the selected frame, or (to be implemented
;; later) the active eyebrowse or persp desktop.
;; This is implemented using a (somewhat) OOP style with eieio and
;; static functions, where each scope type is expected to know how to
;; query the current scope (e.g. the selected frame) and how to set up
;; and tear down itself (e.g. deleting a frames associated buffer when
;; the frame is deleted)
;;; Code:
(require 'dash)
(require 'eieio)
(require 'treemacs-core-utils)
(require 'treemacs-customization)
(require 's)
(require 'inline)
(eval-when-compile
(require 'treemacs-macros)
(require 'cl-lib))
(eval-when-compile
(cl-declaim (optimize (speed 3) (safety 0))))
(treemacs-import-functions-from "treemacs-filewatch-mode"
treemacs--stop-filewatch-for-current-buffer)
(treemacs-import-functions-from "treemacs-interface"
treemacs-quit
treemacs-select-window)
(treemacs-import-functions-from "treemacs-workspaces"
treemacs--find-workspace)
(cl-defstruct (treemacs-scope-shelf
(:conc-name treemacs-scope-shelf->)
(:constructor treemacs-scope-shelf->create!))
buffer
workspace)
(defvar treemacs-scope-types (list (cons 'Frames 'treemacs-frame-scope))
"List of all known scope types.
The car is the name seen in interactive selection. The cdr is the eieio class
name.")
(defvar treemacs--current-scope-type 'treemacs-frame-scope
"The general type of objects/items treemacs is currently scoped to.")
(defvar treemacs--scope-storage nil
"Alist of all active scopes mapped to their buffers & workspaces.
The car is the scope, the cdr is a `treemacs-scope-shelf'.")
(define-inline treemacs-scope-shelf->kill-buffer (self)
"Kill the buffer stored in SELF."
(inline-letevals (self)
(inline-quote
(progn
(let ((buffer (treemacs-scope-shelf->buffer ,self)))
(when (buffer-live-p buffer) (kill-buffer buffer)))
(setf (treemacs-scope-shelf->buffer ,self) nil)))))
(define-inline treemacs--scope-store ()
"Return `treemacs--scope-storage'."
(inline-quote treemacs--scope-storage))
(define-inline treemacs-current-scope-type ()
"Return the current scope type."
(declare (side-effect-free t))
(inline-quote treemacs--current-scope-type))
(define-inline treemacs-current-scope ()
"Return the current scope."
(declare (side-effect-free t))
(inline-quote
(treemacs-scope->current-scope (treemacs-current-scope-type))))
(define-inline treemacs-current-scope-shelf (&optional scope)
"Return the current scope shelf, containing the active workspace and buffer.
Use either the given SCOPE or `treemacs-current-scope' otherwise.
Can be used with `setf'."
(declare (side-effect-free t))
(inline-letevals (scope)
(inline-quote
(cdr (assoc (or ,scope (treemacs-current-scope)) treemacs--scope-storage)))))
(gv-define-setter treemacs-current-scope-shelf (val)
`(let* ((current-scope (treemacs-current-scope))
(shelf-mapping (assoc current-scope treemacs--scope-storage)))
(if (cdr shelf-mapping)
(setf (cdr shelf-mapping) ,val)
(push (cons current-scope ,val) treemacs--scope-storage))))
(defclass treemacs-scope () () :abstract t)
(cl-defmethod treemacs-scope->current-scope ((_ (subclass treemacs-scope)))
"Get the current scope."
(error "Default `current-scope' implementation was called"))
(cl-defmethod treemacs-scope->current-scope-name ((_ (subclass treemacs-scope)) scope)
"Get the name of the given SCOPE."
(ignore scope)
nil)
(cl-defmethod treemacs-scope->setup ((_ (subclass treemacs-scope)))
"Setup for a scope type."
nil)
(cl-defmethod treemacs-scope->cleanup ((_ (subclass treemacs-scope)))
"Tear-down for a scope type."
nil)
(defclass treemacs-frame-scope (treemacs-scope) () :abstract t)
(cl-defmethod treemacs-scope->current-scope ((_ (subclass treemacs-frame-scope)))
"Get the current scope."
(selected-frame))
(cl-defmethod treemacs-scope->current-scope-name ((_ (subclass treemacs-frame-scope)) frame)
"Prints the given FRAME."
(prin1-to-string frame))
(cl-defmethod treemacs-scope->setup ((_ (subclass treemacs-frame-scope)))
"Frame-scope setup."
(add-hook 'delete-frame-functions #'treemacs--on-scope-kill))
(cl-defmethod treemacs-scope->cleanup ((_ (subclass treemacs-frame-scope)))
"Frame-scope tear-down."
(remove-hook 'delete-frame-functions #'treemacs--on-scope-kill))
(defun treemacs-set-scope-type (new-scope-type)
"Set a NEW-SCOPE-TYPE for treemacs buffers.
Valid values for TYPE are the `car's of the elements of `treemacs-scope-types'.
This is meant for programmatic use. For an interactive selection see
`treemacs-select-buffer-scope-type'."
(-let [class (alist-get new-scope-type treemacs-scope-types)]
(unless class (user-error "'%s' is not a valid scope new-scope-type. Valid types are: %s"
new-scope-type
(-map #'car treemacs-scope-types)))
(treemacs--do-set-scope-type class)))
(defun treemacs--do-set-scope-type (new-scope-type)
"Set NEW-SCOPE-TYPE as the scope managing class.
Kill all treemacs buffers and windows and reset the buffer store.
NEW-SCOPE-TYPE: T: treemacs-scope"
(treemacs-scope->cleanup treemacs--current-scope-type)
(setf treemacs--current-scope-type new-scope-type)
(dolist (frame (frame-list))
(dolist (window (window-list frame))
(when (treemacs-is-treemacs-window? window)
(delete-window window))))
(dolist (it treemacs--scope-storage)
(treemacs-scope-shelf->kill-buffer (cdr it)))
(setf treemacs--scope-storage nil)
(treemacs-scope->setup new-scope-type))
(defun treemacs--on-buffer-kill ()
"Cleanup to run when a treemacs buffer is killed."
(when (eq t treemacs--in-this-buffer)
;; stop watch must come first since we need a reference to the killed buffer
;; to remove it from the filewatch list
(treemacs--stop-filewatch-for-current-buffer)
;; not present for extension buffers
(-when-let (shelf (treemacs-current-scope-shelf))
(setf (treemacs-scope-shelf->buffer shelf) nil))))
(defun treemacs--on-scope-kill (scope)
"Kill and remove the buffer assigned to the given SCOPE."
(-when-let (shelf (treemacs-current-scope-shelf scope))
(treemacs-scope-shelf->kill-buffer shelf)
(setf treemacs--scope-storage (--reject-first (equal (car it) scope) treemacs--scope-storage))))
(defun treemacs--create-buffer-for-scope (scope)
"Create and store a new buffer for the given SCOPE."
(-let [shelf (treemacs-current-scope-shelf scope)]
(unless shelf
(setf shelf (treemacs-scope-shelf->create!))
(push (cons scope shelf) treemacs--scope-storage)
(treemacs--find-workspace (buffer-file-name)))
(treemacs-scope-shelf->kill-buffer shelf)
(let* ((name (format "%s%s"
treemacs-buffer-name-prefix
(or (funcall treemacs-buffer-name-function scope)
(treemacs-default-buffer-name scope))))
(buffer (get-buffer-create name)))
(setf (treemacs-scope-shelf->buffer shelf) buffer)
buffer)))
(defun treemacs-default-buffer-name (scope)
"Default buffer name implementation for a given SCOPE.
Appends the name of the given scope to the mandatory
`treemacs-buffer-name-prefix'."
(or (treemacs-scope->current-scope-name treemacs--current-scope-type scope)
(prin1-to-string scope)))
(defun treemacs--change-buffer-on-scope-change (&rest _)
"Switch the treemacs buffer after the current scope was changed."
(--when-let (treemacs-get-local-window)
(save-selected-window
(with-selected-window it
(treemacs-quit))
(treemacs-select-window))))
(defun treemacs--select-visible-window ()
"Switch to treemacs buffer, given that it is currently visible."
(-some->> treemacs--scope-storage
(assoc (treemacs-scope->current-scope treemacs--current-scope-type))
(cdr)
(treemacs-scope-shelf->buffer)
(get-buffer-window)
(select-window))
(run-hook-with-args 'treemacs-select-functions 'visible))
(defun treemacs-get-local-buffer ()
"Return the treemacs buffer local to the current scope-type.
Returns nil if no such buffer exists.."
(declare (side-effect-free t))
(let* ((scope (treemacs-scope->current-scope treemacs--current-scope-type))
(buffer (-some->> treemacs--scope-storage
(assoc scope)
(cdr)
(treemacs-scope-shelf->buffer))))
(and (buffer-live-p buffer) buffer)))
(defun treemacs-get-local-buffer-create ()
"Get the buffer for the current scope, creating a new one if needed."
(or (treemacs-get-local-buffer)
(treemacs--create-buffer-for-scope (treemacs-scope->current-scope treemacs--current-scope-type))))
(defun treemacs-get-local-window ()
"Return the window displaying the treemacs buffer in the current frame.
Returns nil if no treemacs buffer is visible."
(declare (side-effect-free error-free))
(->> (window-list (selected-frame))
(--first (->> it
(window-buffer)
(buffer-name)
(s-starts-with? treemacs-buffer-name-prefix)))))
(define-inline treemacs-current-visibility ()
"Return whether the current visibility state of the treemacs buffer.
Valid states are \\='visible, \\='exists and \\='none."
(declare (side-effect-free t))
(inline-quote
(cond
((treemacs-get-local-window) 'visible)
((treemacs-get-local-buffer) 'exists)
(t 'none))))
(treemacs-only-during-init
(setf treemacs--current-scope-type 'treemacs-frame-scope)
(treemacs-scope->setup 'treemacs-frame-scope))
(provide 'treemacs-scope)
;;; treemacs-scope.el ends here