diff options
| author | gcv <gepardcv@gmail.com> | 2022-03-19 15:23:17 -0700 |
|---|---|---|
| committer | gcv <gepardcv@gmail.com> | 2022-03-19 15:23:17 -0700 |
| commit | a34466feb5fb56589672bdd5fd563f7f7bee797e (patch) | |
| tree | 9bf90aafa0cf63461be3bdd6470ad790ccabe3ae | |
| parent | 14cbdb5460d7ee728d397aace280702866eb880e (diff) | |
| parent | e6637c4e9e193ffb924d29b6cb9acb75fa4fb87d (diff) | |
Merge branch 'master' of https://github.com/NicholasBHubbard/perspective-el into NicholasBHubbard-master
| -rw-r--r-- | README.md | 30 | ||||
| -rw-r--r-- | perspective.el | 182 | ||||
| -rw-r--r-- | test/test-perspective.el | 35 |
3 files changed, 223 insertions, 24 deletions
@@ -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 |
