aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgcv <gepardcv@gmail.com>2022-03-19 15:23:17 -0700
committergcv <gepardcv@gmail.com>2022-03-19 15:23:17 -0700
commita34466feb5fb56589672bdd5fd563f7f7bee797e (patch)
tree9bf90aafa0cf63461be3bdd6470ad790ccabe3ae
parent14cbdb5460d7ee728d397aace280702866eb880e (diff)
parente6637c4e9e193ffb924d29b6cb9acb75fa4fb87d (diff)
Merge branch 'master' of https://github.com/NicholasBHubbard/perspective-el into NicholasBHubbard-master
-rw-r--r--README.md30
-rw-r--r--perspective.el182
-rw-r--r--test/test-perspective.el35
3 files changed, 223 insertions, 24 deletions
diff --git a/README.md b/README.md
index 52ee503..6023bfc 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ please refer to it for release notes.
- [Sample Use Cases](#sample-use-cases)
- [Multiple Projects](#multiple-projects)
- [Yak Shaving](#yak-shaving)
+ - [Perspective Merging](#perspective-merging)
- [Similar Packages](#similar-packages)
- [Compatibility](#compatibility)
- [Installation](#installation)
@@ -83,6 +84,35 @@ Y, you close perspective `bugfix-Y` and return to `feature-X`.
— see documentation below.)
+### Perspective Merging
+
+Yak shaving is useful for working on projects that are largely unrelated but
+sometimes you are working on multiple projects that are very much related, to
+the point that you want to view files from both projects at the same time. This
+is where perspective merging comes in.
+
+Suppose you are working on a project that requires developing multiple auxiliary
+libraries. It may get messy to develop both the main project and all the
+libraries from the same perspective so instead you put each library in its own
+perspective so you can work on them in isolation. All of a sudden though you
+wish to see library code from the main projects perspective. Instead of
+switching back and forth between the library and main projects perspectives you
+can run `M-x persp-merge` and import the buffers from the libraries perspective.
+When you are done you can run remove the imported buffers with
+`M-x persp-unmerge`.
+
+The purpose of perspective merging is to combine the buffer lists of different
+perspectives while keeping a clear distinction of which buffers belong to which
+perspective.
+
+- You can merge together as many perspectives as you want.
+- Merging is one directional so if you merge A into B, B's buffers will not be
+available in A.
+- Merging is not transitive so if you merge A into B, then B into C, the buffers
+in A will not be available in C.
+- The merge state is saved across sessions when using [persp-state-{save,load}](#saving-sessions-to-disk).
+
+
## Similar Packages
The following Emacs packages implement comparable functionality:
diff --git a/perspective.el b/perspective.el
index 792eb49..3afb517 100644
--- a/perspective.el
+++ b/perspective.el
@@ -326,6 +326,8 @@ Run with the activated perspective active.")
(define-key perspective-map (kbd "p") 'persp-prev)
(define-key perspective-map (kbd "<left>") 'persp-prev)
(define-key perspective-map persp-mode-prefix-key 'persp-switch-last)
+(define-key perspective-map (kbd "m") 'persp-merge)
+(define-key perspective-map (kbd "C-m") 'persp-unmerge)
(define-key perspective-map (kbd "C-s") 'persp-state-save)
(define-key perspective-map (kbd "C-l") 'persp-state-load)
(define-key perspective-map (kbd "`") 'persp-switch-by-number)
@@ -1690,6 +1692,7 @@ PERSP-SET-IDO-BUFFERS)."
;; }
;; }
;; :order [...]
+;; :merge-list
;; }
;; ]
;; }
@@ -1698,14 +1701,38 @@ PERSP-SET-IDO-BUFFERS)."
files
frames)
+;; Keep around old version to maintain backwards compatibility.
(cl-defstruct persp--state-frame
persps
order)
+(cl-defstruct persp--state-frame-v2
+ persps
+ order
+ merge-list)
+
(cl-defstruct persp--state-single
buffers
windows)
+(defun persp--state-complete-v2 (state-complete)
+ "Apply this function to persp--state-complete structs to be guarenteed a
+persp--state-complete that is compatible with merge-list saving. Useful for
+maintaining backwards compatibility."
+ (let* ((state-frames (persp--state-complete-frames state-complete))
+ (state-frames-v2
+ (mapcar (lambda (state-frame)
+ (if (persp--state-frame-v2-p state-frame)
+ state-frame
+ (make-persp--state-frame-v2
+ :persps (persp--state-frame-persps state-frame)
+ :order (persp--state-frame-order state-frame)
+ :merge-list nil)))
+ state-frames)))
+ (make-persp--state-complete
+ :files (persp--state-complete-files state-complete)
+ :frames state-frames-v2)))
+
(defun persp--state-interesting-buffer-p (buffer)
(and (buffer-name buffer)
(not (string-match "^[[:space:]]*\\*" (buffer-name buffer)))
@@ -1793,9 +1820,10 @@ to the perspective's *scratch* buffer."
:buffers buffers
:windows windows)
persps-in-frame)))))
- (make-persp--state-frame
+ (make-persp--state-frame-v2
:persps persps-in-frame
- :order persp-names-in-order)))))
+ :order persp-names-in-order
+ :merge-list (frame-parameter nil 'persp-merge-list))))))
;;;###autoload
(cl-defun persp-state-save (&optional file interactive?)
@@ -1870,7 +1898,7 @@ visible in a perspective as windows, they will be saved as
FILE defaults to the value of persp-state-default-file if it is
set.
-Frames are restored, along with each frame's perspective list.
+Frames are restored, along with each frame's perspective list and merge list.
Each perspective's buffer list and window layout are also
restored."
(interactive (list
@@ -1885,10 +1913,11 @@ restored."
;; actually load
(let ((tmp-persp-name (format "%04x%04x" (random (expt 16 4)) (random (expt 16 4))))
(frame-count 0)
- (state-complete (read
- (with-temp-buffer
- (insert-file-contents file)
- (buffer-string)))))
+ (state-complete (persp--state-complete-v2
+ (read
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))))))
;; open all files in a temporary perspective to avoid polluting "main"
(persp-switch tmp-persp-name)
(cl-loop for file in (persp--state-complete-files state-complete) do
@@ -1897,23 +1926,27 @@ restored."
;; iterate over the frames
(cl-loop for frame in (persp--state-complete-frames state-complete) do
(cl-incf frame-count)
- (when (> frame-count 1)
- (make-frame-command))
- (let* ((frame-persp-table (persp--state-frame-persps frame))
- (frame-persp-order (reverse (persp--state-frame-order frame))))
- ;; iterate over the perspectives in the frame in the appropriate order
- (cl-loop for persp in frame-persp-order do
- (let ((state-single (gethash persp frame-persp-table)))
- (persp-switch persp)
- (cl-loop for buffer in (persp--state-single-buffers state-single) do
- (persp-add-buffer buffer))
- ;; XXX: split-window-horizontally is necessary for
- ;; window-state-put to succeed? Something goes haywire with root
- ;; windows without it.
- (split-window-horizontally)
- (window-state-put (persp--state-single-windows state-single)
- (frame-root-window (selected-frame))
- 'safe)))))
+ (let ((emacs-frame (if (> frame-count 1) (make-frame-command) (selected-frame)))
+ (frame-persp-table (persp--state-frame-v2-persps frame))
+ (frame-persp-order (reverse (persp--state-frame-v2-order frame)))
+ (frame-persp-merge-list (persp--state-frame-v2-merge-list frame)))
+ (with-selected-frame emacs-frame
+ ;; restore the merge list
+ (set-frame-parameter emacs-frame 'persp-merge-list frame-persp-merge-list)
+ ;; iterate over the perspectives in the frame in the appropriate order
+ (cl-loop for persp in frame-persp-order do
+ (let ((state-single (gethash persp frame-persp-table)))
+ (persp-switch persp)
+ (set-frame-parameter nil 'persp-merge-list frame-persp-merge-list)
+ (cl-loop for buffer in (persp--state-single-buffers state-single) do
+ (persp-add-buffer buffer))
+ ;; XXX: split-window-horizontally is necessary for
+ ;; window-state-put to succeed? Something goes haywire with root
+ ;; windows without it.
+ (split-window-horizontally)
+ (window-state-put (persp--state-single-windows state-single)
+ (frame-root-window emacs-frame)
+ 'safe))))))
;; cleanup
(persp-kill tmp-persp-name))
;; after hook
@@ -1922,6 +1955,107 @@ restored."
(defalias 'persp-state-restore 'persp-state-load)
+ ;;; --- perspective merging
+
+(defun persp-get-merge (base-name merged-name &optional frame)
+ "Return a merge in FRAME with :base-perspective BASE-NAME and
+:merged-perspective MERGED-NAME."
+ (cl-find-if
+ (lambda (m)
+ (and (string= base-name (plist-get m :base-perspective))
+ (string= merged-name (plist-get m :merged-perspective))))
+ (frame-parameter frame 'persp-merge-list)))
+
+(defun persp-merges-with-base (&optional name frame)
+ "Return a list of all merges in FRAME with base perspective NAME."
+ (if (null name) (setq name (persp-current-name)))
+ (cl-remove-if-not
+ (lambda (m)
+ (string= name (plist-get m :base-perspective)))
+ (frame-parameter frame 'persp-merge-list)))
+
+(defun persp-perspectives-merged-with-base (&optional name frame)
+ "Return a list of all perspectives in FRAME that are merged to NAME."
+ (if (null name) (setq name (persp-current-name)))
+ (mapcar (lambda (m) (plist-get m :merged-perspective))
+ (persp-merges-with-base name frame)))
+
+(defun persp-merge (base-persp-name to-merge-persp-name)
+ "Merge the buffer list of TO-MERGE-PERSP-NAME into the buffer list for
+BASE-PERSP-NAME."
+ (interactive
+ (list (persp-current-name)
+ (funcall persp-interactive-completion-function
+ "Perspective name: "
+ (remove (persp-current-name) (persp-names)) nil t)))
+ (cl-assert (member base-persp-name (persp-names)))
+ (cl-assert (member to-merge-persp-name (persp-names)))
+ (let* ((merge (persp-get-merge base-persp-name to-merge-persp-name))
+ (all-to-merge-persp-buffers (persp-get-buffer-names to-merge-persp-name))
+ (merged-into-to-merge-persp-buffers (cl-loop for m in (persp-merges-with-base to-merge-persp-name)
+ append (plist-get m :merged-buffers)))
+ (buffers-to-merge (delete-dups
+ (cl-remove-if
+ (lambda (buf)
+ (or (member buf merged-into-to-merge-persp-buffers)
+ (string= buf (persp-scratch-buffer to-merge-persp-name))))
+ all-to-merge-persp-buffers))))
+ (with-perspective base-persp-name
+ (if merge
+ ;; update an existing merge
+ (let ((merged-buffers (plist-get merge :merged-buffers)))
+ (dolist (buf buffers-to-merge)
+ (unless (persp-is-current-buffer (get-buffer buf))
+ (persp-add-buffer buf)
+ (push buf merged-buffers)))
+ (set-frame-parameter
+ nil
+ 'persp-merge-list
+ (cl-nsubstitute-if (list :base-perspective base-persp-name
+ :merged-perspective to-merge-persp-name
+ :merged-buffers merged-buffers)
+ (lambda (m) (equal merge m))
+ (frame-parameter nil 'persp-merge-list))))
+ ;; create a new merge
+ (let ((merged-buffers))
+ (dolist (buf buffers-to-merge)
+ (unless (persp-is-current-buffer (get-buffer buf))
+ (persp-add-buffer buf)
+ (push buf merged-buffers)))
+ (set-frame-parameter
+ nil
+ 'persp-merge-list
+ (push (list :base-perspective base-persp-name
+ :merged-perspective to-merge-persp-name
+ :merged-buffers merged-buffers)
+ (frame-parameter nil 'persp-merge-list))))))))
+
+(defun persp-unmerge (base-persp-name to-unmerge-persp-name)
+ "Unmerge the buffers from TO-UNMERGE-PERSP-NAME from BASE-PERSP-NAME that were
+were merged in from a previous call to `persp-merge'."
+ (interactive
+ (let* ((base-persp-name (persp-current-name))
+ (persps-merged-with-base (persp-perspectives-merged-with-base base-persp-name))
+ (to-unmerge-persp-name
+ (when persps-merged-with-base
+ (funcall persp-interactive-completion-function
+ "Perspective name: "
+ persps-merged-with-base nil t))))
+ (list base-persp-name to-unmerge-persp-name)))
+ (let ((merge (persp-get-merge base-persp-name to-unmerge-persp-name)))
+ (cond ((null to-unmerge-persp-name)
+ (message "No perspectives merged to \"%s\"" base-persp-name))
+ ((null merge)
+ (message "\"%s\" is not merged to \"%s\"" to-unmerge-persp-name base-persp-name))
+ (t (with-perspective base-persp-name
+ (dolist (buf (plist-get merge :merged-buffers))
+ (persp-remove-buffer buf))
+ (set-frame-parameter
+ nil
+ 'persp-merge-list
+ (remove merge (frame-parameter nil 'persp-merge-list))))))))
+
+
;;; --- ibuffer filter group code
(with-eval-after-load 'ibuffer
diff --git a/test/test-perspective.el b/test/test-perspective.el
index d5a7005..5f521de 100644
--- a/test/test-perspective.el
+++ b/test/test-perspective.el
@@ -2260,4 +2260,39 @@ persp-test-make-sample-environment."
(persp-test-check-sample-environment))
(persp-test-clean-files "A1" "A2" "A3" "B1" "B2" "B3" "B4" "state-1.el")))
+(ert-deftest merge-and-unmerge ()
+ (let ((persp-merge-list nil))
+ (unwind-protect
+ (persp-test-with-persp
+ (persp-test-with-files nil (A1 A2 B1 B2 C1 C2)
+ (with-named-persp "A"
+ (persp-add-buffer "A1")
+ (persp-add-buffer "A2")
+ (with-named-persp "B"
+ (persp-add-buffer "B1")
+ (persp-add-buffer "B2")
+ (with-named-persp "C"
+ (persp-add-buffer "C1")
+ (persp-add-buffer "C2")
+ ;; basic merging
+ (persp-merge "B" "A")
+ (should (equal (list (persp-scratch-buffer "B") "A1" "A2" "B1" "B2")
+ (sort (persp-get-buffer-names "B") #'string-lessp)))
+ ;; merges are not transitive
+ (persp-merge "C" "B")
+ (should (equal (list (persp-scratch-buffer "C") "B1" "B2" "C1" "C2")
+ (sort (persp-get-buffer-names "C") #'string-lessp)))
+ ;; basic unmerging
+ (persp-unmerge "C" "B")
+ (should (equal (list (persp-scratch-buffer "C") "C1" "C2")
+ (sort (persp-get-buffer-names "C") #'string-lessp)))
+ ;; don't unmerge buffers that were in base before the merge
+ (with-perspective "C"
+ (persp-add-buffer "A1"))
+ (persp-merge "C" "A")
+ (persp-unmerge "C" "A")
+ (should (equal (list (persp-scratch-buffer "C") "A1" "C1" "C2")
+ (sort (persp-get-buffer-names "C") #'string-lessp))))))))
+ (persp-test-clean-files "A1" "A2" "B1" "B2" "C1" "C2"))))
+
;;; test-perspective.el ends here