Files
emacs/lisp/treemacs/treemacs-mode.el
2025-06-22 17:08:08 +02:00

451 lines
20 KiB
EmacsLisp

;;; treemacs-mode.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:
;; Major mode definition.
;;; Code:
(require 'eldoc)
(require 's)
(require 'treemacs-interface)
(require 'treemacs-customization)
(require 'treemacs-faces)
(require 'treemacs-core-utils)
(require 'treemacs-icons)
(require 'treemacs-scope)
(require 'treemacs-persistence)
(require 'treemacs-dom)
(require 'treemacs-workspaces)
(require 'treemacs-visuals)
(eval-when-compile
(require 'treemacs-macros)
(require 'cl-lib))
(with-eval-after-load 'bookmark
(require 'treemacs-bookmarks))
(treemacs-import-functions-from "treemacs"
treemacs-refresh
treemacs-version
treemacs-edit-workspaces)
(treemacs-import-functions-from "treemacs-bookmarks"
treemacs-add-bookmark
treemacs--make-bookmark-record)
(treemacs-import-functions-from "treemacs-hydras"
treemacs-helpful-hydra
treemacs-common-helpful-hydra
treemacs-advanced-helpful-hydra)
(treemacs-import-functions-from "treemacs-tags"
treemacs--create-imenu-index-function)
(defvar bookmark-make-record-function)
(defvar-local treemacs--eldoc-msg nil
"Message to be output by `treemacs--eldoc-function'.
Will be set by `treemacs--post-command'.")
(defconst treemacs--eldoc-obarray
(-let [ob (make-vector 59 0)]
(mapatoms
(lambda (cmd) (set (intern (symbol-name cmd) ob) t))
eldoc-message-commands)
(dolist (cmd '(treemacs-next-line
treemacs-previous-line
treemacs-next-neighbour
treemacs-previous-neighbour
treemacs-next-project
treemacs-previous-project
treemacs-goto-parent-node
treemacs-TAB-action
treemacs-select-window
treemacs-leftclick-action))
(set (intern (symbol-name cmd) ob) t))
ob)
"Treemacs' own eldoc obarray.")
(defvar treemacs-project-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "r") 'treemacs-rename-project)
(define-key map (kbd "a") 'treemacs-add-project-to-workspace)
(define-key map (kbd "d") 'treemacs-remove-project-from-workspace)
(define-key map (kbd "c c") 'treemacs-collapse-project)
(define-key map (kbd "c o") 'treemacs-collapse-other-projects)
(define-key map (kbd "c a") 'treemacs-collapse-all-projects)
map)
"Keymap for project-related commands in `treemacs-mode'.")
(defvar treemacs-workspace-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "r") 'treemacs-rename-workspace)
(define-key map (kbd "a") 'treemacs-create-workspace)
(define-key map (kbd "d") 'treemacs-remove-workspace)
(define-key map (kbd "s") 'treemacs-switch-workspace)
(define-key map (kbd "e") 'treemacs-edit-workspaces)
(define-key map (kbd "f") 'treemacs-set-fallback-workspace)
(define-key map (kbd "n") 'treemacs-next-workspace)
map)
"Keymap for workspace-related commands in `treemacs-mode'.")
(defvar treemacs-node-visit-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "v") 'treemacs-visit-node-vertical-split)
(define-key map (kbd "c") 'treemacs-visit-node-close-treemacs)
(define-key map (kbd "h") 'treemacs-visit-node-horizontal-split)
(define-key map (kbd "o") 'treemacs-visit-node-no-split)
(define-key map (kbd "aa") 'treemacs-visit-node-ace)
(define-key map (kbd "ah") 'treemacs-visit-node-ace-horizontal-split)
(define-key map (kbd "av") 'treemacs-visit-node-ace-vertical-split)
(define-key map (kbd "r") 'treemacs-visit-node-in-most-recently-used-window)
(define-key map (kbd "x") 'treemacs-visit-node-in-external-application)
map)
"Keymap for node-visiting commands in `treemacs-mode'.")
(defvar treemacs-toggle-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "h") 'treemacs-toggle-show-dotfiles)
(define-key map (kbd "i") 'treemacs-hide-gitignored-files-mode)
(define-key map (kbd "w") 'treemacs-toggle-fixed-width)
(define-key map (kbd "v") 'treemacs-fringe-indicator-mode)
(define-key map (kbd "g") 'treemacs-git-mode)
(define-key map (kbd "f") 'treemacs-follow-mode)
(define-key map (kbd "a") 'treemacs-filewatch-mode)
(define-key map (kbd "n") 'treemacs-indent-guide-mode)
(define-key map (kbd "c") 'treemacs-indicate-top-scroll-mode)
(define-key map (kbd "d") 'treemacs-git-commit-diff-mode)
map)
"Keymap for commands that toggle state in `treemacs-mode'.")
(defvar treemacs-copy-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "a") 'treemacs-copy-absolute-path-at-point)
(define-key map (kbd "r") 'treemacs-copy-relative-path-at-point)
(define-key map (kbd "p") 'treemacs-copy-project-path-at-point)
(define-key map (kbd "n") 'treemacs-copy-filename-at-point)
(define-key map (kbd "f") 'treemacs-copy-file)
(define-key map (kbd "v") 'treemacs-paste-dir-at-point-to-minibuffer)
map)
"Keymap for copy commands in `treemacs-mode'.")
(defvar treemacs-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "?") 'treemacs-common-helpful-hydra)
(define-key map (kbd "C-?") 'treemacs-advanced-helpful-hydra)
(define-key map [down-mouse-1] 'treemacs-leftclick-action)
(define-key map [drag-mouse-1] 'treemacs-dragleftclick-action)
(define-key map [double-mouse-1] 'treemacs-doubleclick-action)
(define-key map [mouse-3] 'treemacs-rightclick-menu)
(define-key map [tab] 'treemacs-TAB-action)
(define-key map [?\t] 'treemacs-TAB-action)
(define-key map [return] 'treemacs-RET-action)
(define-key map (kbd "RET") 'treemacs-RET-action)
(define-key map (kbd "r") 'treemacs-refresh)
(define-key map (kbd "d") 'treemacs-delete-file)
(define-key map (kbd "cf") 'treemacs-create-file)
(define-key map (kbd "cd") 'treemacs-create-dir)
(define-key map (kbd "R") 'treemacs-rename-file)
(define-key map (kbd "u") 'treemacs-goto-parent-node)
(define-key map (kbd "q") 'treemacs-quit)
(define-key map (kbd "Q") 'treemacs-kill-buffer)
(define-key map (kbd "o") treemacs-node-visit-map)
(define-key map (kbd "P") 'treemacs-peek-mode)
(define-key map (kbd "n") 'treemacs-next-line)
(define-key map (kbd "p") 'treemacs-previous-line)
(define-key map (kbd "M-N") 'treemacs-next-line-other-window)
(define-key map (kbd "M-P") 'treemacs-previous-line-other-window)
(define-key map (kbd "<prior>") 'treemacs-previous-page-other-window)
(define-key map (kbd "<next>") 'treemacs-next-page-other-window)
(define-key map (kbd "M-n") 'treemacs-next-neighbour)
(define-key map (kbd "M-p") 'treemacs-previous-neighbour)
(define-key map (kbd "t") treemacs-toggle-map)
(define-key map (kbd "w") 'treemacs-set-width)
(define-key map (kbd "<") 'treemacs-decrease-width)
(define-key map (kbd ">") 'treemacs-increase-width)
(define-key map (kbd "y") treemacs-copy-map)
(define-key map (kbd "m") 'treemacs-move-file)
(define-key map (kbd "g") 'treemacs-refresh)
(define-key map (kbd "s") 'treemacs-resort)
(define-key map (kbd "b") 'treemacs-add-bookmark)
(define-key map (kbd "C-c C-p") treemacs-project-map)
(define-key map (kbd "C-c C-w") treemacs-workspace-map)
(define-key map (kbd "<M-up>") 'treemacs-move-project-up)
(define-key map (kbd "<M-down>") 'treemacs-move-project-down)
(define-key map (kbd "<backtab>") 'treemacs-collapse-all-projects)
(define-key map (kbd "C-j") 'treemacs-next-project)
(define-key map (kbd "C-k") 'treemacs-previous-project)
(define-key map (kbd "h") 'treemacs-COLLAPSE-action)
(define-key map (kbd "l") 'treemacs-RET-action)
(define-key map (kbd "M-h") 'treemacs-COLLAPSE-action)
(define-key map (kbd "M-l") 'treemacs-RET-action)
(define-key map (kbd "M-H") 'treemacs-root-up)
(define-key map (kbd "M-L") 'treemacs-root-down)
(define-key map (kbd "H") 'treemacs-collapse-parent-node)
(define-key map (kbd "!") 'treemacs-run-shell-command-for-current-node)
(define-key map (kbd "M-!") 'treemacs-run-shell-command-in-project-root)
(define-key map (kbd "C") 'treemacs-cleanup-litter)
(define-key map (kbd "=") 'treemacs-fit-window-width)
(define-key map (kbd "W") 'treemacs-extra-wide-toggle)
(define-key map (kbd "M-m") 'treemacs-bulk-file-actions)
(unless (window-system)
(define-key map [C-i] 'treemacs-TAB-action))
map)
"Keymap for `treemacs-mode'.")
(defun treemacs--setup-mode-line ()
"Create either a simple modeline, or integrate into spaceline."
(setq mode-line-format
(cond (treemacs-user-mode-line-format
(if (eq 'none treemacs-user-mode-line-format)
nil
treemacs-user-mode-line-format))
((fboundp 'spaceline-install)
(spaceline-install
"treemacs" '((workspace-number
:face highlight-face)
major-mode)
nil)
'("%e" (:eval (spaceline-ml-treemacs))))
((and (listp (default-value 'mode-line-format))
(member 'moody-mode-line-buffer-identification
(default-value 'mode-line-format)))
'(:eval (moody-tab " Treemacs " 10 'down)))
((featurep 'doom-modeline)
(with-no-warnings
(eval
'(progn
(require 'doom-modeline)
(doom-modeline-def-segment treemacs-workspace-name
"Display treemacs."
(propertize (format " %s " (treemacs-workspace->name (treemacs-current-workspace)))
'face (doom-modeline-face 'doom-modeline-buffer-minor-mode)))
(doom-modeline-def-modeline 'treemacs '(bar " " major-mode) '(treemacs-workspace-name))
(doom-modeline 'treemacs)))))
(t
'(:eval (format " Treemacs: %s"
(treemacs-workspace->name (treemacs-current-workspace))))))))
(defun treemacs--post-command ()
"Set the default directory to the nearest directory of the current node.
If there is no node at point use \"~/\" instead.
Also skip hidden buttons (as employed by variadic extensions).
Used as a post command hook."
(let ((newline-char 10)
(point-max (point-max)))
(unless (equal newline-char (char-before point-max))
(treemacs-with-writable-buffer
(save-excursion
(goto-char point-max)
(insert newline-char)
;; make sure that the projects-end marker keeps pointing at
;; the end of the last project button
(when (and (eq t treemacs--in-this-buffer)
(equal (point) (marker-position (treemacs--projects-end))))
(move-marker (treemacs--projects-end) (1- (point))))))))
(-when-let (btn (treemacs-current-button))
(when (treemacs-button-get btn 'invisible)
(treemacs-next-line 1))
(-if-let* ((project (treemacs-project-of-node btn))
(path (or (treemacs-button-get btn :default-directory)
(treemacs--nearest-path btn))))
(when (and (treemacs-project->is-readable? project)
(file-readable-p path))
(setf treemacs--eldoc-msg (treemacs--get-eldoc-message path)
default-directory (treemacs--add-trailing-slash
(if (file-directory-p path) path (file-name-directory path)))))
(setf treemacs--eldoc-msg nil)
(when (eq t treemacs--in-this-buffer)
(setf default-directory "~/")))))
(defun treemacs--get-eldoc-message (path)
"Set the eldoc message for given PATH.
Message will be either just the path, or the path plus meta info like file size,
depending on the value of `treemacs-eldoc-display'."
(pcase treemacs-eldoc-display
('detailed
(-let [attr (file-attributes path)]
(format "%s -- %s: %s %s: %s %s: %s"
(propertize path 'face 'font-lock-string-face)
(propertize "Size" 'face 'font-lock-keyword-face)
(propertize
(treemacs--human-readable-bytes (file-attribute-size attr))
'face 'font-lock-type-face)
(propertize "Last Modified" 'face 'font-lock-keyword-face)
(propertize
(format-time-string "%F %T" (file-attribute-modification-time attr))
'face 'font-lock-type-face)
(propertize "Permissions" 'face 'font-lock-keyword-face)
(propertize
(file-attribute-modes attr)
'face 'font-lock-type-face))))
('simple (propertize path 'face 'font-lock-string-face))
(_ (propertize path 'face 'font-lock-string-face))))
(define-inline treemacs--human-readable-bytes (bytes)
"Return a human-readable string version of BYTES."
(declare (pure t) (side-effect-free t))
(inline-letevals (bytes)
(inline-quote
(cl-loop with result = (cons "B" ,bytes)
for i in '("k" "M" "G" "T" "P" "E" "Z" "Y")
while (>= (cdr result) 1024.0)
do (setf result (cons i (/ (cdr result) 1024.0)))
finally return
(pcase (car result)
("B" (format "%sb" ,bytes))
(_ (format "%.1f%s" (cdr result) (car result))))))))
(defun treemacs--eldoc-function ()
"Treemacs' implementation of `eldoc-documentation-function'.
Will simply return `treemacs--eldoc-msg'."
(when (and treemacs-eldoc-display treemacs--eldoc-msg)
treemacs--eldoc-msg))
;;;###autoload
(define-derived-mode treemacs-mode special-mode "Treemacs"
"A major mode for displaying the file system in a tree layout."
(setq buffer-read-only t
truncate-lines t
indent-tabs-mode nil
desktop-save-buffer nil
window-size-fixed (when treemacs-width-is-initially-locked 'width)
treemacs--in-this-buffer t)
(unless treemacs-show-cursor
(setq cursor-type nil))
(when (boundp 'evil-treemacs-state-cursor)
(with-no-warnings
(setq evil-treemacs-state-cursor
(if treemacs-show-cursor
evil-motion-state-cursor
(lambda () (setq cursor-type nil))))))
;; higher fuzz value makes it less likely to start a mouse drag
;; and make a switch to visual state
(setq-local double-click-fuzz 15)
(setq-local show-paren-mode nil)
(setq-local tab-width 1)
(setq-local eldoc-documentation-function #'treemacs--eldoc-function)
(setq-local eldoc-message-commands treemacs--eldoc-obarray)
(setq-local imenu-create-index-function #'treemacs--create-imenu-index-function)
(when (boundp 'context-menu-functions)
(setq-local context-menu-functions nil))
;; integrate with bookmark.el
(setq-local bookmark-make-record-function #'treemacs--make-bookmark-record)
(electric-indent-local-mode -1)
(visual-line-mode -1)
(font-lock-mode -1)
(jit-lock-mode nil)
(buffer-disable-undo)
;; fringe indicator must be set up right here, before hl-line-mode, since activating hl-line-mode will
;; invoke the movement of the fringe overlay that would otherwise be nil
(when treemacs-fringe-indicator-mode
(treemacs--enable-fringe-indicator))
(if treemacs-user-header-line-format
(setf header-line-format treemacs-user-header-line-format)
(when header-line-format
(setf header-line-format nil)))
(hl-line-mode t)
;; needs to run manually the first time treemacs is loaded, since the hook is only added *after*
;; the window config was changed to show treemacs
(unless (member #'treemacs--on-window-config-change (default-value 'window-configuration-change-hook))
(treemacs--on-window-config-change))
;; set the parameter immediately so it can take effect when `treemacs' is called programatically
;; alongside other window layout chaning commands that might delete it again.
;; also check that the buffer has a window as other packages might call `treemacs-mode' on buffers
;; that have no window yet
(when (get-buffer-window (current-buffer))
(set-window-parameter (selected-window) 'no-delete-other-windows treemacs-no-delete-other-windows))
(face-remap-add-relative 'default 'treemacs-window-background-face)
(face-remap-add-relative 'fringe 'treemacs-window-background-face)
(face-remap-add-relative 'hl-line 'treemacs-hl-line-face)
(when treemacs-text-scale
(text-scale-increase treemacs-text-scale))
(add-hook 'window-configuration-change-hook #'treemacs--on-window-config-change)
(add-hook 'kill-buffer-hook #'treemacs--on-buffer-kill nil t)
(add-hook 'post-command-hook #'treemacs--post-command nil t)
(treemacs--build-indentation-cache 6)
(treemacs--select-icon-set)
(treemacs--setup-mode-line)
(treemacs--reset-dom))
(defun treemacs--mode-check-advice (mode-activation &rest args)
"Verify that `treemacs-mode' is called in the right place.
Must be run as advice to prevent changing of the major mode.
Will run original MODE-ACTIVATION and its ARGS only when
`treemacs--in-this-buffer' is non-nil."
(cond
(treemacs--in-this-buffer
(apply mode-activation args))
((eq major-mode 'treemacs-mode)
(ignore "Reactivating the major-mode resets buffer-local variables."))
(t
(switch-to-buffer (get-buffer-create "*Clippy*"))
(erase-buffer)
(insert
(format
"
--------------------------------------------------------------------------------------
| It looks like you are trying to run treemacs. Would you like some help with that? |
| You have called %s, but that is just the major mode for treemacs' |
| buffers, it is not meant to be used manually. |
| |
| Instead you should call a function like |
| * %s, |
| * %s, or |
| * %s |
| |
| You can safely delete this buffer. |
--------------------------------------------------------------------------------------
%s
"
(propertize "treemacs-mode" 'face 'font-lock-function-name-face)
(propertize "treemacs" 'face 'font-lock-function-name-face)
(propertize "treemacs-select-window" 'face 'font-lock-function-name-face)
(propertize "treemacs-add-and-display-current-project" 'face 'font-lock-function-name-face)
(propertize
"\
\\
\\
____
/ \\
| |
@ @
| |
|| |/
|| ||
|\\_/|
\\___/" 'face 'font-lock-keyword-face))))))
(advice-add #'treemacs-mode :around #'treemacs--mode-check-advice)
(provide 'treemacs-mode)
;;; treemacs-mode.el ends here