diff --git a/src/workgroups-buf.el b/src/workgroups-buf.el new file mode 100644 index 0000000000000000000000000000000000000000..4b7a8db3932d385fd92f24276b9cfdf2ece87adb --- /dev/null +++ b/src/workgroups-buf.el @@ -0,0 +1,483 @@ +;;; workgroups-buf.el --- BUFFER class +;;; Commentary: +;; +;; Workgroups Data Structures: +;; https://github.com/pashinin/workgroups2/wiki/Workgroups-data-structures +;; +;; +;; BUFFER is the most low level part Workgroups operates with (except +;; serializing Emacs objects functions). +;; +;; Different buffers we have: +;; - live buffers (just switch-to them) +;; - files/dirs (open them) +;; - special buffers (shells, unknown modes - write support for them) +;; +;; Another different types of "buffers": +;; - standard Emacs buffer (as you know it) +;; - Workgroups Buffer object (Elisp object, a representation of Emacs buffer) +;;; Code: + +(require 'workgroups-pickel) +(require 'workgroups-specialbufs) +(require 'workgroups-structs) + +;;; Variables + +(defvar wg-buffer-workgroup nil + "A workgroup in which this buffer most recently appeared. +Buffer-local.") +(make-variable-buffer-local 'wg-buffer-workgroup) + +(defcustom wg-default-buffer "*scratch*" + "Show this in case everything else fails. +When a buffer can't be restored, when creating a blank wg." + :type 'string + :group 'workgroups) + + +;;; Functions + +(defmacro wg-buf-list () + "Setf'able `wg-current-session' buf-list slot accessor." + `(wg-session-buf-list (wg-current-session))) + +(defun wg-restore-default-buffer () + "Switch to `wg-default-buffer'." + (switch-to-buffer wg-default-buffer t)) + +(defun wg-restore-existing-buffer (buf) + "Just switch to and return existing buffer." + (wg-awhen (wg-find-buf-in-buffer-list buf (buffer-list)) + (switch-to-buffer it t) + (wg-set-buffer-uid-or-error (wg-buf-uid buf)) + it)) + +(defun wg-restore-file-buffer (buf) + "Restore BUF by finding its file. Return the created buffer. +If BUF's file doesn't exist, call `wg-restore-default-buffer'" + (wg-when-let ((file-name (wg-buf-file-name buf))) + (when (or wg-restore-remote-buffers + (not (file-remote-p file-name))) + (cond ((file-exists-p file-name) + (find-file file-name) + (rename-buffer (wg-buf-name buf) t) + (wg-set-buffer-uid-or-error (wg-buf-uid buf)) + (when wg-restore-mark + (set-mark (wg-buf-mark buf)) + (deactivate-mark)) + (wg-deserialize-buffer-local-variables buf) + (current-buffer)) + (t + ;; try directory + (if (not (file-remote-p file-name)) + (if (file-directory-p (file-name-directory file-name)) + (progn + (dired (file-name-directory file-name)) + (current-buffer)) + (progn + (message "Attempt to restore nonexistent file: %S" file-name) + nil)) + nil) + ))))) + +(defun wg-restore-special-buffer (buf) + "Restore a buffer BUF with DESERIALIZER-FN." + (wg-when-let + ((special-data (wg-buf-special-data buf)) + (buffer (save-window-excursion + (condition-case err + (funcall (car special-data) buf) + (error (message "Error deserializing %S: %S" + (wg-buf-name buf) err) + nil))))) + (switch-to-buffer buffer t) + (wg-set-buffer-uid-or-error (wg-buf-uid buf)) + buffer)) + +(defun wg-restore-buffer (buf) + "Restore BUF and return it." + (or (wg-restore-existing-buffer buf) + (wg-restore-special-buffer buf) + (wg-restore-file-buffer buf) + (progn (wg-restore-default-buffer) nil))) + + +;;; buffer object utils + +(defun wg-buffer-uid (buffer-or-name) + "Return BUFFER-OR-NAME's buffer-local value of `wg-buffer-uid'." + (buffer-local-value 'wg-buffer-uid (wg-get-buffer buffer-or-name))) + +(defun wg-bufobj-uid (bufobj) + "Return BUFOBJ's uid." + (cl-etypecase bufobj + (buffer (wg-buffer-uid bufobj)) + (wg-buf (wg-buf-uid bufobj)) + (string (wg-bufobj-uid (wg-get-buffer bufobj))))) + +(defun wg-bufobj-name (bufobj) + "Return BUFOBJ's buffer name." + (cl-etypecase bufobj + (buffer (buffer-name bufobj)) + (wg-buf (wg-buf-name bufobj)) + (string (wg-buffer-name bufobj)))) + +(defun wg-bufobj-file-name (bufobj) + "Return BUFOBJ's filename." + (cl-etypecase bufobj + (buffer (buffer-file-name bufobj)) + (wg-buf (wg-buf-file-name bufobj)) + (string (wg-bufobj-file-name (wg-get-buffer bufobj))))) + +(defun wg-buf-major-mode (buf) + "Return BUF's `major-mode'. +It's stored in BUF's local-vars list, since it's a local variable." + (wg-aget (wg-buf-local-vars buf) 'major-mode)) + +(defun wg-buffer-major-mode (bufobj) + "Return BUFOBJ's `major-mode'. +It works with Emacs buffer, Workgroups buffer object and a simple string." + (cl-etypecase bufobj + (buffer (wg-buffer-major-mode bufobj)) + (wg-buf (wg-buf-major-mode bufobj)) + (string (wg-buffer-major-mode bufobj)))) + +;; `wg-equal-bufobjs' and `wg-find-bufobj' may need to be made a lot smarter +(defun wg-equal-bufobjs (bufobj1 bufobj2) + "Return t if BUFOBJ1 is \"equal\" to BUFOBJ2." + (let ((fname1 (wg-bufobj-file-name bufobj1)) + (fname2 (wg-bufobj-file-name bufobj2))) + (cond ((and fname1 fname2) (string= fname1 fname2)) + ((or fname1 fname2) nil) + ((string= (wg-bufobj-name bufobj1) (wg-bufobj-name bufobj2)) t)))) + +(defun wg-find-bufobj (bufobj bufobj-list) + "Find BUFOBJ in BUFOBJ-LIST, testing with `wg-equal-bufobjs'." + (cl-find bufobj bufobj-list :test 'wg-equal-bufobjs)) + +(defun wg-find-bufobj-by-uid (uid bufobj-list) + "Find the bufobj in BUFOBJ-LIST with uid UID." + (cl-find uid bufobj-list :test 'string= :key 'wg-bufobj-uid)) + +(defun wg-find-buf-in-buf-list (buf buf-list) + "Find BUF in BUF-LIST. +This is only here for completeness." + (cl-find buf buf-list)) + +(defun wg-find-buffer-in-buffer-list (buffer-or-name buffer-list) + "Find BUFFER-OR-NAME in BUFFER-LIST." + (cl-find (wg-get-buffer buffer-or-name) buffer-list :key 'wg-get-buffer)) + +(defun wg-find-buffer-in-buf-list (buffer-or-name buf-list) + "Find BUFFER-OR-NAME in BUF-LIST." + (wg-aif (wg-buffer-uid buffer-or-name) + (wg-find-bufobj-by-uid it buf-list) + (wg-find-bufobj buffer-or-name buf-list))) + +(defun wg-find-buf-in-buffer-list (buf buffer-list) + "Find BUF in BUFFER-LIST." + (or (wg-find-bufobj-by-uid (wg-buf-uid buf) buffer-list) + (wg-find-bufobj buf buffer-list))) + +(defun wg-find-buf-by-uid (uid) + "Find a buf in `wg-buf-list' by UID." + (wg-find-bufobj-by-uid uid (wg-buf-list))) + +(defun wg-set-buffer-uid-or-error (uid &optional buffer) + "Set BUFFER's buffer local value of `wg-buffer-uid' to UID. +If BUFFER already has a buffer local value of `wg-buffer-uid', +and it's not equal to UID, error." + (if wg-buffer-uid + ;;(if (string= wg-buffer-uid uid) uid + ;; (error "uids don't match %S and %S" uid wg-buffer-uid)) + (setq wg-buffer-uid uid))) + + +(defun wg-buffer-special-data (buffer) + "Return BUFFER's auxiliary serialization, or nil." + (cl-some (lambda (fn) (funcall fn buffer)) wg-special-buffer-serdes-functions)) + + +(defun wg-serialize-buffer-local-variables () + "Return an alist of buffer-local variable symbols and their values. +See `wg-buffer-local-variables-alist' for details." + (wg-docar (entry wg-buffer-local-variables-alist) + (wg-dbind (var ser des) entry + (when (local-variable-p var) + (cons var (if ser (funcall ser) (symbol-value var))))))) + +(defun wg-buffer-to-buf (buffer) + "Return the serialization (a wg-buf) of Emacs buffer BUFFER." + (with-current-buffer buffer + (wg-make-buf + :name (buffer-name) + :file-name (buffer-file-name) + :point (point) + :mark (mark) + :local-vars (wg-serialize-buffer-local-variables) + :special-data (wg-buffer-special-data buffer)))) + +(defun wg-add-buffer-to-buf-list (buffer) + "Make a buf from BUFFER, and add it to `wg-buf-list' if necessary. +If there isn't already a buf corresponding to BUFFER in +`wg-buf-list', make one and add it. Return BUFFER's uid +in either case." + (with-current-buffer buffer + (setq wg-buffer-uid + (wg-aif (wg-find-buffer-in-buf-list buffer (wg-buf-list)) + (wg-buf-uid it) + (let ((buf (wg-buffer-to-buf buffer))) + (push buf (wg-buf-list)) + (wg-buf-uid buf)))))) + +(defun wg-buffer-uid-or-add (buffer) + "Return BUFFER's uid. +If there isn't already a buf corresponding to BUFFER in +`wg-buf-list', make one and add it." + (or (wg-buffer-uid buffer) (wg-add-buffer-to-buf-list buffer))) + +(defun wg-bufobj-uid-or-add (bufobj) + "If BUFOBJ is a wg-buf, return its uid. +If BUFOBJ is a buffer or a buffer name, see `wg-buffer-uid-or-add'." + (cl-etypecase bufobj + (wg-buf (wg-buf-uid bufobj)) ;; possibly also add to `wg-buf-list' + (buffer (wg-buffer-uid-or-add bufobj)) + (string (wg-bufobj-uid-or-add (wg-get-buffer bufobj))))) + + +(defun wg-reset-buffer (buffer) + "Return BUFFER. +Currently only sets BUFFER's `wg-buffer-uid' to nil." + (with-current-buffer buffer (setq wg-buffer-uid nil))) + + + +;;; buffer-list-filter commands + +(defun wg-next-buffer-internal (buffer-list &optional prev noerror) + "Switch to the next buffer in Workgroups' filtered buffer list." + (when buffer-list + (let* ((cur (current-buffer)) + (next (or (wg-cyclic-nth-from-elt cur buffer-list (if prev -1 1)) + (car buffer-list)))) + (unless (eq cur next) + (switch-to-buffer next) + (unless prev (bury-buffer cur)) + next)))) + +(defun wg-next-buffer (&optional prev) + "Switch to the next buffer in Workgroups' filtered buffer list. +In the post-command message the current buffer is rotated to the +middle of the list to more easily see where `wg-previous-buffer' +will take you." + (interactive) + (let ((command (if prev 'previous-buffer 'next-buffer))) + (if (not (wg-filter-buffer-list-p)) + (call-interactively (wg-prior-mapping workgroups-mode command)) + (wg-with-buffer-list-filters command + (wg-awhen (wg-filtered-buffer-list) (wg-next-buffer-internal it prev)) + (wg-message (wg-buffer-command-display)))))) + +(defun wg-previous-buffer () + "Switch to the next buffer in Workgroups' filtered buffer list." + (interactive) + (wg-next-buffer t)) + +(defun wg-bury-buffer (&optional buffer-or-name) + "Remove BUFFER-OR-NAME from the current workgroup, bury it, +and switch to the next buffer in the buffer-list-filter." + (interactive (list (current-buffer))) + (if (not (wg-filter-buffer-list-p)) + (call-interactively (wg-prior-mapping workgroups-mode 'bury-buffer)) + (wg-with-buffer-list-filters 'bury-buffer + (wg-next-buffer-internal (wg-filtered-buffer-list)) + (bury-buffer buffer-or-name) + (wg-message (wg-buffer-command-display))))) + +(defun wg-banish-buffer (&optional buffer-or-name) + "Bury BUFFER-OR-NAME." + (interactive) + (let ((buffer (or buffer-or-name (current-buffer)))) + (wg-bury-buffer buffer))) + + +(defun wg-update-buffer-in-buf-list (&optional buffer) + "Update BUFFER's corresponding buf in `wg-buf-list'. +BUFFER nil defaults to `current-buffer'." + (let ((buffer (or buffer (current-buffer)))) + (wg-when-let ((uid (wg-buffer-uid buffer)) + (old-buf (wg-find-buf-by-uid uid)) + (new-buf (wg-buffer-to-buf buffer))) + (setf (wg-buf-uid new-buf) (wg-buf-uid old-buf)) + (wg-asetf (wg-buf-list) (cons new-buf (remove old-buf it)))))) + +(defun wg-update-buf-list (&optional buffer-list) + "Update all bufs in `wg-buf-list' corresponding to buffers in BUFFER-LIST." + (mapc 'wg-update-buffer-in-buf-list (or buffer-list (buffer-list)))) + + + + +(defun wg-buffer-list-display (buffer-list) + "Return the BUFFER-LIST display string." + (wg-display-internal + 'wg-buffer-display + (if wg-center-rotate-buffer-list-display + (wg-center-rotate-list buffer-list) buffer-list))) + +(defun wg-buffer-list-filter-display (&optional workgroup blf-id) + "Return a buffer-list-filter display string from WORKGROUP and BLF-ID." + (wg-fontify + "(" + (wg-workgroup-name (wg-get-workgroup workgroup)) + ":" + (wg-get-buffer-list-filter-val blf-id 'indicator) + ")")) + +(defun wg-buffer-list-filter-prompt (prompt &optional workgroup blf-id) + "Return a prompt string from PROMPT indicating WORKGROUP and BLF-ID." + (wg-fontify + prompt " " + (wg-buffer-list-filter-display workgroup blf-id) + ": ")) + +(defun wg-buffer-command-display (&optional buffer-list) + "Return the buffer command display string." + (concat + (wg-buffer-list-filter-display) ": " + (wg-buffer-list-display (or buffer-list (wg-filtered-buffer-list))))) + + +(defun wg-read-buffer (prompt &optional default require-match) + "Workgroups' version of `read-buffer'. +Read with PROMT DEFAULT REQUIRE-MATCH." + (if (not (wg-filter-buffer-list-p)) + (funcall (wg-read-buffer-function) prompt default require-match) + (wg-with-buffer-list-filters 'read-buffer + (funcall (wg-read-buffer-function) + (wg-buffer-list-filter-prompt + (wg-aif (string-match ": *$" prompt) + (substring prompt 0 it) prompt)) + default require-match)))) + + + +;;; filtered buffer-list construction + +(defun wg-get-buffer-list-filter-id-flexibly (blf-id) + "Return a buffer-list-filter-id one way or another." + (or blf-id wg-current-buffer-list-filter-id 'all)) + +(defun wg-get-buffer-list-filter-val (id key &optional noerror) + "Return ID's KEY's value in `wg-buffer-list-filter-definitions'. +Lots of possible errors here because +`wg-buffer-list-filter-definitions' can be modified by the user." + (let ((slot-num (cl-case key (symbol 0) (indicator 1) (constructor 2)))) + (if (not slot-num) + (unless noerror + (error "`%S' is not a valid buffer-list-filter definition slot" key)) + (let* ((id (wg-get-buffer-list-filter-id-flexibly id)) + (entry (assq id (wg-local-value + 'wg-buffer-list-filter-definitions)))) + (if (not entry) + (unless noerror + (error "`%S' is an undefined buffer-list-filter" id)) + (or (nth slot-num entry) + (unless noerror + (error "Slot `%S' is undefined in `%S's definition" + key id)))))))) + +(defun wg-filtered-buffer-list (&optional names workgroup bfl-id initial) + "Return a filtered buffer-list from NAMES, WORKGROUP, BLF-ID and INITIAL. +NAMES non-nil means return a list of buffer-names instead of buffer objects. +WORKGROUP non-nil should be any workgroup identifier accepted by +`wg-get-workgroup'. +BLF-ID non-nil should be the identifier of a defined buffer-list-filter. +It defaults to `wg-get-buffer-list-filter-val'. +INITIAL non-nil should be an initial buffer-list to filter. It defaults to +`wg-interesting-buffers'." + (let ((buffer-list (funcall (wg-get-buffer-list-filter-val + (wg-get-buffer-list-filter-id-flexibly bfl-id) + 'constructor) + (wg-get-workgroup workgroup) + (or initial (wg-interesting-buffers))))) + (if names (mapcar 'wg-buffer-name buffer-list) buffer-list))) + + +;; buffer-list filters + +(defun wg-buffer-list-filter-all (workgroup initial) + "Return all buffers in INITIAL." + initial) + +(defun wg-filter-buffer-list-by-regexp (regexp buffer-list) + "Return only those buffers in BUFFER-LIST with names matching REGEXP." + (cl-remove-if-not (lambda (bname) (string-match regexp bname)) + buffer-list :key 'buffer-name)) + +(defun wg-filter-buffer-list-by-root-dir (root-dir buffer-list) + "Return only those buffers in BUFFER-LIST visiting files undo ROOT-DIR." + (cl-remove-if-not (lambda (f) (when f (wg-file-under-root-path-p root-dir f))) + buffer-list :key 'buffer-file-name)) + +(defun wg-filter-buffer-list-by-major-mode (major-mode buffer-list) + "Return only those buffers in BUFFER-LIST in major-mode MAJOR-MODE." + (cl-remove-if-not (lambda (mm) (eq mm major-mode)) + buffer-list :key 'wg-buffer-major-mode)) + + +;; Example custom buffer-list-filters + +(defun wg-buffer-list-filter-irc (workgroup buffer-list) + "Return only those buffers in BUFFER-LIST with names starting in \"#\"." + (wg-filter-buffer-list-by-regexp "^#" buffer-list)) + +(defun wg-buffer-list-filter-home-dir (workgroup buffer-list) + "Return only those buffers in BUFFER-LIST visiting files under ~/." + (wg-filter-buffer-list-by-root-dir "~/" buffer-list)) + + + +;; buffer-list-filter context + +(defun wg-buffer-list-filter-order (command) + "Return WORKGROUP's buffer-list-filter order for COMMAND, or a default." + (let ((bfo (wg-local-value 'wg-buffer-list-filter-order-alist))) + (or (wg-aget bfo command) (wg-aget bfo 'default)))) + +(defmacro wg-prior-mapping (mode command) + "Return whatever COMMAND would call if MODE wasn't on." + `(or (let (,mode) (command-remapping ,command)) ,command)) + +(defun wg-filter-buffer-list-p () + "Return the current workgroup when buffer-list-filters are on." + (and workgroups-mode wg-buffer-list-filtration-on (wg-current-workgroup t))) + +(defmacro wg-with-buffer-list-filters (command &rest body) + "Create buffer-list filter context for COMMAND, and eval BODY. +Binds `wg-current-buffer-list-filter-id' in BODY." + (declare (indent 1)) + (wg-with-gensyms (order status) + `(let* ((wg-previous-minibuffer-contents nil) + (,order (wg-buffer-list-filter-order ,command))) + (catch 'wg-result + (while 'your-mom + (let* ((wg-current-buffer-list-filter-id (car ,order)) + (,status (catch 'wg-action (list 'done (progn ,@body))))) + (cl-case (car ,status) + (done (throw 'wg-result (cadr ,status))) + (next (setq ,order (wg-rotate-list ,order 1)) + (setq wg-previous-minibuffer-contents (cadr ,status))) + (prev (setq ,order (wg-rotate-list ,order -1)) + (setq wg-previous-minibuffer-contents + (cadr ,status)))))))))) + +(defun wg-toggle-buffer-list-filtration () + "Toggle `wg-buffer-list-filtration-on'." + (interactive) + (wg-toggle-and-message 'wg-buffer-list-filtration-on)) + + +(provide 'workgroups-buf) +;;; workgroups-buf.el ends here