├── .gitignore ├── .mailmap ├── Makefile ├── .github └── workflows │ └── test-perspective.yml ├── MIT-LICENSE ├── CONTRIBUTING.org ├── CHANGELOG.md ├── README.md └── perspective.el /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.elc -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Natalie Weizenbaum Nathan Weizenbaum 2 | Natalie Weizenbaum Nathan Weizenbaum -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EMACS ?= emacs 2 | ELFILES := perspective.el 3 | ELCFILES = $(ELFILES:.el=.elc) 4 | 5 | all: test 6 | 7 | .PHONY: test 8 | test: 9 | $(EMACS) -nw -Q -batch -L . -l ert $(addprefix -l ,$(wildcard test/*.el)) \ 10 | --eval "(ert-run-tests-batch-and-exit)" 11 | 12 | .PHONY: compile 13 | compile: $(ELCFILES) 14 | 15 | $(ELCFILES): %.elc: %.el 16 | $(EMACS) --batch -Q -L . -f batch-byte-compile $< 17 | -------------------------------------------------------------------------------- /.github/workflows/test-perspective.yml: -------------------------------------------------------------------------------- 1 | name: Perspective tests 2 | on: [push, pull_request] 3 | jobs: 4 | test-perspective: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | emacs-version: 10 | - 30.1 11 | - 29.3 12 | - 28.2 13 | - 27.2 14 | - 26.3 15 | - 25.3 16 | - 24.4 17 | steps: 18 | - uses: purcell/setup-emacs@master 19 | with: 20 | version: ${{ matrix.emacs-version }} 21 | - uses: actions/checkout@v2 22 | - name: Emacs version 23 | run: | 24 | emacs --version 25 | - name: Run tests 26 | run: | 27 | make 28 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Natalie Weizenbaum 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.org: -------------------------------------------------------------------------------- 1 | * Contributing to Perspective 2 | 3 | Thank you for considering making a code contribution to Perspective! 4 | 5 | Please follow the following procedures before opening a pull request. 6 | 7 | 8 | ** Run automated tests 9 | 10 | Perspective has a growing handful of tests. Please run them manually before [[https://github.com/nex3/perspective-el/actions][GitHub Actions]] does. 11 | 12 | From your ~perspective-el~ repository, run the following: 13 | 14 | #+BEGIN_SRC shell 15 | EMACS=/path/to/emacs make test 16 | #+END_SRC 17 | 18 | 19 | ** Test in the MELPA sandbox 20 | 21 | Emacs makes extending it so frictionless that unexpected dependencies can creep in. A package intended for use by other people must be run in a minimal environment to verify that all its dependencies are explicit. 22 | 23 | In addition, the Emacs byte-compiler introduces some unexpected behaviors, particularly with eager macroexpansion. They are well-flagged by warnings. 24 | 25 | MELPA makes these things (relatively) easy to test. Refer to the [[https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org#test-your-recipe][Test your recipe]] section of [[https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org][MELPA’s CONTRIBUTING.org]] documentation for details, but here is a terse summary of the process: 26 | 27 | - Clone the [[https://github.com/melpa/melpa/][melpa repository]], and then change ~melpa/recipes/perspective~ to point to your local Perspective repo: 28 | 29 | #+BEGIN_SRC elisp 30 | (perspective :fetcher git :url "/home/you/code/perspective-el") 31 | #+END_SRC 32 | 33 | - From the melpa repo, run: 34 | 35 | #+BEGIN_SRC shell 36 | EMACS_COMMAND=/path/to/emacs make recipes/perspective 37 | #+END_SRC 38 | 39 | - Install Ivy and Counsel in the melpa sandbox: 40 | 41 | #+BEGIN_SRC shell 42 | EMACS_COMMAND=/path/to/emacs make sandbox INSTALL=ivy 43 | # exit emacs after this step 44 | EMACS_COMMAND=/path/to/emacs make sandbox INSTALL=counsel 45 | # exit emacs after this step 46 | #+END_SRC 47 | 48 | - Install Perspective: 49 | 50 | #+BEGIN_SRC shell 51 | EMACS_COMMAND=/path/to/emacs make sandbox INSTALL=perspective 52 | #+END_SRC 53 | 54 | *Important:* This step should produce no byte-compiler errors or warnings. 55 | 56 | *Also important:* After making changes to your ~perspective-el~ repository, commit them locally, or the melpa recipe builder will not pick them up! Rerun ~make recipes/perspective~, delete the ~perspective~ directory in ~melpa/sandbox~, and rerun ~make sandbox INSTALL=perspective~ to look at the byte-compiler output. 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. Note that 4 | Perspective was started in 2008 and this log was only added in 2021. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 7 | 8 | 9 | ## [2.20] — 2025-05-23 10 | 11 | ### Changed 12 | 13 | - Change the behavior of `persp-reactivate-buffers` so that the order of buffers follows `(buffer-list)` ([#215](https://github.com/nex3/perspective-el/pull/215)). 14 | 15 | 16 | ## [2.19] — 2024-10-30 17 | 18 | ### Fixed 19 | 20 | - `persp-ibuffer-generate-filter-groups`: load `ibuf-ext` library if needed ([#202](https://github.com/nex3/perspective-el/issues/202)). 21 | 22 | 23 | ### Added 24 | 25 | - `persp-kill-other-buffers`: kill other buffers in the current perspectives. 26 | 27 | 28 | ## [2.18] — 2022-09-20 29 | 30 | ### Added 31 | 32 | - `persp-purge-initial-persp-on-save`: support for optionally killing all buffers of the initial perspective upon calling `perps-state-save`, thus treating the initial perspective as a disposable scratch area. 33 | - `persp-add-buffer-to-frame-global`: support for special frame-specific "global" perspectives; buffers which they contain will appear in Perspective-aware buffer listings in _all_ perspectives in their containing frames 34 | - `persp-switch-to-scratch-buffer`: interactive function to switch to the current perspective's scratch buffer, creating one if missing. 35 | - `persp-forget-buffer`: disassociate buffer with perspective without the risk of killing it. This balances `persp-add-buffer`. Newly created buffers via `get-buffer-create` are rogue buffers not found in any perspective, this function allows to get back to that state. 36 | - Support for using Consult's `consult-buffer` as a Perspective-aware buffer switcher. 37 | - `persp-merge` and `persp-unmerge`: temporarily import buffers from one perspective into another. 38 | 39 | 40 | ### Changed 41 | 42 | - Avoid killing the last buffer in a perspective by default (`persp-avoid-killing-last-buffer-in-perspective`, default `t`; this was formerly the experimental feature flag `persp-feature-flag-prevent-killing-last-buffer-in-perspective` which defaulted to `nil`). 43 | - **Breaking change:** (Emacs 28+ only): no longer provide a default `persp-mode-prefix-key`. It was `C-x x` in the past, which conflicts with key bindings shipping with Emacs 28. Users of Emacs 28 must now pick their own prefix key. The default remains unchanged for users of Emacs 27 and earlier. 44 | - `persp-remove-buffer`: do not kill/remove a perspective's last left buffer. 45 | - `persp-switch-to-buffer*`: tag with `'buffer` category so Marginalia can add its annotations. 46 | - `persp-other-buffer`: rewrite so it respects ignored buffer list. 47 | 48 | 49 | ### Fixed 50 | 51 | - `persp-new`: enable `initial-major-mode` only if the scratch buffer is in `fundamental-mode`. 52 | - `persp-new`: properly substitute command keys when inserting `initial-scratch-message` into scratch buffers. 53 | - `persp-new`: do not recreate existing perspectives. This prevents from resetting perspectives to a state where in the perspective there's only the scratch buffer. 54 | - `persp-reset-windows`: set `switch-to-buffer-preserve-window-point` to `nil` before calling `delete-window`, that up to Emacs 27.2 updates `window-prev-buffers` of all windows, unless the former is turned off. 55 | - `persp-remove-buffer`: force update the `current-buffer` to the current window's buffer due to `with-selected-window` saving/restoring the `current-buffer` when executing it's BODY. This properly updates the `current-buffer` to what should be the real current buffer when burying the current buffer. 56 | - `persp-activate`: force update the `current-buffer` to the current window's buffer due to `make-persp` saving/restoring the `current-buffer` when executing it's BODY. This properly updates the `current-buffer` to what should be the real current buffer when switching to a new perspective. 57 | - `persp-add-buffer`: discard unexisting buffer as argument. 58 | - Added a workaround for potential problems caused by recursive minibuffer use. 59 | - Made activating `persp-mode` repeatedly idempotent (should fix [interactive enable-theme invocation bug](https://github.com/nex3/perspective-el/issues/185)). 60 | 61 | 62 | ## [2.17] — 2021-09-18 63 | 64 | ### Added 65 | 66 | - Improved Helm integration. `helm-buffers-list` now lists buffers in all perspectives when called with a prefix argument. It also now has actions to add to the current perspective, and to remove buffers from the current perspective. 67 | 68 | 69 | ### Changed 70 | 71 | - Rewrote Ivy / Counsel buffer switchers to make better use of the Ivy API. As a result, C-k to kill buffers directly from the switcher now works. 72 | 73 | 74 | ## [2.16] — 2021-07-31 75 | 76 | ### Added 77 | 78 | - `persp-kill-others` 79 | - Make `xref` rings perspective-specific (so popping back will not inadvertently jump to a file in a different perspective). 80 | - Support for grouping buffers by `persp-name` in ibuffer. 81 | 82 | 83 | ### Changed 84 | 85 | - Switched from now-defunct Travis CI to GitHub Actions. 86 | 87 | 88 | ### Fixed 89 | 90 | - `header-line-format` wrangling bug. 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perspective for Emacs 2 | 3 | ![Test perspective](https://github.com/nex3/perspective-el/actions/workflows/test-perspective.yml/badge.svg) 4 | 5 | The Perspective package provides multiple named workspaces (or "perspectives") 6 | in Emacs, similar to multiple desktops in window managers like Awesome and 7 | XMonad, and Spaces on the Mac. 8 | 9 | Each perspective has its own buffer list and its own window layout, along with 10 | some other isolated niceties, like the 11 | [xref](https://www.gnu.org/software/emacs/manual/html_node/emacs/Xref.html) 12 | ring. This makes it easy to work on many separate projects without getting lost 13 | in all the buffers. Switching to a perspective activates its window 14 | configuration, and when in a perspective, only its buffers are available (by 15 | default). 16 | 17 | Each Emacs frame has a distinct list of perspectives. 18 | 19 | Perspective supports saving its state to a file, so long-lived work sessions may 20 | be saved and recovered as needed. 21 | 22 | At long last this project has a 23 | [changelog](https://github.com/nex3/perspective-el/blob/master/CHANGELOG.md); 24 | please refer to it for release notes. 25 | 26 | 27 | - [Sample Use Cases](#sample-use-cases) 28 | - [Multiple Projects](#multiple-projects) 29 | - [Yak Shaving](#yak-shaving) 30 | - [Perspective Merging](#perspective-merging) 31 | - [Similar Packages](#similar-packages) 32 | - [Compatibility](#compatibility) 33 | - [Installation](#installation) 34 | - [Getting Started](#getting-started) 35 | - [Usage](#usage) 36 | - [Buffer Switching](#buffer-switching) 37 | - [Notes on `completing-read` Enhancements](#notes-on-completing-read-enhancements) 38 | - [Saving Sessions to Disk](#saving-sessions-to-disk) 39 | - [Customization](#customization) 40 | - [Some Musings on Emacs Window Layouts](#some-musings-on-emacs-window-layouts) 41 | 42 | 43 | 44 | ## Sample Use Cases 45 | 46 | ### Multiple Projects 47 | 48 | Working on multiple projects can become difficult to organize as their buffer 49 | lists mix together during a long-running Emacs session. Searching for a buffer 50 | by name works well if you know what to search for, but sometimes picking from a 51 | list is easier — in which case, keeping the list well-pruned for relevant 52 | buffers becomes an important source of efficiency in file and buffer management. 53 | Perspective helps out by letting you keep separate named buffer lists and window 54 | layouts. 55 | 56 | This use case works really well in conjunction with 57 | [Projectile](https://github.com/bbatsov/projectile). Projectile helps with 58 | buffer navigation (and other project-specific tasks) in cases when a project has 59 | a well-defined root directory. Perspective then steps in to help manage 60 | unrelated buffers: shells, REPLs, `dired` buffers visiting directories outside 61 | the project, or files relevant to the project not under the same root as the 62 | rest of the source. It also helps deal with the situation of one project with 63 | multiple source repositories where having a shared window layout or buffer list 64 | makes sense. 65 | 66 | 67 | ### [Yak Shaving](http://catb.org/jargon/html/Y/yak-shaving.html) 68 | 69 | Suppose you're developing feature X in perspective `feature-X`. This keeps you 70 | working with one set of files and windows. You then realize that this feature 71 | requires you to fix a bug in an unrelated set of files. You don't want to lose 72 | all the context you have built up for feature X, so you open a new perspective, 73 | `bugfix-Y`, letting you open new files and buffers without disturbing your work 74 | on `feature-X`. Then you are asked to urgently look into something related to 75 | development of feature Z, but again: you don't want to lose context. So you open 76 | a new perspective `feature-Z`, and fill it with a whole bunch of new files and 77 | windows — all without losing any of the context for your work on bug Y or 78 | feature X. 79 | 80 | When you finish looking at Z, you close perspective `feature-Z`, and return to 81 | `bugfix-Y`, and restore its window layout and buffer list. When you finish with 82 | Y, you close perspective `bugfix-Y` and return to `feature-X`. 83 | 84 | (Hint: this workflow works best with the `persp-sort` variable set to `'created` 85 | — see documentation below.) 86 | 87 | 88 | ### Perspective Merging 89 | 90 | Yak shaving is useful for working on projects that are largely unrelated but 91 | sometimes you are working on multiple projects that are very much related, to 92 | the point that you want to view files from both projects at the same time. This 93 | is where perspective merging comes in. 94 | 95 | Suppose you are working on a project that requires developing multiple auxiliary 96 | libraries. It may get messy to develop both the main project and all the 97 | libraries from the same perspective so instead you put each library in its own 98 | perspective so you can work on them in isolation. All of a sudden though you 99 | wish to see library code from the main projects perspective. Instead of 100 | switching back and forth between the library and main projects perspectives you 101 | can run `M-x persp-merge` and import the buffers from the libraries perspective. 102 | When you are done you can run remove the imported buffers with 103 | `M-x persp-unmerge`. 104 | 105 | The purpose of perspective merging is to combine the buffer lists of different 106 | perspectives while keeping a clear distinction of which buffers belong to which 107 | perspective. 108 | 109 | - You can merge together as many perspectives as you want. 110 | - Merging is one directional so if you merge A into B, B's buffers will not be 111 | available in A. 112 | - Merging is not transitive so if you merge A into B, then B into C, the buffers 113 | in A will not be available in C. 114 | - The merge state is saved across sessions when using [persp-state-{save,load}](#saving-sessions-to-disk). 115 | 116 | 117 | ## Similar Packages 118 | 119 | The following Emacs packages implement comparable functionality: 120 | 121 | - [persp-mode](https://github.com/Bad-ptr/persp-mode.el): A Perspective 122 | fork, which implements perspective sharing between Emacs frames. It also has a 123 | different approach to saving state and different configuration options. There 124 | has been some 125 | [interest](https://github.com/nex3/perspective-el/issues/88#issuecomment-513996542) 126 | [expressed](https://github.com/nex3/perspective-el/issues/111) in merging the 127 | two projects. _Due to conflicting function names, `persp-mode.el` and 128 | Perspective cannot be installed simultaneously._ 129 | - [Workgroups 2](https://github.com/pashinin/workgroups2): Similar to 130 | Perspective in terms of features. Its [original 131 | codebase](https://github.com/tlh/workgroups.el) seems to predate Emacs 132 | acquiring a native ability to serialize window layouts, so it has custom 133 | serialization code. 134 | - [eyebrowse](https://github.com/wasamasa/eyebrowse): Supports window layouts 135 | but not buffer lists. 136 | - [wconf](https://github.com/ilohmar/wconf): Supports window layouts but not 137 | buffer lists. 138 | - [ElScreen](https://github.com/knu/elscreen): Supports window layouts but not 139 | buffer lists; seems unmaintained. 140 | - [Burly](https://github.com/alphapapa/burly.el): An approach to persisting window and frame configurations using Emacs bookmarks. 141 | 142 | Emacs 27 includes two new buffer and window organizing features: Tab Line 143 | (`global-tab-line-mode`) and Tab Bar (`tab-bar-mode`). 144 | - Tab Line maintains a list of buffers which had been opened in a given window, 145 | and anchors it to that window. It is analogous to tabs as used in web browsers 146 | and other text editors, and therefore orthogonal to Perspective. 147 | - Tab Bar maintains window layouts (with optional names). In this, it is similar 148 | to Perspective. Unlike Perspective, it does not support buffer lists. Using 149 | Perspective and Tab Bar at the same time is not recommended at this time, 150 | since the tab list is global (i.e., will show up in all perspectives) and is 151 | likely to cause confusion. It would be an interesting future feature for 152 | Perspective to adopt the tab bar and allow keeping a distinct set of tabs 153 | per-perspective. 154 | 155 | 156 | ## Compatibility 157 | 158 | Perspective does not work with [Emacs 159 | `desktop.el`](https://www.gnu.org/software/emacs/manual/html_node/emacs/Saving-Emacs-Sessions.html). 160 | This is because Perspective state stores buffer and window information in frame 161 | parameters, and `desktop-save-mode` does not support saving those types of data. 162 | 163 | Instead, Perspective provides its own [disk save and 164 | load](#saving-sessions-to-disk) feature, which cleanly saves perspectives. 165 | 166 | 167 | ## Installation 168 | 169 | You should install Perspective from [MELPA](https://melpa.org/) or [MELPA Stable](https://stable.melpa.org/). 170 | 171 | Users of [`use-package`](https://github.com/jwiegley/use-package) can install Perspective as follows: 172 | 173 | ```elisp 174 | (use-package perspective 175 | :bind 176 | ("C-x C-b" . persp-list-buffers) ; or use a nicer switcher, see below 177 | :custom 178 | (persp-mode-prefix-key (kbd "C-c M-p")) ; pick your own prefix key here 179 | :init 180 | (persp-mode)) 181 | ``` 182 | 183 | Replace the binding for `C-x C-b`, the default Emacs buffer switcher, with one 184 | of the nicer implementations described in the [Buffer 185 | switchers](#buffer-switchers) section. 186 | 187 | If not using `use-package`, put `perspective.el` from this source repository 188 | somewhere on your load path, and use something similar to this: 189 | 190 | ```elisp 191 | (require 'perspective) 192 | (global-set-key (kbd "C-x C-b") 'persp-list-buffers) 193 | (customize-set-variable 'persp-mode-prefix-key (kbd "C-c M-p")) 194 | (persp-mode) 195 | ``` 196 | 197 | Users of Debian 9 or later or Ubuntu 16.04 or later may simply `apt-get install 198 | elpa-perspective`, though be aware that the stable version provided in these 199 | repositories is likely to be (extremely) outdated. 200 | 201 | 202 | ## Getting Started 203 | 204 | Assuming you installed Perspective using `use-package` as noted in the 205 | Installation section, `persp-mode` should now be active, and you should be in 206 | the `main` perspective. Any buffers you have open now will be associated with 207 | `main`. (If you have no buffers open, you should open a couple for the purposes 208 | of this exercise.) 209 | 210 | Now, press C-c M-p s. Note that C-c M-p is the Perspective 211 | prefix, and you had to define it in the `use-package` invocation. If you changed 212 | it to something else, use your own prefix instead! s is the prefixed 213 | binding of the `persp-switch` command. Enter the name of a different perspective 214 | (e.g. `two`). Emacs will switch to perspective `two`. The buffers you have 215 | associated with `main` will be backgrounded, and you will see the scratch buffer 216 | associated with perspective `two` (`*scratch* two`). Any buffers you open now 217 | will be associated with perspective `two`. 218 | 219 | Now, press C-c M-p s again, and type `main` when prompted. Emacs will 220 | then background perspective `two` and bring up perspective `main`, restoring its 221 | window layout and buffer list. 222 | 223 | 224 | ## Usage 225 | 226 | To activate Perspective, use `(persp-mode)`. This creates a single default 227 | `main` perpsective. 228 | 229 | > :information_source: Since the release of Emacs 28, Perspective no longer 230 | > ships with a default command prefix. Users should pick a prefix comfortable 231 | > for them. In the days of Emacs 27 and earlier, the default prefix was `C-x x`. 232 | > This conflicts with bindings built into Emacs 28. 233 | 234 | To set a prefix key for all Perspective commands, customize 235 | `persp-mode-prefix-key`. Reasonable choices include `C-x x` (for users who don't 236 | care about the Emacs buffer-related commands this shadows), `C-z` (for users who 237 | don't suspend Emacs to shell background), `C-c C-p` (for users who don't mind 238 | the conflicting keys with `org-mode` and `markdown-mode`), `C-c M-p` (for users 239 | who don't mind the slightly awkward chord), and `H-p` (for users who don't mind 240 | relying exclusively on a non-standard Hyper modifier). 241 | 242 | The actual command keys (the ones pressed after the prefix) are defined in 243 | `perspective-map`. Here are the main commands defined in `perspective-map`: 244 | 245 | - `s` — `persp-switch`: Query a perspective to switch to, or create 246 | - `` ` `` — `persp-switch-by-number`: Switch to perspective by number, or switch 247 | quickly using numbers `1, 2, 3.. 0` as prefix args; note this will probably be 248 | most useful with `persp-sort` set to `'created` 249 | - `k` — `persp-remove-buffer`: Query a buffer to remove from current perspective 250 | - `c` — `persp-kill` : Query a perspective to kill 251 | - `r` — `persp-rename`: Rename current perspective 252 | - `a` — `persp-add-buffer`: Query an open buffer to add to current perspective 253 | - `A` — `persp-set-buffer`: Add buffer to current perspective and remove it from all others 254 | - `b` - `persp-switch-to-buffer`: Like `switch-to-buffer`; includes all buffers 255 | from all perspectives; changes perspective if necessary 256 | - `i` — `persp-import`: Import a given perspective from another frame. 257 | - `n`, `` — `persp-next`: Switch to next perspective 258 | - `p`, `` — `persp-prev`: Switch to previous perspective 259 | - `m` — `persp-merge`: Temporarily merge the buffers from one perspective into another 260 | - `u` — `persp-unmerge`: Undo the effects of a `persp-merge` 261 | - `g` — `persp-add-buffer-to-frame-global`: Add buffer to a frame-specific "global" perspective 262 | - `C-s` — `persp-state-save`: Save all perspectives in all frames to a file 263 | - `C-l` — `persp-state-load`: Load all perspectives from a file 264 | 265 | 266 | ### Buffer Switching 267 | 268 | Since Perspective maintains distinct buffer lists for each perspective, it helps 269 | to use Perspective-aware methods for buffer switching. 270 | 271 | Since Emacs 27.1, the commands `previous-buffer` and `next-buffer` can be made 272 | Perspective-aware using the `switch-to-prev-buffer-skip` variable as follows: 273 | 274 | ```elisp 275 | (setq switch-to-prev-buffer-skip 276 | (lambda (win buff bury-or-kill) 277 | (not (persp-is-current-buffer buff)))) 278 | ``` 279 | 280 | When using one of the following buffer switchers, you will only be prompted for 281 | buffers in the current perspective and the frame-specific "global" shared 282 | perspective. (The `persp-add-buffer-to-frame-global` command adds a buffer to 283 | this special frame-specific perspective, whose name is determined by 284 | `persp-frame-global-perspective-name` and defaults to `GLOBAL`.) 285 | 286 | 287 | **Ido**: [Interactive Do (Ido, 288 | `ido-mode`)](https://www.gnu.org/software/emacs/manual/html_node/ido/index.html), 289 | in particular its `ido-switch-buffer` command, is automatically 290 | Perspective-aware when `persp-mode` is enabled. 291 | 292 | **list-buffers / buffer-menu**: Perspective provides wrappers for the similar 293 | [`list-buffers` and 294 | `buffer-menu`](https://www.gnu.org/software/emacs/manual/html_node/emacs/List-Buffers.html): 295 | `persp-list-buffers` and `persp-buffer-menu`. (Note that Emacs binds `C-x C-b` 296 | to `list-buffers` by default.) When these functions are called normally, they 297 | show the buffer menu filtered by the current perspective. With a prefix 298 | argument, they show the buffer menu of all the buffers in all perspectives. (The 299 | difference between `list-buffers` and `buffer-menu`: the former calls 300 | `display-buffer`, i.e., may split windows depending on `display-buffer-alist`, 301 | and the latter calls `switch-to-buffer`, i.e., flips the current window to the 302 | buffer list buffer.) 303 | 304 | **`bs.el`**: Perspective provides a wrapper for 305 | [`bs-show`](https://www.gnu.org/software/emacs/manual/html_node/emacs/Buffer-Menus.html): 306 | `persp-bs-show`. When this function is called normally, it shows a list of 307 | buffers filtered by the current perspective. With a prefix argument, it shows a 308 | list of buffers in all perspectives. 309 | 310 | **IBuffer**: Perspective provides a wrapper for 311 | [`ibuffer`](https://www.gnu.org/software/emacs/manual/html_node/emacs/Buffer-Menus.html): 312 | `persp-ibuffer`. When this function is called normally, it shows a list of 313 | buffers filtered by the current perspective. With a prefix argument, it shows a 314 | list of buffers in all perspectives. 315 | 316 | If you want to group buffers by persp-name in ibuffer buffer, use 317 | `persp-ibuffer-set-filter-groups`. Or, make it the default: 318 | ``` 319 | (add-hook 'ibuffer-hook 320 | (lambda () 321 | (persp-ibuffer-set-filter-groups) 322 | (unless (eq ibuffer-sorting-mode 'alphabetic) 323 | (ibuffer-do-sort-by-alphabetic)))) 324 | ``` 325 | 326 | **Helm**: Perspective ships with buffer-listing advice for Helm, so Helm's 327 | buffer listing code should be automatically Perspective-aware when `persp-mode` 328 | is enabled. (Older versions of Helm relied on the machinery of `ido-mode` for 329 | listing buffers, so they did not require this advice; see [`this Helm 330 | commit`](https://github.com/emacs-helm/helm/commit/f7fa3a9e0ef1f69c42e0c513d02c9f76ea9a4344) 331 | and [`this Perspective 332 | commit`](https://github.com/nex3/perspective-el/commit/c2d3542418967b55f05d5b5ba71c9fbfe4cd3d4f) 333 | for details.) If `helm-buffers-list` is called with a prefix argument, it will 334 | show buffers in all perspectives. In addition, Perspective adds actions to 335 | `helm-buffers-list` to add buffers to the current perspective (mainly relevant 336 | to the prefix-argument version) and to remove buffers from the current 337 | perspective. 338 | 339 | **Consult**: Perspective provides `persp-consult-source` source that will list 340 | buffers in current perspective. You can hide default buffer source 341 | and add `persp-consult-source` to `consult-buffer-sources` for consult 342 | to only list buffers in current perspective like so: 343 | 344 | ```emacs-lisp 345 | (consult-customize consult--source-buffer :hidden t :default nil) 346 | (add-to-list 'consult-buffer-sources persp-consult-source) 347 | ``` 348 | Note that you can still access list of all buffers in all perspectives by 349 | [narrowing](https://github.com/minad/consult#narrowing-and-grouping) 350 | using prefix `b`. 351 | 352 | **Ivy / Counsel**: Perspective provides two commands for listing buffers using 353 | Ivy and Counsel: `persp-ivy-switch-buffer` and `persp-counsel-switch-buffer`. 354 | When these functions are called normally, they show a list of buffers filtered 355 | by the current perspective. With a prefix argument, they shows a list of buffers 356 | in all perspectives. The distinction between the `ivy` and `counsel` versions is 357 | the same as between `ivy-switch-buffer` and `counsel-switch-buffer`: the latter 358 | shows a preview of the buffer to switch to, and the former does not. 359 | 360 | It is a good idea to bind one these helper functions with the `:bind` form of 361 | `use-package`. Or, if you do not use `use-package`, it can also be bound 362 | globally, e.g.: 363 | 364 | ```emacs-lisp 365 | (global-set-key (kbd "C-x C-b") (lambda (arg) 366 | (interactive "P") 367 | (if (fboundp 'persp-bs-show) 368 | (persp-bs-show arg) 369 | (bs-show "all")))) 370 | ``` 371 | 372 | 373 | ### Notes on `completing-read` Enhancements 374 | 375 | Users of a `completing-read` enhancement framework (such as Ivy, 376 | [Selectrum](https://github.com/raxod502/selectrum), or 377 | [Vertico](https://github.com/minad/vertico)) may wish to use the following two 378 | functions: 379 | - `persp-switch-to-buffer*` replaces `switch-to-buffer` 380 | - `persp-kill-buffer*` replaces `kill-buffer` 381 | 382 | Both these functions behave like the built-ins, but use `completing-read` 383 | directly. When called normally, they list buffers filtered by the current 384 | perspective. With a prefix argument, they list buffers in all perspectives. 385 | 386 | The following sample `use-package` invocation changes Emacs default key bindings 387 | to use the replacements: 388 | 389 | ``` 390 | (use-package perspective 391 | :bind (("C-x b" . persp-switch-to-buffer*) 392 | ("C-x k" . persp-kill-buffer*)) 393 | :config 394 | (persp-mode)) 395 | ``` 396 | 397 | 398 | ## Saving Sessions to Disk 399 | 400 | A pair of functions, `persp-state-save` and `persp-state-load`, implement 401 | perspective durability on disk. When called interactively, they prompt for files 402 | to save sessions to and restore from. 403 | 404 | A custom variable, `persp-state-default-file`, sets a default file to use for 405 | saving and restoring perspectives. When it is set, `persp-state-save` may be 406 | called non-interactively without an argument and it will save to the file 407 | referenced by that variable. This makes it easy to automatically save 408 | perspective sessions when Emacs exits: 409 | 410 | ``` 411 | (add-hook 'kill-emacs-hook #'persp-state-save) 412 | ``` 413 | 414 | A limitation of `persp-state-save` and `persp-state-load` is that they do not 415 | attempt to deal with non-file-visiting buffers with non-trivial state. Saving 416 | shell, REPL, and `compilation-mode` buffers is not supported. When saved to a 417 | file, any windows pointing to them are changed to point to the perspective's 418 | `*scratch*` buffer. (Live windows are, of course, left alone.) 419 | 420 | 421 | ## Customization 422 | 423 | Perspective supports several custom variables (see its section in `M-x 424 | customize`). The following are likely to be of most interest: 425 | 426 | - `persp-sort`: Select the order in which to sort perspectives when calling 427 | `persp-switch`. Defaults to `'name` (alphabetical), but `'access` (by most 428 | recently accessed) and `'created` (by order created) are available. Note that 429 | `persp-switch-by-number` is likely to be confusing when this is set to 430 | `'access`, as the numbers associated with a perspective will change all the time. 431 | - `persp-interactive-completion-function`: Used for prompting for a perspective 432 | name. `completing-read` is the default, with `ido-completing-read` enabled 433 | with `ido-mode`. `ivy-completing-read` is broadly compatible, but 434 | unfortunately sorts alphabetically and therefore breaks the `persp-sort` 435 | setting. Helm, unfortunately, does not have a `completing-read` compatible 436 | implementation out of the box (`helm-completing-read-default-1` purports to be 437 | this but does not have the same `&optional` defaults). _`ido-completing-read` 438 | is the recommended setting here unless a `completing-read` enhancement 439 | framework is used._ 440 | - `persp-mode-prefix-key`: Changes the default key prefix for Perspective 441 | commands. 442 | - `persp-state-default-file`: Changes the default file to use for saving and 443 | loading Perspective state. 444 | - `persp-show-modestring`: Determines if Perspective should show its status in 445 | the modeline. It defaults to `t`, but can also be `nil` (turning off the 446 | modeline status display) or `'header` (which uses the header line instead of 447 | the modeline). 448 | - `persp-modestring-short`: When set to `t`, show a shortened modeline string 449 | with only the current perspective instead of the full list. Defaults to `nil`. 450 | - `persp-purge-initial-persp-on-save`: When set to `t`, will kill all buffers 451 | of the initial perspective upon calling `perps-state-save`. The buffers whose name 452 | match a regexp in the list `persp-purge-initial-persp-on-save-exceptions` won't 453 | get killed. This allows using the initial perspective as a kind of scratch space. 454 | 455 | To change keys used after the prefix key, with `use-package` you can do: 456 | 457 | ;; remap n to N to switch to next perspective 458 | (use-package perspective 459 | :bind ( 460 | :map perspective-map 461 | ("n" . nil) 462 | ("N" . persp-next))) 463 | 464 | Or without `use-package`: 465 | 466 | (define-key perspective-map (kbd "n") nil) 467 | (define-key perspective-map (kbd "N") 'persp-next) 468 | 469 | 470 | ## Some Musings on Emacs Window Layouts 471 | 472 | The following discussion exceeds the needs of documenting Perspective, but it 473 | falls in the category of helping users learn to manage Emacs sessions, and 474 | therefore will likely help potential users of Perspective make the experience 475 | smoother. 476 | 477 | Emacs has bad default behavior when it comes to window handling: many commands 478 | and modes have a habit of splitting existing windows and changing the user's 479 | carefully thought-out window layout. This tends to be a more serious problem for 480 | people who run Emacs on large displays (possibly in full-screen mode): the 481 | greater amount of screen real estate makes it easy to split the frame into many 482 | smaller windows, making any unexpected alterations more disruptive. 483 | 484 | As a result of indiscriminate-seeming window splits and buffer switching in 485 | existing windows, new Emacs users can get into the habit of expecting Emacs and 486 | its packages to lack basic respect for their layouts. Hence the popularity of 487 | things like `winner-mode`, and packages like 488 | [shackle](https://github.com/wasamasa/shackle). 489 | 490 | This may make the value of Perspective seem questionable: why bother with 491 | carefully preserving window layouts if Emacs just throws them away on a `M-x 492 | compile`? The answer is to fix the broken defaults. This is fairly easy: 493 | 494 | ```emacs-lisp 495 | (customize-set-variable 'display-buffer-base-action 496 | '((display-buffer-reuse-window display-buffer-same-window) 497 | (reusable-frames . t))) 498 | 499 | (customize-set-variable 'even-window-sizes nil) ; avoid resizing 500 | ``` 501 | 502 | These settings do the following: 503 | 504 | 1. Tell `display-buffer` to reuse existing windows as much as possible, 505 | including in other frames. For example, if there is already a `*compilation*` 506 | buffer in a visible window, switch to that window. This means that Emacs will 507 | usually switch windows in a "do what I mean" manner for a warmed-up workflow 508 | (one with, say, a couple of source windows, a compilation output window, and 509 | a Magit window). 510 | 2. Prevent splits by telling `display-buffer` to switch to the target buffer in 511 | the _current_ window. For example, if there is no `*compilation*` buffer 512 | visible, then the buffer in whichever window was current when `compile` was 513 | run will be replaced with `*compilation*`. This may seem intrusive, since it 514 | changes out the current buffer, but keep in mind that most buffers popped up 515 | in this manner are easy to dismiss, either with a dedicated keybinding (often 516 | `q`) or the universally-applicable `kill-buffer`. This is easier than 517 | restoring window arrangements. It is also easier to handle for pre-arranged 518 | window layouts, since the appropriate command can simply be run in a window 519 | prepared for it in advance. (If this is a step too far, then replace 520 | `display-buffer-same-window` with `display-buffer-pop-up-window`.) 521 | 522 | (An earlier version of this hint modified `display-buffer-alist` instead of 523 | `display-buffer-base-action`. This was [too 524 | aggressive](https://debbugs.gnu.org/cgi/bugreport.cgi?bug=49069#25) and can 525 | impact packages which may legitimately want to split windows.) 526 | 527 | [Documentation of the Emacs framework responsible for "pop-up" windows, 528 | `display-buffer`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Displaying-Buffers.html#Displaying-Buffers), 529 | is dense and difficult to read, so there have been attempts to summarize the 530 | most important bits: 531 | 532 | - https://ess.r-project.org/Manual/ess.html#Controlling-buffer-display 533 | - https://old.reddit.com/r/emacs/comments/cpdr6m/any_additional_docstutorials_on_displaybuffer_and/ews94n1/ 534 | - https://www.masteringemacs.org/article/demystifying-emacs-window-manager 535 | -------------------------------------------------------------------------------- /perspective.el: -------------------------------------------------------------------------------- 1 | ;;; perspective.el --- switch between named "perspectives" of the editor -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2008-2020 Natalie Weizenbaum 4 | ;; 5 | ;; Licensed under the same terms as Emacs and under the MIT license. 6 | 7 | ;; Author: Natalie Weizenbaum 8 | ;; URL: http://github.com/nex3/perspective-el 9 | ;; Package-Requires: ((emacs "24.4") (cl-lib "0.5")) 10 | ;; Version: 2.20 11 | ;; Created: 2008-03-05 12 | ;; By: Natalie Weizenbaum 13 | ;; Keywords: workspace, convenience, frames 14 | 15 | ;;; Commentary: 16 | 17 | ;; This package provides tagged workspaces in Emacs, similar to 18 | ;; workspaces in windows managers such as Awesome and XMonad (and 19 | ;; somewhat similar to multiple desktops in Gnome or Spaces in OS X). 20 | 21 | ;; Perspective provides multiple workspaces (or "perspectives") for each Emacs 22 | ;; frame. This makes it easy to work on many separate projects without getting 23 | ;; lost in all the buffers. 24 | 25 | ;; Each perspective is composed of a window configuration and a set of 26 | ;; buffers. Switching to a perspective activates its window 27 | ;; configuration, and when in a perspective only its buffers are 28 | ;; available by default. 29 | 30 | ;;; Code: 31 | 32 | (require 'cl-lib) 33 | (require 'ido) 34 | (require 'rx) 35 | (require 'subr-x) 36 | (require 'thingatpt) 37 | 38 | 39 | ;;; --- customization 40 | 41 | (defgroup perspective-mode 'nil 42 | "Customization for Perspective mode" 43 | :group 'frames) 44 | 45 | (defcustom persp-initial-frame-name "main" 46 | "Name used for the initial perspective when enabling `persp-mode'." 47 | :type 'string 48 | :group 'perspective-mode) 49 | 50 | (defcustom persp-show-modestring t 51 | "Determines if the list of perspectives is shown in the modeline. 52 | If the value is 'header, the list of perspectives is shown in the 53 | header line instead." 54 | :group 'perspective-mode 55 | :type '(choice (const :tag "Off" nil) 56 | (const :tag "Modeline" t) 57 | (const :tag "Header" header))) 58 | 59 | (defcustom persp-modestring-dividers '("[" "]" "|") 60 | "Plist of strings used to create the string shown in the modeline. 61 | First string is the start of the modestring, second is the 62 | closing of the mode string, and the last is the divider between 63 | perspectives." 64 | :group 'perspective-mode 65 | :type '(list (string :tag "Open") 66 | (string :tag "Close") 67 | (string :tag "Divider"))) 68 | 69 | (defcustom persp-modestring-short nil 70 | "When t, show a shortened modeline string. 71 | A shortened modeline string only displays the current perspective 72 | instead of the full perspective list." 73 | :group 'perspective-mode 74 | :type 'boolean) 75 | 76 | (defcustom persp-mode-prefix-key (if (version< emacs-version "28.0") (kbd "C-x x") nil) 77 | "Prefix key to activate perspective-map." 78 | :group 'perspective-mode 79 | :set (lambda (sym value) 80 | (when (and (bound-and-true-p persp-mode-map) 81 | (bound-and-true-p perspective-map)) 82 | (persp-mode-set-prefix-key value)) 83 | (set-default sym value)) 84 | :type '(choice (const :tag "None" nil) 85 | key-sequence)) 86 | 87 | (defcustom persp-interactive-completion-function 88 | (if ido-mode 'ido-completing-read 'completing-read) 89 | "Function used by Perspective to interactively complete user input." 90 | :group 'perspective-mode 91 | :type 'function) 92 | 93 | (defcustom persp-switch-wrap t 94 | "Whether `persp-next' and `persp-prev' should wrap." 95 | :group 'perspective-mode 96 | :type 'boolean) 97 | 98 | (defcustom persp-sort 'name 99 | "What order to sort perspectives. 100 | If 'name, then sort alphabetically. 101 | If 'access, then sort by last time accessed (latest first). 102 | If 'created, then sort by time created (latest first)." 103 | :group 'perspective-mode 104 | :type '(choice (const :tag "By Name" name) 105 | (const :tag "By Time Accessed" access) 106 | (const :tag "By Time Created" created))) 107 | 108 | (defcustom persp-frame-global-perspective-name "GLOBAL" 109 | "The name for a frames global perspective." 110 | :group 'perspective-mode 111 | :type 'string) 112 | 113 | (defcustom persp-frame-global-perspective-include-scratch-buffer nil 114 | "If non-nil include `persp-frame-global-perspective-name's scratch buffer to 115 | buffer switch options." 116 | :group 'perspective-mode 117 | :type 'boolean) 118 | 119 | (defcustom persp-state-default-file nil 120 | "When non-nil, it provides a default argument for `persp-state-save` and `persp-state-load` to work with. 121 | 122 | `persp-state-save` overwrites this file without prompting, which 123 | makes it easy to use in, e.g., `kill-emacs-hook` to automatically 124 | save state when exiting Emacs." 125 | :group 'perspective-mode 126 | :type 'file) 127 | 128 | (defcustom persp-suppress-no-prefix-key-warning nil 129 | "When non-nil, do not warn the user about `persp-mode-prefix-key' not being set." 130 | :group 'perspective-mode 131 | :type 'boolean) 132 | 133 | (defcustom persp-avoid-killing-last-buffer-in-perspective t 134 | "Avoid killing the last buffer in a perspective. 135 | 136 | This should not be set to nil unless there's a bug. This was 137 | formerly a feature flag (persp-feature-flag-prevent-killing-last-buffer-in-perspective), 138 | but it seems likely to stick around as a just-in-case for a while. It makes sense 139 | to upgrade this from an experimental feature flag to a toggle. 140 | TODO: Eventually eliminate this setting?" 141 | :group 'perspective-mode 142 | :type 'boolean) 143 | (defalias 'persp-avoid-killing-last-buffer-in-perspective 144 | 'persp-feature-flag-prevent-killing-last-buffer-in-perspective) 145 | 146 | (defcustom persp-purge-initial-persp-on-save nil 147 | "When non-nil, kills all the buffers in the initial perspective upon state save. 148 | 149 | When calling `persp-state-save`, all the buffers in the initial 150 | perspective (\"main\" by default) are killed, expect the buffers 151 | whose name match the regexes in 152 | `persp-purge-initial-persp-on-save-exceptions'." 153 | :group 'perspective-mode 154 | :type 'boolean) 155 | 156 | (defcustom persp-purge-initial-persp-on-save-exceptions nil 157 | "Buffer whose name match with any regexp of this list 158 | won't be killed upon state save if persp-purge-initial-persp-on-save is t" 159 | :group 'perspective-mode 160 | :type '(repeat regexp)) 161 | 162 | 163 | ;;; --- implementation 164 | 165 | ;;; XXX: Nasty kludge to deal with the byte compiler, eager macroexpansion, and 166 | ;;; frame parameters being already set when this file is being compiled during a 167 | ;;; package upgrade. This enumerates all frame-parameters starting with 168 | ;;; persp--*, saves them in persp--kludge-save-frame-params, and then blanks 169 | ;;; them out of the frame parameters. They will be restored in the matching 170 | ;;; eval-when-compile form at the bottom of this source file. See 171 | ;;; https://github.com/nex3/perspective-el/issues/93. 172 | (eval-when-compile 173 | (defvar persp--kludge-save-frame-params) 174 | (setq persp--kludge-save-frame-params 175 | (cl-loop for kv in (frame-parameters nil) 176 | if (string-prefix-p "persp--" (symbol-name (car kv))) 177 | collect kv)) 178 | (modify-frame-parameters 179 | nil 180 | ;; Set persp-- frame parameters to nil. The expression below creates an alist 181 | ;; where the keys are the relevant frame parameters and the values are nil. 182 | (mapcar (lambda (x) (list (car x))) persp--kludge-save-frame-params))) 183 | 184 | (defmacro persp-let-frame-parameters (bindings &rest body) 185 | "Like `let', but for frame parameters. 186 | Temporariliy set frame parameters according to BINDINGS then eval BODY. 187 | After BODY is evaluated, frame parameters are reset to their original values." 188 | (declare (indent 1)) 189 | (let ((current-frame-parameters (mapcar (lambda (binding) (cons (car binding) (frame-parameter nil (car binding)))) bindings))) 190 | `(unwind-protect 191 | (progn ,@(mapcar (lambda (binding) `(set-frame-parameter nil (quote ,(car binding)) ,(cadr binding))) bindings) 192 | ,@body) 193 | ;; Revert the frame-parameters 194 | (modify-frame-parameters nil (quote ,current-frame-parameters))))) 195 | 196 | (cl-defstruct (perspective 197 | (:conc-name persp-) 198 | (:constructor make-persp-internal)) 199 | name buffers killed local-variables 200 | (last-switch-time (current-time)) 201 | (created-time (current-time)) 202 | (window-configuration (current-window-configuration)) 203 | (point-marker (point-marker))) 204 | 205 | (defmacro with-current-perspective (&rest body) 206 | "Operate on BODY when we are in a perspective." 207 | (declare (indent 0)) 208 | `(when (persp-curr) 209 | ,@body)) 210 | 211 | (defmacro with-perspective (name &rest body) 212 | "Switch to the perspective given by NAME while evaluating BODY." 213 | (declare (indent 1)) 214 | (let ((old (cl-gensym))) 215 | `(progn 216 | (let ((,old (with-current-perspective (persp-current-name))) 217 | (last-persp-cache (persp-last)) 218 | (result)) 219 | (unwind-protect 220 | (progn 221 | (persp-switch ,name 'norecord) 222 | (setq result (progn ,@body))) 223 | (when ,old (persp-switch ,old 'norecord))) 224 | (set-frame-parameter nil 'persp--last last-persp-cache) 225 | result)))) 226 | 227 | (defun persp--make-ignore-buffer-rx () 228 | (defvar ido-ignore-buffers) 229 | (if ido-ignore-buffers 230 | ;; convert a list of regexps to one 231 | (rx-to-string (append (list 'or) 232 | (mapcar (lambda (rx) `(regexp ,rx)) 233 | ido-ignore-buffers))) 234 | ;; return a regex which matches nothing, and therefore should ignore nothing 235 | "$^")) 236 | 237 | ;; NOTE: This macro is used as a place for setf expressions so be careful with 238 | ;; how you modify it as you may break things in surprising ways. 239 | (defmacro persp-current-buffers () 240 | "Return a list of all buffers in the current perspective." 241 | `(persp-buffers (persp-curr))) 242 | 243 | (defun persp-current-buffers* (&optional include-global) 244 | "Same as `persp-current-buffers' but if INCLUDE-GLOBAL include buffers from 245 | the frame global perspective." 246 | (if (not include-global) 247 | (persp-current-buffers) 248 | (delete-dups 249 | (append (persp-current-buffers) 250 | (when (member persp-frame-global-perspective-name (persp-names)) 251 | (with-perspective persp-frame-global-perspective-name 252 | (if persp-frame-global-perspective-include-scratch-buffer 253 | (persp-current-buffers) 254 | (remove (persp-get-scratch-buffer) (persp-current-buffers))))))))) 255 | 256 | (defun persp-current-buffer-names (&optional include-global) 257 | "Return a list of names of all living buffers in the current perspective. 258 | Include the names of the buffers in the frame global perspective when 259 | INCLUDE-GLOBAL." 260 | (let ((ignore-rx (persp--make-ignore-buffer-rx))) 261 | (cl-loop for buf in (persp-current-buffers* include-global) 262 | if (and (buffer-live-p buf) 263 | (not (string-match-p ignore-rx (buffer-name buf)))) 264 | collect (buffer-name buf)))) 265 | 266 | (defun persp-is-current-buffer (buf &optional include-global) 267 | "Return T if BUF is in the current perspective. When INCLUDE-GLOBAL, also 268 | return T if BUF is in the frame global perspective." 269 | (memq buf (persp-current-buffers* include-global))) 270 | 271 | (defun persp-buffer-filter (buf &optional include-global) 272 | "Return F if BUF is in the current perspective. When INCLUDE-GLOBAL, also 273 | return F if BUF is in the frame global perspective. Used for filtering in buffer 274 | display modes like ibuffer." 275 | (not (persp-is-current-buffer buf include-global))) 276 | 277 | (defun persp-buffer-list-filter (bufs &optional include-global) 278 | "Return the subset of BUFS which is in the current perspective. When 279 | EXCLUDE-GLOBAL include buffers that are members of the frame global perspective." 280 | (cl-loop for buf in bufs 281 | if (persp-is-current-buffer (get-buffer buf) include-global) 282 | collect buf)) 283 | 284 | (defun persp-valid-name-p (name) 285 | "Return T if NAME is a valid perspective name." 286 | (and (not (null name)) 287 | (not (string= "" name)))) 288 | 289 | (defun persp-current-name () 290 | "Get the name of the current perspective." 291 | (persp-name (persp-curr))) 292 | 293 | (defun persp-scratch-buffer (&optional name) 294 | (let* ((current-name (persp-current-name)) 295 | (name (or name current-name)) 296 | (initial-persp (equal name persp-initial-frame-name))) 297 | (concat "*scratch*" 298 | (unless initial-persp 299 | (format " (%s)" name))))) 300 | 301 | (defun persp-get-scratch-buffer (&optional name) 302 | "Return the \"*scratch* (NAME)\" buffer. 303 | Create it if the current perspective doesn't have one yet." 304 | (let* ((scratch-buffer-name (persp-scratch-buffer name)) 305 | (scratch-buffer (get-buffer scratch-buffer-name))) 306 | ;; Do not interfere with an existing scratch buffer's status. 307 | (unless scratch-buffer 308 | (setq scratch-buffer (get-buffer-create scratch-buffer-name)) 309 | (with-current-buffer scratch-buffer 310 | (when (eq major-mode 'fundamental-mode) 311 | (funcall initial-major-mode)) 312 | (when (and (zerop (buffer-size)) 313 | initial-scratch-message) 314 | (insert (substitute-command-keys initial-scratch-message)) 315 | (set-buffer-modified-p nil)) 316 | ;; Turn flymake off to prevent the annoying error in the 317 | ;; minibuffer 318 | (when (and (require 'flymake nil t) 319 | (boundp flymake-mode)) 320 | (flymake-mode -1)))) 321 | scratch-buffer)) 322 | 323 | (defun persp-switch-to-scratch-buffer () 324 | "Switch to the current perspective's scratch buffer. 325 | Create the scratch buffer if there isn't one yet." 326 | (interactive) 327 | (switch-to-buffer (persp-get-scratch-buffer))) 328 | 329 | (defalias 'persp-killed-p 'persp-killed 330 | "Return whether the perspective CL-X has been killed.") 331 | 332 | (defvar persp-started-after-server-mode nil 333 | "XXX: A nasty workaround for a strange timing bug which occurs 334 | if the Emacs server was started before Perspective initialized. 335 | For some reason, persp-delete-frame gets called multiple times 336 | in unexpected ways. To reproduce: (0) make sure server-start is 337 | called before persp-mode is turned on and comment out the use 338 | of persp-started-after-server-mode, (1) get a session going 339 | with a main frame, (2) switch perspectives a couple of 340 | times, (3) use emacsclient -c to edit a file in a new 341 | frame, (4) C-x 5 0 to kill that frame. This will cause an 342 | unintended perspective switch in the primary frame, and mark 343 | the previous perspective as deleted. There is also a note in 344 | the *Messages* buffer. TODO: It would be good to get to the 345 | bottom of this problem, rather than just paper over it.") 346 | 347 | (defvar persp-before-switch-hook nil 348 | "A hook that's run before `persp-switch'. 349 | Run with the previous perspective as `persp-curr'.") 350 | 351 | (defvar persp-switch-hook nil 352 | "A hook that's run after `persp-switch'. 353 | Run with the newly created perspective as `persp-curr'.") 354 | 355 | (defvar persp-mode-hook nil 356 | "A hook that's run after `persp-mode' has been activated.") 357 | 358 | (defvar persp-created-hook nil 359 | "A hook that's run after a perspective has been created. 360 | Run with the newly created perspective as `persp-curr'.") 361 | 362 | (defvar persp-killed-hook nil 363 | "A hook that's run just before a perspective is destroyed. 364 | Run with the perspective to be destroyed as `persp-curr'.") 365 | 366 | (defvar persp-activated-hook nil 367 | "A hook that's run after a perspective has been activated. 368 | Run with the activated perspective active.") 369 | 370 | (defvar persp-before-rename-hook nil 371 | "A hook run immediately before renaming a perspective.") 372 | 373 | (defvar persp-after-rename-hook nil 374 | "A hook run immediately after renaming a perspective.") 375 | 376 | (defvar persp-state-before-save-hook nil 377 | "A hook run immediately before saving persp state to disk.") 378 | 379 | (defvar persp-state-after-save-hook nil 380 | "A hook run immediately after saving persp state to disk.") 381 | 382 | (defvar persp-state-before-load-hook nil 383 | "A hook run immediately before loading persp state from disk.") 384 | 385 | (defvar persp-state-after-load-hook nil 386 | "A hook run immediately after loading persp state from disk.") 387 | 388 | (defvar persp-mode-map (make-sparse-keymap) 389 | "Keymap for perspective-mode.") 390 | 391 | (defvar perspective-map nil 392 | "Sub-keymap for perspective-mode") 393 | 394 | (define-prefix-command 'perspective-map) 395 | (when persp-mode-prefix-key 396 | (define-key persp-mode-map persp-mode-prefix-key 'perspective-map)) 397 | 398 | (define-key perspective-map (kbd "s") 'persp-switch) 399 | (define-key perspective-map (kbd "k") 'persp-remove-buffer) 400 | (define-key perspective-map (kbd "c") 'persp-kill) 401 | (define-key perspective-map (kbd "r") 'persp-rename) 402 | (define-key perspective-map (kbd "a") 'persp-add-buffer) 403 | (define-key perspective-map (kbd "A") 'persp-set-buffer) 404 | (define-key perspective-map (kbd "b") 'persp-switch-to-buffer) 405 | (define-key perspective-map (kbd "B") 'persp-switch-to-scratch-buffer) 406 | (define-key perspective-map (kbd "i") 'persp-import) 407 | (define-key perspective-map (kbd "n") 'persp-next) 408 | (define-key perspective-map (kbd "") 'persp-next) 409 | (define-key perspective-map (kbd "p") 'persp-prev) 410 | (define-key perspective-map (kbd "") 'persp-prev) 411 | (define-key perspective-map (kbd "m") 'persp-merge) 412 | (define-key perspective-map (kbd "u") 'persp-unmerge) 413 | (define-key perspective-map (kbd "g") 'persp-add-buffer-to-frame-global) 414 | (define-key perspective-map (kbd "C-s") 'persp-state-save) 415 | (define-key perspective-map (kbd "C-l") 'persp-state-load) 416 | (define-key perspective-map (kbd "`") 'persp-switch-by-number) 417 | 418 | (define-key perspective-map (kbd "1") (lambda () (interactive) (persp-switch-by-number 1))) 419 | (define-key perspective-map (kbd "2") (lambda () (interactive) (persp-switch-by-number 2))) 420 | (define-key perspective-map (kbd "3") (lambda () (interactive) (persp-switch-by-number 3))) 421 | (define-key perspective-map (kbd "4") (lambda () (interactive) (persp-switch-by-number 4))) 422 | (define-key perspective-map (kbd "5") (lambda () (interactive) (persp-switch-by-number 5))) 423 | (define-key perspective-map (kbd "6") (lambda () (interactive) (persp-switch-by-number 6))) 424 | (define-key perspective-map (kbd "7") (lambda () (interactive) (persp-switch-by-number 7))) 425 | (define-key perspective-map (kbd "8") (lambda () (interactive) (persp-switch-by-number 8))) 426 | (define-key perspective-map (kbd "9") (lambda () (interactive) (persp-switch-by-number 9))) 427 | (define-key perspective-map (kbd "0") (lambda () (interactive) (persp-switch-by-number 10))) 428 | 429 | (with-eval-after-load 'which-key 430 | (declare-function which-key-add-keymap-based-replacements "which-key.el") 431 | (when (fboundp 'which-key-add-keymap-based-replacements) 432 | (which-key-add-keymap-based-replacements perspective-map 433 | "1" "switch to 1" 434 | "2" "switch to 2" 435 | "3" "switch to 3" 436 | "4" "switch to 4" 437 | "5" "switch to 5" 438 | "6" "switch to 6" 439 | "7" "switch to 7" 440 | "8" "switch to 8" 441 | "9" "switch to 9" 442 | "0" "switch to 10"))) 443 | 444 | (defun perspectives-hash (&optional frame) 445 | "Return a hash containing all perspectives in FRAME. 446 | FRAME defaults to the currently selected frame. The keys are the 447 | perspectives' names. The values are persp structs, with the 448 | fields NAME, WINDOW-CONFIGURATION, BUFFERS, KILLED, POINT-MARKER, 449 | and LOCAL-VARIABLES. 450 | 451 | NAME is the name of the perspective. 452 | 453 | WINDOW-CONFIGURATION is the configuration given by 454 | `current-window-configuration' last time the perspective was 455 | saved (if this isn't the current perspective, this is when the 456 | perspective was last active). 457 | 458 | BUFFERS is a list of buffer objects that are associated with this 459 | perspective. 460 | 461 | KILLED is non-nil if the perspective has been killed. 462 | 463 | POINT-MARKER is the point position in the active buffer. 464 | Otherwise, when multiple windows are visiting the same buffer, 465 | all but one of their points will be overwritten. 466 | 467 | LOCAL-VARIABLES is an alist from variable names to their 468 | perspective-local values." 469 | ;; XXX: This must return a non-nil value to avoid breaking frames initialized 470 | ;; with after-make-frame-functions bound to nil. 471 | (or (frame-parameter frame 'persp--hash) 472 | (make-hash-table))) 473 | 474 | (defun persp-mode-guard () 475 | (unless (bound-and-true-p persp-mode) 476 | (persp-error "persp-mode is not active"))) 477 | 478 | (defun persp-curr (&optional frame) 479 | "Get the current perspective in FRAME. 480 | FRAME defaults to the currently selected frame." 481 | ;; XXX: This must return a non-nil value to avoid breaking frames initialized 482 | ;; with after-make-frame-functions bound to nil. 483 | (persp-mode-guard) 484 | (or (frame-parameter frame 'persp--curr) 485 | (make-persp-internal))) 486 | 487 | (defun persp-last (&optional frame) 488 | "Get the last active perspective in FRAME. 489 | FRAME defaults to the currently selected frame." 490 | ;; XXX: Unlike persp-curr, it is unsafe to return a default value of 491 | ;; (make-persp-internal) here, since some code assumes (persp-last) can return 492 | ;; nil. 493 | (frame-parameter frame 'persp--last)) 494 | 495 | (defun persp-mode-set-prefix-key (newkey) 496 | "Set NEWKEY as the prefix key to activate persp-mode." 497 | (substitute-key-definition 'perspective-map nil persp-mode-map) 498 | (when newkey 499 | (define-key persp-mode-map newkey 'perspective-map))) 500 | 501 | (defvar persp-protected nil 502 | "Whether a perspective error should cause persp-mode to be disabled. 503 | Dynamically bound by `persp-protect'.") 504 | 505 | (defface persp-selected-face 506 | '((t (:weight bold :foreground "Blue"))) 507 | "The face used to highlight the current perspective on the modeline.") 508 | 509 | (defmacro persp-protect (&rest body) 510 | "Wrap BODY to disable persp-mode when it errors out. 511 | This prevents the persp-mode from completely breaking Emacs." 512 | (declare (indent 0)) 513 | (let ((persp-protected t)) 514 | `(condition-case err 515 | (progn ,@body) 516 | (persp-error 517 | (message "Fatal persp-mode error: %S" err) 518 | (persp-mode -1))))) 519 | 520 | (defun persp-error (&rest args) 521 | "Like `error', but mark it as a persp-specific error. 522 | Used along with `persp-protect' to ensure that persp-mode doesn't 523 | bring down Emacs. 524 | 525 | ARGS will be interpreted by `format-message'." 526 | (if persp-protected 527 | (signal 'persp-error (list (apply 'format args))) 528 | (apply 'error args))) 529 | 530 | (defun check-persp (persp) 531 | "Raise an error if PERSP has been killed." 532 | (cond 533 | ((not persp) 534 | (persp-error "Expected perspective, was nil")) 535 | ((persp-killed-p persp) 536 | (persp-error "Using killed perspective `%s'" (persp-name persp))))) 537 | 538 | (defmacro make-persp (&rest args) 539 | "Create a new perspective struct and put it in `perspectives-hash'. 540 | 541 | ARGS is a list of keyword arguments followed by an optional BODY. 542 | The keyword arguments set the fields of the perspective struct. 543 | If BODY is given, it is executed to set the window configuration 544 | for the perspective. 545 | 546 | Save point, and current buffer before executing BODY, and then 547 | restore them after. If the current buffer is changed in BODY, 548 | that change is lost when getting out, hence the current buffer 549 | will need to be changed again after calling `make-persp'." 550 | (declare (indent defun)) 551 | (let ((keywords)) 552 | (while (keywordp (car args)) 553 | (dotimes (_ 2) (push (pop args) keywords))) 554 | (setq keywords (reverse keywords)) 555 | `(let ((persp (make-persp-internal ,@keywords))) 556 | (with-current-perspective 557 | (setf (persp-local-variables persp) (persp-local-variables (persp-curr)))) 558 | (puthash (persp-name persp) persp (perspectives-hash)) 559 | (with-perspective (persp-name persp) 560 | ,(when args 561 | ;; Body form given 562 | `(save-excursion ,@args)) 563 | ;; If the `current-buffer' changes while in `save-excursion', 564 | ;; that change isn't kept when getting out, since the current 565 | ;; buffer is saved before executing BODY and restored after. 566 | (run-hooks 'persp-created-hook)) 567 | persp))) 568 | 569 | (defun persp-save () 570 | "Save the current perspective state. 571 | Specifically, save the current window configuration and 572 | perspective-local variables to `persp-curr'" 573 | (with-current-perspective 574 | (setf (persp-local-variables (persp-curr)) 575 | (mapcar 576 | (lambda (c) 577 | (let ((name (car c))) 578 | (list name (symbol-value name)))) 579 | (persp-local-variables (persp-curr)))) 580 | (setf (persp-window-configuration (persp-curr)) (current-window-configuration)) 581 | (setf (persp-point-marker (persp-curr)) (point-marker)))) 582 | 583 | (defun persp-names () 584 | "Return a list of the names of all perspectives on the `selected-frame'. 585 | 586 | If `persp-sort' is 'name (the default), then return them sorted 587 | alphabetically. If `persp-sort' is 'access, then return them 588 | sorted by the last time the perspective was switched to, the 589 | current perspective being the first. If `persp-sort' is 'created, 590 | then return them in the order they were created, with the newest 591 | first." 592 | (let ((persps (hash-table-values (perspectives-hash)))) 593 | (cond ((eq persp-sort 'name) 594 | (sort (mapcar 'persp-name persps) 'string<)) 595 | ((eq persp-sort 'access) 596 | (mapcar 'persp-name 597 | (sort persps (lambda (a b) 598 | (time-less-p (persp-last-switch-time b) 599 | (persp-last-switch-time a)))))) 600 | ((eq persp-sort 'created) 601 | (mapcar 'persp-name 602 | (sort persps (lambda (a b) 603 | (time-less-p (persp-created-time b) 604 | (persp-created-time a))))))))) 605 | 606 | (defun persp-all-names (&optional not-frame) 607 | "Return a list of the perspective names for all frames. 608 | Excludes NOT-FRAME, if given." 609 | (cl-reduce 'cl-union 610 | (mapcar 611 | (lambda (frame) 612 | (unless (equal frame not-frame) 613 | (with-selected-frame frame (persp-names)))) 614 | (frame-list)))) 615 | 616 | (defun persp-prompt (&optional default require-match) 617 | "Prompt for the name of a perspective. 618 | 619 | DEFAULT is a default value for the prompt. 620 | 621 | REQUIRE-MATCH can take the same values as in `completing-read'." 622 | (funcall persp-interactive-completion-function 623 | (concat "Perspective name" 624 | (if default (concat " (default " default ")") "") 625 | ": ") 626 | (persp-names) 627 | nil require-match nil nil default)) 628 | 629 | (defun persp-reset-windows () 630 | "Remove all windows, ensure the remaining one has no window parameters. 631 | This prevents the propagation of reserved window parameters like 632 | window-side creating perspectives." 633 | (let ((ignore-window-parameters t) 634 | ;; Required up to Emacs 27.2 to prevent `delete-window' from 635 | ;; updating `window-prev-buffers' for all windows. Allowing 636 | ;; to create a fresh window (aka `split-window'), with empty 637 | ;; `window-prev-buffers'. If the latter is not empty, other 638 | ;; perspectives may pull in buffers of the current one, as a 639 | ;; side effect when `persp-reactivate-buffers' is called and 640 | ;; the perspective is then switched. 641 | (switch-to-buffer-preserve-window-point nil)) 642 | (delete-other-windows 643 | ;; XXX: Ugly workaround for problems related to 644 | ;; https://github.com/nex3/perspective-el/issues/163 and 645 | ;; https://github.com/nex3/perspective-el/issues/167 646 | (when (eq (minibuffer-window) (selected-window)) 647 | (previous-window (minibuffer-window)))) 648 | (when (ignore-errors 649 | ;; Create a fresh window without any window parameters, the 650 | ;; selected window is still in a window that may have window 651 | ;; parameters we don't want. 652 | (split-window)) 653 | ;; Delete the selected window so that the only window left has no window 654 | ;; parameters. 655 | (delete-window)))) 656 | 657 | (defun persp-new (name) 658 | "Return a perspective named NAME, or create a new one if missing. 659 | The new perspective will start with only an `initial-major-mode' 660 | buffer called \"*scratch* (NAME)\"." 661 | (or (gethash name (perspectives-hash)) 662 | (make-persp :name name 663 | (switch-to-buffer (persp-get-scratch-buffer name)) 664 | (persp-reset-windows)))) 665 | 666 | (defun persp-reactivate-buffers (buffers) 667 | "Raise BUFFERS to the top of the most-recently-selected list. 668 | Returns BUFFERS with all non-living buffers removed. 669 | 670 | See also `other-buffer'." 671 | (cl-loop for buf in (reverse (buffer-list)) 672 | when (and (buffer-live-p buf) (member buf buffers)) 673 | collect buf into result-buffers 674 | and do (switch-to-buffer buf) 675 | finally return result-buffers)) 676 | 677 | (defun persp-set-local-variables (vars) 678 | "Set the local variables given in VARS. 679 | VARS should be an alist of variable names to values." 680 | (dolist (var vars) (apply 'set var))) 681 | 682 | (defun persp-intersperse (list interspersed-val) 683 | "Intersperse a value into a list. 684 | Return a new list made from taking LIST and inserting 685 | INTERSPERSED-VAL between every pair of items. 686 | 687 | For example, (persp-intersperse '(1 2 3) 'a) gives '(1 a 2 a 3)." 688 | (reverse 689 | (cl-reduce 690 | (lambda (list el) (if list (cl-list* el interspersed-val list) (list el))) 691 | list :initial-value nil))) 692 | 693 | (defconst persp-mode-line-map 694 | (let ((map (make-sparse-keymap))) 695 | (define-key map [mode-line down-mouse-1] 'persp-mode-line-click) 696 | map)) 697 | 698 | (defconst persp-header-line-map 699 | (let ((map (make-sparse-keymap))) 700 | (define-key map [header-line down-mouse-1] 'persp-mode-line-click) 701 | map)) 702 | 703 | (defun persp-mode-line-click (event) 704 | "Select the clicked perspective. 705 | EVENT is the click event triggering this function call." 706 | (interactive "e") 707 | (persp-switch (format "%s" (car (posn-string (event-start event))))) 708 | ;; XXX: Force update of modestring because otherwise it's inconsistent with 709 | ;; the order of perspectives maintained by persp-sort. The call to 710 | ;; persp-update-modestring inside persp-switch happens too early. 711 | (persp-update-modestring)) 712 | 713 | (defun persp-mode-line () 714 | "Return the string displayed in the modeline representing the perspectives." 715 | (frame-parameter nil 'persp--modestring)) 716 | 717 | (defun persp-update-modestring () 718 | "Update the string to reflect the current perspectives. 719 | Has no effect when `persp-show-modestring' is nil." 720 | (when persp-show-modestring 721 | (let ((open (list (nth 0 persp-modestring-dividers))) 722 | (close (list (nth 1 persp-modestring-dividers))) 723 | (sep (nth 2 persp-modestring-dividers))) 724 | (set-frame-parameter nil 'persp--modestring 725 | (append open 726 | (if persp-modestring-short 727 | (list (persp-current-name)) 728 | (persp-intersperse (mapcar 'persp-format-name 729 | (persp-names)) sep)) 730 | close))))) 731 | 732 | (defun persp-format-name (name) 733 | "Format the perspective name given by NAME for display in the mode line or header line." 734 | (let ((string-name (format "%s" name))) 735 | (if (equal name (persp-current-name)) 736 | (propertize string-name 'face 'persp-selected-face) 737 | (cond ((eq persp-show-modestring 'header) 738 | (propertize string-name 739 | 'local-map persp-header-line-map 740 | 'mouse-face 'header-line-highlight)) 741 | ((eq persp-show-modestring t) 742 | (propertize string-name 743 | 'local-map persp-mode-line-map 744 | 'mouse-face 'mode-line-highlight)))))) 745 | 746 | (defun persp-get-quick (char &optional prev) 747 | "Return the name of the first perspective that begins with CHAR. 748 | Perspectives are sorted alphabetically. 749 | 750 | PREV can be the name of a perspective. If it's passed, 751 | this will try to return the perspective alphabetically after PREV. 752 | This is used for cycling between perspectives." 753 | (persp-get-quick-helper char prev (persp-names))) 754 | 755 | (defun persp-get-quick-helper (char prev names) 756 | "Helper for `persp-get-quick' using CHAR, PREV, and NAMES." 757 | (if (null names) nil 758 | (let ((name (car names))) 759 | (cond 760 | ((and (null prev) (eq (string-to-char name) char)) name) 761 | ((equal name prev) 762 | (if (and (not (null (cdr names))) (eq (string-to-char (cadr names)) char)) 763 | (cadr names) 764 | (persp-get-quick char))) 765 | (t (persp-get-quick-helper char prev (cdr names))))))) 766 | 767 | (defun persp-switch-last () 768 | "Switch to the perspective accessed before the current one." 769 | (interactive) 770 | (unless (persp-last) 771 | (persp-error "There is no last perspective")) 772 | (persp-switch (persp-name (persp-last)))) 773 | 774 | (defun persp-switch (name &optional norecord) 775 | "Switch to the perspective given by NAME. 776 | If it doesn't exist, create a new perspective and switch to that. 777 | 778 | Switching to a perspective means that all buffers associated with 779 | that perspective are reactivated (see `persp-reactivate-buffers'), 780 | the perspective's window configuration is restored, and the 781 | perspective's local variables are set. 782 | 783 | If NORECORD is non-nil, do not update the 784 | `persp-last-switch-time' for the switched perspective." 785 | (interactive "i") 786 | (unless (persp-valid-name-p name) 787 | (setq name (persp-prompt (and (persp-last) (persp-name (persp-last)))))) 788 | (if (and (persp-curr) (equal name (persp-current-name))) name 789 | (let ((persp (persp-new name))) 790 | (set-frame-parameter nil 'persp--last (persp-curr)) 791 | (unless norecord 792 | (run-hooks 'persp-before-switch-hook)) 793 | (persp-activate persp) 794 | (when (fboundp 'persp--set-xref-marker-ring) (persp--set-xref-marker-ring)) 795 | (unless norecord 796 | (setf (persp-last-switch-time persp) (current-time)) 797 | (run-hooks 'persp-switch-hook)) 798 | name))) 799 | 800 | (defun persp-switch-by-number (num) 801 | "Switch to the perspective given by NUMBER." 802 | (interactive "NSwitch to perspective number: ") 803 | (let* ((persps (persp-names)) 804 | (max-persps (length persps))) 805 | (if (<= num max-persps) 806 | (persp-switch (nth (- num 1) persps)) 807 | (message "Perspective number %s not available, only %s exist%s" 808 | num 809 | max-persps 810 | (if (= 1 max-persps) "s" "")))) 811 | ;; XXX: Have to force the modestring to update in this case, since the call 812 | ;; inside persp-switch happens too early. Otherwise, it may be inconsistent 813 | ;; with persp-sort. 814 | (persp-update-modestring)) 815 | 816 | (defun persp-activate (persp) 817 | "Activate the perspective given by the persp struct PERSP." 818 | (check-persp persp) 819 | (persp-save) 820 | (set-frame-parameter nil 'persp--curr persp) 821 | (persp-reset-windows) 822 | (persp-set-local-variables (persp-local-variables persp)) 823 | (setf (persp-buffers persp) (persp-reactivate-buffers (persp-buffers persp))) 824 | (set-window-configuration (persp-window-configuration persp)) 825 | (when (marker-position (persp-point-marker persp)) 826 | (goto-char (persp-point-marker persp))) 827 | (persp-update-modestring) 828 | ;; force update of `current-buffer' 829 | (set-buffer (window-buffer)) 830 | (run-hooks 'persp-activated-hook)) 831 | 832 | (defun persp-switch-quick (char) 833 | "Switch to the first perspective, alphabetically, that begins with CHAR. 834 | 835 | Sets `this-command' (and thus `last-command') to (persp-switch-quick . CHAR). 836 | 837 | See `persp-switch', `persp-get-quick'." 838 | (interactive "c") 839 | (let ((persp (if (and (consp last-command) (eq (car last-command) this-command)) 840 | (persp-get-quick char (cdr last-command)) 841 | (persp-get-quick char)))) 842 | (setq this-command (cons this-command persp)) 843 | (if persp (persp-switch persp) 844 | (persp-error (concat "No perspective name begins with " (string char)))))) 845 | 846 | (defun persp-next () 847 | "Switch to next perspective (to the right)." 848 | (interactive) 849 | (let* ((names (persp-names)) 850 | (pos (cl-position (persp-current-name) names))) 851 | (cond 852 | ((null pos) (persp-find-some)) 853 | ((= pos (1- (length names))) 854 | (if persp-switch-wrap (persp-switch (nth 0 names)))) 855 | (t (persp-switch (nth (1+ pos) names)))))) 856 | 857 | (defun persp-prev () 858 | "Switch to previous perspective (to the left)." 859 | (interactive) 860 | (let* ((names (persp-names)) 861 | (pos (cl-position (persp-current-name) names))) 862 | (cond 863 | ((null pos) (persp-find-some)) 864 | ((= pos 0) 865 | (if persp-switch-wrap (persp-switch (nth (1- (length names)) names)))) 866 | (t (persp-switch (nth (1- pos) names)))))) 867 | 868 | (defun persp-find-some () 869 | "Return the name of a valid perspective. 870 | 871 | This function tries to return the \"most appropriate\" 872 | perspective to switch to. It tries: 873 | 874 | * The perspective given by `persp-last'. 875 | * The \"first\" perspective, based on the ordering of persp-names. 876 | * The main perspective. 877 | * The first existing perspective, alphabetically. 878 | 879 | If none of these perspectives can be found, this function will 880 | create a new main perspective and return \"main\"." 881 | (cond 882 | ((persp-last) (persp-name (persp-last))) 883 | ((> (length (persp-names)) 1) (car (persp-names))) 884 | ((gethash persp-initial-frame-name (perspectives-hash)) persp-initial-frame-name) 885 | ;; TODO: redundant? 886 | ((> (hash-table-count (perspectives-hash)) 0) (car (persp-names))) 887 | (t (persp-activate 888 | (make-persp :name persp-initial-frame-name :buffers (buffer-list) 889 | :window-configuration (current-window-configuration) 890 | :point-marker (point-marker))) 891 | persp-initial-frame-name))) 892 | 893 | (defun persp-add-buffer (buffer-or-name) 894 | "Associate BUFFER-OR-NAME with the current perspective. 895 | 896 | See also `persp-switch' and `persp-remove-buffer'." 897 | (interactive 898 | (list 899 | (let ((read-buffer-function nil)) 900 | (read-buffer "Add buffer to perspective: ")))) 901 | (let ((buffer (get-buffer buffer-or-name))) 902 | (if (not (buffer-live-p buffer)) 903 | (message "buffer %s doesn't exist" buffer-or-name) 904 | (unless (persp-is-current-buffer buffer) 905 | (push buffer (persp-current-buffers)))))) 906 | 907 | (defun persp-add-buffer-to-frame-global (buffer-or-name) 908 | "Associate BUFFER-OR-NAME with the frame global perspective. 909 | 910 | See also `persp-add-buffer'." 911 | (interactive 912 | (list 913 | (let ((read-buffer-function nil)) 914 | (read-buffer "Add buffer to frame global perspective: ")))) 915 | (with-perspective persp-frame-global-perspective-name 916 | (persp-add-buffer buffer-or-name))) 917 | 918 | (defun persp-set-buffer (buffer-or-name) 919 | "Associate BUFFER-OR-NAME with the current perspective and remove it from any other." 920 | (interactive 921 | (list 922 | (let ((read-buffer-function nil)) 923 | (read-buffer "Set buffer to perspective: ")))) 924 | (let ((buffer (get-buffer buffer-or-name))) 925 | (if (not (buffer-live-p buffer)) 926 | (message "buffer %s doesn't exist" buffer-or-name) 927 | (persp-add-buffer buffer) 928 | ;; Do not use the combination "while `persp-buffer-in-other-p'", 929 | ;; if the buffer is not removed from other perspectives, it will 930 | ;; go into an infinite loop. 931 | (cl-loop for other-persp in (remove (persp-current-name) (persp-all-names)) 932 | do (with-perspective other-persp 933 | (persp-forget-buffer buffer)))))) 934 | 935 | (defun persp-set-frame-global-perspective (buffer-or-name) 936 | "Associate BUFFER-OR-NAME with the frame global perspective and remove it from 937 | any other. 938 | 939 | See also `persp-set-buffer'." 940 | (list 941 | (let ((read-buffer-function nil)) 942 | (read-buffer "Set buffer to frame global perspective: "))) 943 | (with-perspective persp-frame-global-perspective-name 944 | (persp-set-buffer buffer-or-name))) 945 | 946 | (cl-defun persp-buffer-in-other-p (buffer) 947 | "Returns nil if BUFFER is only in the current perspective. 948 | Otherwise, returns (FRAME . NAME), the frame and name of another 949 | perspective that has the buffer. 950 | 951 | Prefers perspectives in the selected frame." 952 | (cl-loop for frame in (sort (frame-list) (lambda (_frame1 frame2) (eq frame2 (selected-frame)))) 953 | do (cl-loop for persp being the hash-values of (perspectives-hash frame) 954 | if (and (not (and (equal frame (selected-frame)) 955 | (equal (persp-name persp) (persp-name (persp-curr frame))))) 956 | (memq buffer (persp-buffers persp))) 957 | do (cl-return-from persp-buffer-in-other-p 958 | (cons frame (persp-name persp))))) 959 | nil) 960 | 961 | (defun persp-switch-to-buffer (buffer-or-name) 962 | "Like `switch-to-buffer', but switches to another perspective if necessary." 963 | (interactive 964 | (list 965 | (let ((read-buffer-function nil)) 966 | (read-buffer-to-switch "Switch to buffer: ")))) 967 | (let ((buffer (window-normalize-buffer-to-switch-to buffer-or-name))) 968 | (if (persp-is-current-buffer buffer) 969 | (switch-to-buffer buffer) 970 | (let ((other-persp (persp-buffer-in-other-p buffer))) 971 | (when (eq (car-safe other-persp) (selected-frame)) 972 | (persp-switch (cdr other-persp))) 973 | (switch-to-buffer buffer))))) 974 | 975 | (cl-defun persp-maybe-kill-buffer () 976 | "Don't kill a buffer if it's the only buffer in a perspective. 977 | 978 | This is the default behaviour of `kill-buffer'. Perspectives 979 | with only one buffer should keep it alive to prevent adding a 980 | buffer from another perspective, replacing the killed buffer. 981 | 982 | Will also cleanup killed buffers form each perspective's list 983 | of buffers containing the buffer to be killed. 984 | 985 | This is a hook for `kill-buffer-query-functions'. Don't call 986 | this directly, otherwise the current buffer may be removed or 987 | killed from perspectives. 988 | 989 | See also `persp-remove-buffer'." 990 | ;; List candidates where the buffer to be killed should be removed 991 | ;; instead, whom are perspectives with more than one buffer. This 992 | ;; is to allow the buffer to live for perspectives that have it as 993 | ;; their only buffer. 994 | (persp-protect 995 | (let* ((buffer (current-buffer)) 996 | (bufstr (buffer-name buffer)) 997 | candidates-for-removal candidates-for-keeping) 998 | ;; XXX: For performance reasons, always allow killing off obviously 999 | ;; temporary buffers. According to Emacs convention, these buffers' names 1000 | ;; start with a space. 1001 | (when (string-match-p (rx string-start (one-or-more blank)) bufstr) 1002 | (cl-return-from persp-maybe-kill-buffer t)) 1003 | (dolist (name (persp-names)) 1004 | (let ((buffer-names (persp-get-buffer-names name))) 1005 | (when (member bufstr buffer-names) 1006 | (if (cdr buffer-names) 1007 | (push name candidates-for-removal) 1008 | ;; We use a list for debugging purposes, a simple bool 1009 | ;; can suffice for what we are doing here. 1010 | (push name candidates-for-keeping))))) 1011 | (cond 1012 | ;; When there aren't perspectives with the buffer as the only 1013 | ;; buffer, it can be killed safely. Also cleanup killed ones 1014 | ;; found in perspectives listing the buffer to be killed. 1015 | ((not candidates-for-keeping) 1016 | ;; Switching to a perspective that isn't the current, should 1017 | ;; automatically cleanup previously killed buffers which are 1018 | ;; still in the perspective's list of buffers. Removing the 1019 | ;; buffer to be killed should also keep the list clean. 1020 | (dolist (name candidates-for-removal) 1021 | (with-perspective name 1022 | ;; remove the buffer that has to be killed from the list 1023 | (setf (persp-current-buffers) (remq buffer (persp-current-buffers))))) 1024 | t) 1025 | ;; When a perspective have the buffer as the only buffer, the 1026 | ;; buffer should not be killed, but removed from perspectives 1027 | ;; that have more than one buffer. Those perspectives should 1028 | ;; forget about the buffer. 1029 | (candidates-for-removal 1030 | (dolist (name candidates-for-removal) 1031 | (with-perspective name 1032 | (persp-forget-buffer buffer))) 1033 | nil))))) 1034 | 1035 | (defun persp-forget-buffer (buffer) 1036 | "Disassociate BUFFER with the current perspective. 1037 | If BUFFER isn't in any perspective, then it is in limbo. 1038 | 1039 | XXX: This function is hard to understand, with a lot of 1040 | subtleties around visible buffers, and needs to get revisited. 1041 | 1042 | See also `persp-add-buffer' and `persp-remove-buffer'." 1043 | (interactive 1044 | (list (funcall persp-interactive-completion-function "Disassociate buffer with perspective: " (persp-current-buffer-names)))) 1045 | (setq buffer (when buffer (get-buffer buffer))) 1046 | (cond ((not (buffer-live-p buffer))) 1047 | ;; Do not disassociate a perspective's last left buffer or one 1048 | ;; that's not part of the current perspective. 1049 | ((or (not (persp-is-current-buffer buffer)) 1050 | (and (memq 'persp-maybe-kill-buffer kill-buffer-query-functions) 1051 | (not (remove (buffer-name buffer) (persp-current-buffer-names))))) 1052 | (setq buffer nil)) 1053 | ;; Make the buffer go away if we can see it. 1054 | ((let (buffer-in-any-window) 1055 | (walk-windows (lambda (window) 1056 | (when (eq buffer (window-buffer window)) 1057 | (setq buffer-in-any-window t) 1058 | ;; Burying the current buffer should also 1059 | ;; act as an `unrecord-window-buffer'. 1060 | (with-selected-window window (bury-buffer))))) 1061 | ;; XXX: The next expression has compatibility problems with 1062 | ;; switch-to-prev-buffer-skip. Since it seems to just error out when 1063 | ;; the buffer is visible even though it has already been buried, 1064 | ;; let's just not do that for now and see what happens. 1065 | ;;(let ((window (get-buffer-window buffer))) 1066 | ;; (when window 1067 | ;; (error "Buried buffer %s found in window %s, but it shouldn't" 1068 | ;; buffer window))) 1069 | ;; `with-selected-window' restores the `current-buffer'. 1070 | ;; If the current buffer is buried, it should not be the 1071 | ;; next current buffer. Remember to fix it later. 1072 | buffer-in-any-window)) 1073 | (t (bury-buffer buffer))) 1074 | ;; If the `current-buffer' was buried in `with-selected-window', set 1075 | ;; the real current buffer, since `with-selected-window' restored it 1076 | ;; as the next current buffer after processing its body. 1077 | (set-buffer (window-buffer)) 1078 | (setf (persp-current-buffers) (remq buffer (persp-current-buffers)))) 1079 | 1080 | (defun persp-forget-frame-global-buffer (buffer) 1081 | "Disassociate BUFFER from the frame global perspective. 1082 | If BUFFER isn't in any perspective, then it is in limbo. 1083 | 1084 | See also `persp-forget-buffer'." 1085 | (interactive 1086 | (list (funcall persp-interactive-completion-function "Disassociate buffer from frame global perspective: " 1087 | (with-perspective persp-frame-global-perspective-name 1088 | (persp-current-buffer-names))))) 1089 | (with-perspective persp-frame-global-perspective-name 1090 | (persp-forget-buffer buffer))) 1091 | 1092 | (defun persp-remove-buffer (buffer) 1093 | "Remove BUFFER from the current perspective. 1094 | Kill BUFFER if it falls into limbo (not in any perspective). 1095 | 1096 | To disassociate BUFFER without the chance of killing it, see 1097 | `persp-forget-buffer'. 1098 | 1099 | See also `persp-switch' and `persp-add-buffer'." 1100 | (interactive 1101 | (list (funcall persp-interactive-completion-function "Remove buffer from perspective: " (persp-current-buffer-names)))) 1102 | (setq buffer (when buffer (get-buffer buffer))) 1103 | (cond ((not (buffer-live-p buffer))) 1104 | ;; Do not kill or remove a buffer if the perspective will then 1105 | ;; switch to the buffer of another perspective. It may happen 1106 | ;; when the buffer is the perspective's last left buffer or if 1107 | ;; the next candidate is a perspective's special buffer. This 1108 | ;; could not be enforced when a perspective is killed. 1109 | ((and (persp-is-current-buffer buffer) 1110 | (memq 'persp-maybe-kill-buffer kill-buffer-query-functions) 1111 | (not (remove (buffer-name buffer) (persp-current-buffer-names))))) 1112 | ;; Only kill the buffer if no other perspectives are using it. 1113 | ((not (persp-buffer-in-other-p buffer)) 1114 | (kill-buffer buffer)) 1115 | ;; Make the buffer go away if we can see it. 1116 | ((persp-forget-buffer buffer)))) 1117 | 1118 | (defun persp-remove-frame-global-buffer (buffer) 1119 | "Remove BUFFER from the frame global perspective. 1120 | 1121 | See also `persp-remove-buffer'." 1122 | (interactive 1123 | (list (funcall persp-interactive-completion-function "Remove buffer from frame global perspective: " 1124 | (with-perspective persp-frame-global-perspective-name 1125 | (persp-current-buffer-names))))) 1126 | (with-perspective persp-frame-global-perspective-name 1127 | (persp-remove-buffer buffer))) 1128 | 1129 | (defun persp-kill (name) 1130 | "Kill the perspective given by NAME. 1131 | 1132 | Killing a perspective means that all buffers associated with that 1133 | perspective and no others are killed." 1134 | (interactive "i") 1135 | (if (null name) (setq name (persp-prompt (persp-current-name) t))) 1136 | (remove-hook 'kill-buffer-query-functions 'persp-maybe-kill-buffer) 1137 | (with-perspective name 1138 | (run-hooks 'persp-killed-hook) 1139 | (mapc 'persp-remove-buffer (persp-current-buffers)) 1140 | (setf (persp-killed (persp-curr)) t)) 1141 | (when persp-avoid-killing-last-buffer-in-perspective 1142 | (add-hook 'kill-buffer-query-functions 'persp-maybe-kill-buffer)) 1143 | (remhash name (perspectives-hash)) 1144 | (when (boundp 'persp--xref-marker-ring) (remhash name persp--xref-marker-ring)) 1145 | (persp-update-modestring) 1146 | (when (and (persp-last) (equal name (persp-name (persp-last)))) 1147 | (set-frame-parameter 1148 | nil 'persp--last 1149 | (let* ((persp-sort 'access) 1150 | (names (persp-names)) 1151 | (last (nth 1 names))) 1152 | (when last 1153 | (gethash last (perspectives-hash)))))) 1154 | (when (or (not (persp-curr)) (equal name (persp-current-name))) 1155 | ;; Don't let persp-last get set to the deleted persp. 1156 | (persp-let-frame-parameters ((persp--last (persp-last))) 1157 | (persp-switch (persp-find-some))))) 1158 | 1159 | (defun persp-kill-others () 1160 | "Kill all perspectives except the current one." 1161 | (interactive) 1162 | (let ((self (persp-current-name))) 1163 | (when (yes-or-no-p (concat "Really kill all perspectives other than `" self "'? ")) 1164 | (cl-loop for p in (persp-names) 1165 | when (not (string-equal p self)) do 1166 | (persp-kill p))))) 1167 | 1168 | (defun persp-rename (name) 1169 | "Rename the current perspective to NAME." 1170 | (interactive "sNew name: ") 1171 | (unless (persp-valid-name-p name) 1172 | (persp-error "Invalid perspective name")) 1173 | (if (gethash name (perspectives-hash)) 1174 | (persp-error "Perspective `%s' already exists" name) 1175 | ;; before hook 1176 | (run-hooks 'persp-before-rename-hook) 1177 | ;; rename the perspective-specific *scratch* buffer 1178 | (let* ((old-scratch-name (persp-scratch-buffer)) 1179 | (new-scratch-name (persp-scratch-buffer name)) 1180 | (scratch-buffer (get-buffer old-scratch-name))) 1181 | (when scratch-buffer 1182 | (if (get-buffer new-scratch-name) 1183 | ;; https://github.com/nex3/perspective-el/issues/128 1184 | ;; Buffer already exists, probably on another frame. Pull it into 1185 | ;; the current perspective; they'll be shared. 1186 | (persp-add-buffer new-scratch-name) 1187 | ;; Buffer with new-scratch-name does not exist, so just rename it. 1188 | (with-current-buffer scratch-buffer 1189 | (rename-buffer new-scratch-name))))) 1190 | ;; rewire the rest of the perspective inside its data structures 1191 | (remhash (persp-current-name) (perspectives-hash)) 1192 | (puthash name (persp-curr) (perspectives-hash)) 1193 | (setf (persp-name (persp-curr)) name) 1194 | (persp-update-modestring) 1195 | ;; after hook 1196 | (run-hooks 'persp-after-rename-hook))) 1197 | 1198 | (cl-defun persp-all-get (name not-frame) 1199 | "Returns the list of buffers for a perspective named NAME from any 1200 | frame other than NOT-FRAME. 1201 | 1202 | This doesn't return the window configuration because those can't be 1203 | copied across frames." 1204 | (dolist (frame (frame-list)) 1205 | (unless (equal frame not-frame) 1206 | (with-selected-frame frame 1207 | (let ((persp (gethash name (perspectives-hash)))) 1208 | (if persp (cl-return-from persp-all-get (persp-buffers persp)))))))) 1209 | 1210 | (defun persp-get-buffers (&optional persp-or-name frame) 1211 | "Return the list of PERSP-OR-NAME buffers in FRAME. 1212 | If PERSP-OR-NAME isn't given or nil use the current perspective. 1213 | If FRAME isn't nil, fetch PERSP-OR-NAME in FRAME, otherwise stay 1214 | in the selected frame. 1215 | 1216 | Uses `persp-current-buffers' as backhand. 1217 | 1218 | See also `persp-get-buffer-names' to get only live buffers. See 1219 | `persp-all-get' to get buffers from all frames." 1220 | (let ((name (if (stringp persp-or-name) 1221 | persp-or-name 1222 | (persp-name (or persp-or-name (persp-curr))))) 1223 | buffers) 1224 | (with-selected-frame (or frame (selected-frame)) 1225 | (when (member name (persp-names)) 1226 | (with-perspective name 1227 | (setq buffers (persp-current-buffers))))) 1228 | buffers)) 1229 | 1230 | (defun persp-get-buffer-names (&optional persp-or-name frame) 1231 | "Return the list of PERSP-OR-NAME live buffers in FRAME. 1232 | If PERSP-OR-NAME isn't given or nil use the current perspective. 1233 | If FRAME isn't nil, fetch PERSP-OR-NAME in FRAME, otherwise stay 1234 | in the selected frame. 1235 | 1236 | Uses `persp-current-buffer-names' as backhand. 1237 | 1238 | See also `persp-get-buffers' to get all buffers." 1239 | (let ((name (if (stringp persp-or-name) 1240 | persp-or-name 1241 | (persp-name (or persp-or-name (persp-curr))))) 1242 | buffers) 1243 | (with-selected-frame (or frame (selected-frame)) 1244 | (when (member name (persp-names)) 1245 | (with-perspective name 1246 | (setq buffers (persp-current-buffer-names))))) 1247 | buffers)) 1248 | 1249 | (defun persp-read-buffer (prompt &optional def require-match predicate) 1250 | "A replacement for the built-in `read-buffer', meant to be used with `read-buffer-function'. 1251 | Return the name of the buffer selected, only selecting from buffers 1252 | within the current perspective. 1253 | 1254 | PROMPT, DEF, and REQUIRE-MATCH documented in `read-buffer'. 1255 | 1256 | With a prefix arg, uses the old `read-buffer' instead." 1257 | (persp-protect 1258 | (let ((read-buffer-function nil)) 1259 | (if current-prefix-arg 1260 | (read-buffer prompt def require-match predicate) 1261 | ;; Most of this is taken from `minibuffer-with-setup-hook', 1262 | ;; slightly modified because it's not a macro. 1263 | ;; The only functional difference is that the append argument 1264 | ;; to add-hook is t, so that it'll be run after the hook added 1265 | ;; by `read-buffer-to-switch'. 1266 | (let ((rb-completion-table (persp-complete-buffer)) 1267 | (persp-read-buffer-hook)) 1268 | (setq persp-read-buffer-hook 1269 | (lambda () 1270 | (remove-hook 'minibuffer-setup-hook persp-read-buffer-hook) 1271 | (setq minibuffer-completion-table rb-completion-table))) 1272 | (unwind-protect 1273 | (progn 1274 | (add-hook 'minibuffer-setup-hook persp-read-buffer-hook t) 1275 | (read-buffer prompt def require-match predicate)) 1276 | (remove-hook 'minibuffer-setup-hook persp-read-buffer-hook))))))) 1277 | 1278 | (defun persp-complete-buffer () 1279 | "Perform completion on all buffers within the current perspective." 1280 | (let ((persp-names (mapcar 'buffer-name (persp-current-buffers)))) 1281 | (apply-partially 'completion-table-with-predicate 1282 | (or minibuffer-completion-table 'internal-complete-buffer) 1283 | (lambda (name) 1284 | (member (if (consp name) (car name) name) persp-names)) 1285 | nil))) 1286 | 1287 | (cl-defun persp-import (name &optional dont-switch) 1288 | "Import a perspective named NAME from another frame. If DONT-SWITCH 1289 | is non-nil or with prefix arg, don't switch to the new perspective." 1290 | ;; TODO: Have some way of selecting which frame the perspective is imported from. 1291 | (interactive "i\nP") 1292 | (unless name 1293 | (setq name (funcall persp-interactive-completion-function 1294 | "Import perspective: " (persp-all-names (selected-frame)) nil t))) 1295 | (if (and (gethash name (perspectives-hash)) 1296 | (not (yes-or-no-p (concat "Perspective `" name "' already exits. Continue? ")))) 1297 | (cl-return-from persp-import)) 1298 | (let ((buffers (persp-all-get name (selected-frame))) 1299 | persp) 1300 | (if (null buffers) 1301 | (persp-error "Perspective `%s' doesn't exist in another frame" name)) 1302 | (setq persp (make-persp :name name :buffers buffers 1303 | (switch-to-buffer (cl-loop for buffer in buffers 1304 | if (buffer-live-p buffer) 1305 | return buffer)) 1306 | (persp-reset-windows))) 1307 | (if dont-switch 1308 | (persp-update-modestring) 1309 | (persp-activate persp)))) 1310 | 1311 | (defadvice switch-to-buffer (after persp-add-buffer-adv) 1312 | "Add BUFFER to the current perspective. 1313 | 1314 | See also `persp-add-buffer'." 1315 | (persp-protect 1316 | (let ((buf (ad-get-arg 0))) 1317 | (when buf 1318 | (persp-add-buffer buf))))) 1319 | 1320 | (defadvice display-buffer (after persp-add-buffer-adv) 1321 | "Add BUFFER to the perspective for the frame on which it's displayed. 1322 | 1323 | See also `persp-add-buffer'." 1324 | (persp-protect 1325 | (when ad-return-value 1326 | (let ((buf (ad-get-arg 0)) 1327 | (frame (window-frame ad-return-value))) 1328 | (when (and buf frame) 1329 | (with-selected-frame frame 1330 | (persp-add-buffer buf))))))) 1331 | 1332 | (defadvice set-window-buffer (after persp-add-buffer-adv) 1333 | "Add BUFFER to the perspective for window's frame. 1334 | 1335 | See also `persp-add-buffer'." 1336 | (persp-protect 1337 | (let ((buf (ad-get-arg 1)) 1338 | (frame (window-frame (ad-get-arg 0)))) 1339 | (when (and buf frame) 1340 | (with-selected-frame frame 1341 | (persp-add-buffer buf)))))) 1342 | 1343 | (defadvice switch-to-prev-buffer (around persp-ensure-buffer-in-persp) 1344 | "Ensure that the selected buffer is in WINDOW's perspective." 1345 | (let* ((window (window-normalize-window window t)) 1346 | (frame (window-frame window)) 1347 | (old-buffer (window-buffer window))) 1348 | ad-do-it 1349 | (let ((buffer (window-buffer window))) 1350 | (with-selected-frame frame 1351 | (unless (persp-is-current-buffer buffer) 1352 | ;; If a buffer from outside this perspective was selected, it's because 1353 | ;; this perspective is out of buffers. For lack of any better option, we 1354 | ;; recreate the scratch buffer. 1355 | ;; 1356 | ;; If we were just in a scratch buffer, change the name slightly. 1357 | ;; Otherwise our new buffer will get deleted too. 1358 | (let ((name (persp-scratch-buffer))) 1359 | (when (and bury-or-kill (equal name (buffer-name old-buffer))) 1360 | (setq name (persp-scratch-buffer))) 1361 | (with-selected-window window 1362 | (switch-to-buffer name) 1363 | (funcall initial-major-mode)))))))) 1364 | 1365 | (defadvice recursive-edit (around persp-preserve-for-recursive-edit) 1366 | "Preserve the current perspective when entering a recursive edit." 1367 | (persp-protect 1368 | (persp-save) 1369 | (persp-let-frame-parameters ((persp--recursive (persp-curr))) 1370 | (let ((old-hash (copy-hash-table (perspectives-hash)))) 1371 | ad-do-it 1372 | ;; We want the buffer lists that were created in the recursive edit, 1373 | ;; but not the window configurations 1374 | (maphash (lambda (key new-persp) 1375 | (let ((persp (gethash key old-hash))) 1376 | (when persp 1377 | (setf (persp-buffers persp) (persp-buffers new-persp))))) 1378 | (perspectives-hash)) 1379 | (set-frame-parameter nil 'persp--hash old-hash))))) 1380 | 1381 | (defadvice exit-recursive-edit (before persp-restore-after-recursive-edit) 1382 | "Restore the old perspective when exiting a recursive edit." 1383 | (persp-protect 1384 | (if (frame-parameter nil 'persp--recursive) (persp-switch (persp-name (frame-parameter nil 'persp--recursive)))))) 1385 | 1386 | 1387 | ;;;###autoload 1388 | (define-minor-mode persp-mode 1389 | "Toggle perspective mode. 1390 | When active, keeps track of multiple 'perspectives', 1391 | named collections of buffers and window configurations." 1392 | :global t 1393 | :keymap persp-mode-map 1394 | (if persp-mode 1395 | ;; activate persp-mode, preferably in an idempotent manner: the presence 1396 | ;; of a non-nil 'persp--hash parameter in (selected-frame) should be a 1397 | ;; good proxy for whether the mode is actually active... 1398 | (unless (frame-parameter nil 'persp--hash) 1399 | (persp-protect 1400 | (when (bound-and-true-p server-process) 1401 | (setq persp-started-after-server-mode t)) 1402 | ;; TODO: Convert to nadvice, which has been available since 24.4 and is 1403 | ;; the earliest Emacs version Perspective supports. 1404 | (ad-activate 'switch-to-buffer) 1405 | (ad-activate 'display-buffer) 1406 | (ad-activate 'set-window-buffer) 1407 | (ad-activate 'switch-to-prev-buffer) 1408 | (ad-activate 'recursive-edit) 1409 | (ad-activate 'exit-recursive-edit) 1410 | (persp--helm-enable) 1411 | (add-hook 'after-make-frame-functions 'persp-init-frame) 1412 | (add-hook 'delete-frame-functions 'persp-delete-frame) 1413 | (add-hook 'ido-make-buffer-list-hook 'persp-set-ido-buffers) 1414 | (when persp-avoid-killing-last-buffer-in-perspective 1415 | (add-hook 'kill-buffer-query-functions 'persp-maybe-kill-buffer)) 1416 | (setq read-buffer-function 'persp-read-buffer) 1417 | (mapc 'persp-init-frame (frame-list)) 1418 | (setf (persp-current-buffers) (buffer-list)) 1419 | (unless (or persp-mode-prefix-key persp-suppress-no-prefix-key-warning) 1420 | (display-warning 1421 | 'perspective 1422 | (format-message "persp-mode-prefix-key is not set! If you see this warning, you are using Emacs 28 or later, and have not customized persp-mode-prefix-key. Please refer to the Perspective documentation for further information (https://github.com/nex3/perspective-el). To suppress this warning without choosing a prefix key, set persp-suppress-no-prefix-key-warning to `t'.") 1423 | :warning)) 1424 | (run-hooks 'persp-mode-hook))) 1425 | ;; deactivate persp-mode 1426 | (persp--helm-disable) 1427 | (ad-deactivate-regexp "^persp-.*") 1428 | (remove-hook 'delete-frame-functions 'persp-delete-frame) 1429 | (remove-hook 'after-make-frame-functions 'persp-init-frame) 1430 | (remove-hook 'ido-make-buffer-list-hook 'persp-set-ido-buffers) 1431 | (remove-hook 'kill-buffer-query-functions 'persp-maybe-kill-buffer) 1432 | (setq read-buffer-function nil) 1433 | (set-frame-parameter nil 'persp--hash nil) 1434 | (setq global-mode-string (delete '(:eval (persp-mode-line)) global-mode-string)) 1435 | (let ((default-header-line-format (default-value 'header-line-format))) 1436 | (set-default 'header-line-format (delete '(:eval (persp-mode-line)) default-header-line-format)) 1437 | (unless (delete "" default-header-line-format) 1438 | ;; need to set header-line-format to nil to completely remove the header from the buffer 1439 | (set-default 'header-line-format nil))))) 1440 | 1441 | (defun persp-init-frame (frame) 1442 | "Initialize the perspectives system in FRAME. 1443 | By default, this uses the current frame." 1444 | (with-selected-frame frame 1445 | (modify-frame-parameters 1446 | frame 1447 | '((persp--hash) (persp--curr) (persp--last) (persp--recursive) (persp--modestring))) 1448 | ;; Don't set these variables in modify-frame-parameters 1449 | ;; because that won't do anything if they've already been accessed 1450 | (set-frame-parameter frame 'persp--hash (make-hash-table :test 'equal :size 10)) 1451 | (when persp-show-modestring 1452 | (if (eq persp-show-modestring 'header) 1453 | (let ((val (or (default-value 'header-line-format) '("")))) 1454 | (unless (member '(:eval (persp-mode-line)) val) 1455 | (set-default 'header-line-format (append val '((:eval (persp-mode-line))))))) 1456 | (setq global-mode-string (or global-mode-string '(""))) 1457 | (unless (member '(:eval (persp-mode-line)) global-mode-string) 1458 | (setq global-mode-string (append global-mode-string '((:eval (persp-mode-line))))))) 1459 | (persp-update-modestring)) 1460 | ;; A frame must open with a reasonable initial buffer in its main 1461 | ;; perspective. This behaves differently from an emacsclient invocation, but 1462 | ;; should respect `initial-buffer-choice'. 1463 | (when (frame-parameter frame 'client) 1464 | (let* ((scratch-buf (persp-scratch-buffer persp-initial-frame-name)) 1465 | (init-buf (cond ((stringp initial-buffer-choice) initial-buffer-choice) 1466 | ((functionp initial-buffer-choice) (or (funcall initial-buffer-choice) 1467 | scratch-buf)) 1468 | (t scratch-buf)))) 1469 | (switch-to-buffer init-buf t))) 1470 | (persp-activate 1471 | (make-persp :name persp-initial-frame-name :buffers (list (current-buffer)) 1472 | :window-configuration (current-window-configuration) 1473 | :point-marker (point-marker))))) 1474 | 1475 | (defun persp-delete-frame (frame) 1476 | "Clean up perspectives in FRAME. 1477 | By default this uses the current frame." 1478 | (with-selected-frame frame 1479 | (unless persp-started-after-server-mode 1480 | (mapcar #'persp-kill (persp-names))))) 1481 | 1482 | (defun persp-make-variable-persp-local (variable) 1483 | "Make VARIABLE become perspective-local. 1484 | This means that whenever a new perspective is switched into, the 1485 | variable will take on its local value for that perspective. When 1486 | a new perspective is created, the variable will inherit its value 1487 | from the current perspective at time of creation." 1488 | (unless (assq variable (persp-local-variables (persp-curr))) 1489 | (let ((entry (list variable (symbol-value variable)))) 1490 | (dolist (frame (frame-list)) 1491 | (cl-loop for persp being the hash-values of (perspectives-hash frame) 1492 | do (push entry (persp-local-variables persp))))))) 1493 | 1494 | (defmacro persp-setup-for (name &rest body) 1495 | "Add code that should be run to set up the perspective named NAME. 1496 | Whenever a new perspective named NAME is created, runs BODY in 1497 | it. In addition, if one exists already, runs BODY in it immediately." 1498 | (declare (indent 1)) 1499 | `(progn 1500 | (add-hook 'persp-created-hook 1501 | (lambda () 1502 | (when (string= (persp-current-name) ,name) 1503 | ,@body)) 1504 | 'append) 1505 | (when (gethash ,name (perspectives-hash)) 1506 | (with-perspective ,name ,@body)))) 1507 | 1508 | (defun persp-set-ido-buffers () 1509 | "Restrict the ido buffer to the current perspective." 1510 | (defvar ido-temp-list) 1511 | (let ((persp-names 1512 | (remq nil (mapcar 'buffer-name (persp-current-buffers* t)))) 1513 | (indices (make-hash-table :test 'equal))) 1514 | (cl-loop for elt in ido-temp-list 1515 | for i upfrom 0 1516 | do (puthash elt i indices)) 1517 | (setq ido-temp-list 1518 | (sort (cl-intersection persp-names ido-temp-list) 1519 | (lambda (a b) 1520 | (< (gethash a indices) 1521 | (gethash b indices))))))) 1522 | 1523 | (defun quick-perspective-keys () 1524 | "Bind quick key commands to switch to perspectives. 1525 | All C-S-letter key combinations are bound to switch to the first 1526 | perspective beginning with the given letter." 1527 | (cl-loop for c from ?a to ?z 1528 | do (define-key persp-mode-map 1529 | (read-kbd-macro (concat "C-S-" (string c))) 1530 | `(lambda () 1531 | (interactive) 1532 | (persp-switch-quick ,c))))) 1533 | 1534 | (defun persp-turn-off-modestring () 1535 | "Deactivate the perspective modestring." 1536 | (interactive) 1537 | (set-frame-parameter nil 'persp--modestring nil) 1538 | (setq persp-show-modestring nil)) 1539 | 1540 | (defun persp-turn-on-modestring () 1541 | "Activate the perspective modestring." 1542 | (interactive) 1543 | (setq persp-show-modestring t) 1544 | (persp-update-modestring)) 1545 | 1546 | (cl-defun persp-other-buffer (&optional skip-buffer _visible-ok frame) 1547 | "A version of `other-buffer' which respects perspectives. 1548 | This respects ido-ignore-buffers. 1549 | TODO: The VISIBLE-OK parameter is currently ignored." 1550 | (let ((ignore-rx (persp--make-ignore-buffer-rx))) 1551 | (cl-loop for b in (buffer-list frame) do 1552 | (let ((name (buffer-name b))) 1553 | (when (and (not (and (buffer-live-p skip-buffer) (equal skip-buffer b))) 1554 | (not (string-prefix-p " " name)) 1555 | (not (string-match-p ignore-rx name)) 1556 | (member b (persp-current-buffers))) 1557 | (cl-return-from persp-other-buffer b))))) 1558 | ;; fallback: 1559 | (persp-get-scratch-buffer)) 1560 | 1561 | 1562 | ;;; --- perspective-aware buffer switchers 1563 | 1564 | ;; Buffer switching integration: useful for frameworks which enhance the 1565 | ;; built-in completing-read (e.g., Selectrum). 1566 | ;;;###autoload 1567 | (defun persp-switch-to-buffer* (buffer-or-name) 1568 | "Like `switch-to-buffer', restricted to the current perspective. 1569 | This respects ido-ignore-buffers, since we automatically add 1570 | buffer filtering to ido-mode already (see use of 1571 | PERSP-SET-IDO-BUFFERS)." 1572 | (interactive 1573 | (list 1574 | (if (or current-prefix-arg (not persp-mode)) 1575 | (let ((read-buffer-function nil)) 1576 | (read-buffer-to-switch "Switch to buffer")) 1577 | (let* ((candidates (persp-current-buffer-names t)) 1578 | (other (buffer-name (persp-other-buffer (current-buffer))))) 1579 | ;; NB: This intentionally calls completing-read instead of 1580 | ;; persp-interactive-completion-function, since it is expected to have 1581 | ;; been replaced by a completion framework. 1582 | (completing-read (format "Switch to buffer%s: " 1583 | (if other 1584 | (format " (default %s)" other) 1585 | "")) 1586 | (lambda (string predicate action) 1587 | (if (eq 'metadata action) 1588 | '(metadata (category . buffer)) 1589 | (complete-with-action action candidates string predicate))) 1590 | nil nil nil nil 1591 | other))))) 1592 | (let ((buffer (window-normalize-buffer-to-switch-to buffer-or-name))) 1593 | (switch-to-buffer buffer))) 1594 | 1595 | ;; Buffer killing integration: useful for frameworks which enhance the 1596 | ;; built-in completing-read (e.g., Selectrum). 1597 | ;;;###autoload 1598 | (defun persp-kill-buffer* (buffer-or-name) 1599 | "Like `kill-buffer', restricted to the current perspective. 1600 | This respects ido-ignore-buffers, since we automatically add 1601 | buffer filtering to ido-mode already (see use of 1602 | PERSP-SET-IDO-BUFFERS)." 1603 | (interactive 1604 | (list 1605 | (if (or current-prefix-arg (not persp-mode)) 1606 | (let ((read-buffer-function nil)) 1607 | (read-buffer "Kill buffer: " (current-buffer))) 1608 | ;; NB: This intentionally calls completing-read instead of 1609 | ;; persp-interactive-completion-function, since it is expected to have 1610 | ;; been replaced by a completion framework. 1611 | (completing-read (format "Kill buffer (default %s): " (buffer-name (current-buffer))) 1612 | (lambda (string predicate action) 1613 | (if (eq 'metadata action) 1614 | '(metadata (category . buffer)) 1615 | (complete-with-action action (persp-current-buffer-names) string predicate))) 1616 | nil nil nil nil 1617 | (buffer-name (current-buffer)))))) 1618 | (kill-buffer buffer-or-name)) 1619 | 1620 | ;; Buffer killing integration: kill all buffers in the current perspective 1621 | ;; except the current one and the perspective's scratch buffer. 1622 | ;;;###autoload 1623 | (defun persp-kill-other-buffers () 1624 | "Kill all buffers in the current perspective other than the current one. 1625 | Also excludes the perspective's scratch buffer." 1626 | (interactive) 1627 | (when (y-or-n-p "Are you sure you want to kill all buffers in the current perspective except the current buffer? ") 1628 | (cl-loop for buf in (persp-current-buffers) 1629 | unless (or (eq buf (current-buffer)) 1630 | (eq buf (get-buffer (persp-scratch-buffer)))) 1631 | do (kill-buffer buf)))) 1632 | 1633 | ;; Buffer switching integration: buffer-menu. 1634 | ;;;###autoload 1635 | (defun persp-buffer-menu (arg) 1636 | "Like the default C-x C-b, but filters for the current perspective's buffers." 1637 | (interactive "P") 1638 | (if (and persp-mode (null arg)) 1639 | (switch-to-buffer 1640 | (list-buffers-noselect nil (seq-filter 'buffer-live-p (persp-current-buffers* t)))) 1641 | (switch-to-buffer (list-buffers-noselect)))) 1642 | 1643 | ;; Buffer switching integration: list-buffers. 1644 | ;;;###autoload 1645 | (defun persp-list-buffers (arg) 1646 | "Like the default C-x C-b, but filters for the current perspective's buffers." 1647 | (interactive "P") 1648 | (if (and persp-mode (null arg)) 1649 | (display-buffer 1650 | (list-buffers-noselect nil (seq-filter 'buffer-live-p (persp-current-buffers* t)))) 1651 | (display-buffer (list-buffers-noselect)))) 1652 | 1653 | ;; Buffer switching integration: bs.el. 1654 | ;;;###autoload 1655 | (defun persp-bs-show (arg) 1656 | "Invoke BS-SHOW with a configuration enabled for Perspective. 1657 | With a prefix arg, show buffers in all perspectives. 1658 | This respects ido-ignore-buffers, since we automatically add 1659 | buffer filtering to ido-mode already (see use of 1660 | PERSP-SET-IDO-BUFFERS)." 1661 | (interactive "P") 1662 | (unless (featurep 'bs) 1663 | (user-error "bs not loaded")) 1664 | (defvar bs-configurations) 1665 | (declare-function bs--show-with-configuration "bs.el") 1666 | (let* ((ignore-rx (persp--make-ignore-buffer-rx)) 1667 | (bs-configurations (append bs-configurations 1668 | (list `("perspective" nil nil 1669 | ,ignore-rx (lambda (buf) (persp-buffer-filter buf t)) nil)) 1670 | (list `("all-perspectives" nil nil 1671 | ,ignore-rx nil nil))))) 1672 | (if (and persp-mode (null arg)) 1673 | (bs--show-with-configuration "perspective") 1674 | (bs--show-with-configuration "all-perspectives")))) 1675 | 1676 | ;; Buffer switching integration: IBuffer. 1677 | ;;;###autoload 1678 | (defun persp-ibuffer (arg) 1679 | "Invoke IBUFFER with a configuration enabled for Perspective. 1680 | With a prefix arg, show buffers in all perspectives. 1681 | This respects ido-ignore-buffers, since we automatically add 1682 | buffer filtering to ido-mode already (see use of 1683 | PERSP-SET-IDO-BUFFERS)." 1684 | (interactive "P") 1685 | (unless (featurep 'ibuffer) 1686 | (user-error "IBuffer not loaded")) 1687 | (defvar ido-ignore-buffers) 1688 | (defvar ibuffer-maybe-show-predicates) 1689 | (if (and persp-mode (null arg)) 1690 | (let ((ibuffer-maybe-show-predicates (append ibuffer-maybe-show-predicates 1691 | (list #'(lambda (buf) (persp-buffer-filter buf t))) 1692 | ido-ignore-buffers))) 1693 | (ibuffer)) 1694 | (ibuffer))) 1695 | 1696 | ;; Buffer switching integration: Consult 1697 | (with-eval-after-load 'consult 1698 | (declare-function consult--buffer-state "consult.el") 1699 | (declare-function consult--buffer-query "consult.el") 1700 | 1701 | (defvar persp-consult-source 1702 | (list :name "Perspective" 1703 | :narrow ?s 1704 | :category 'buffer 1705 | :state #'consult--buffer-state 1706 | :history 'buffer-name-history 1707 | :default t 1708 | :items 1709 | #'(lambda () (consult--buffer-query :sort 'visibility 1710 | :predicate '(lambda (buf) (persp-is-current-buffer buf t)) 1711 | :as #'buffer-name))))) 1712 | 1713 | ;; Buffer switching integration: Ivy. 1714 | ;; 1715 | ;; An alternative implementation, which has the drawback of not allowing a 1716 | ;; prefix argument to list all buffers: 1717 | ;; 1718 | ;; (defun persp-ivy-read-advice (args) 1719 | ;; (append args 1720 | ;; (list :predicate 1721 | ;; (lambda (b) (persp-is-current-buffer (cdr b) t))))) 1722 | ;; (advice-add 'ivy-read :filter-args #'persp-ivy-read-advice) 1723 | ;; (advice-remove 'ivy-read #'persp-ivy-read-advice) 1724 | 1725 | (defun persp--switch-buffer-ivy-counsel-helper (arg fallback) 1726 | (unless (featurep 'ivy) 1727 | (user-error "Ivy not loaded")) 1728 | (declare-function ivy-read "ivy.el") 1729 | (if (and persp-mode (null arg)) 1730 | (let ((real-ivy-read (symbol-function 'ivy-read)) 1731 | (current-bufs (persp-current-buffers* t))) 1732 | (cl-letf (((symbol-function 'ivy-read) 1733 | (lambda (&rest args) 1734 | (apply real-ivy-read 1735 | (append args 1736 | (list :predicate 1737 | (lambda (b) 1738 | (memq (cdr b) current-bufs)))))))) 1739 | (funcall fallback))) 1740 | (funcall fallback))) 1741 | 1742 | ;;;###autoload 1743 | (defun persp-ivy-switch-buffer (arg) 1744 | "A version of `ivy-switch-buffer' which respects perspectives." 1745 | (interactive "P") 1746 | (declare-function ivy-switch-buffer "ivy.el") 1747 | (persp--switch-buffer-ivy-counsel-helper arg #'ivy-switch-buffer)) 1748 | 1749 | ;;;###autoload 1750 | (defun persp-counsel-switch-buffer (arg) 1751 | "A version of `counsel-switch-buffer' which respects perspectives." 1752 | (interactive "P") 1753 | (unless (featurep 'counsel) 1754 | (user-error "Counsel not loaded")) 1755 | (declare-function counsel-switch-buffer "counsel.el") 1756 | (persp--switch-buffer-ivy-counsel-helper arg #'counsel-switch-buffer)) 1757 | 1758 | 1759 | ;;; --- Helm integration 1760 | 1761 | (defun persp--helm-buffer-list-filter (bufs) 1762 | (if current-prefix-arg 1763 | bufs 1764 | (persp-buffer-list-filter bufs t))) 1765 | 1766 | (defun persp--helm-remove-buffers-from-perspective (_arg) 1767 | (interactive) 1768 | (declare-function helm-marked-candidates "helm.el") 1769 | (cl-loop for candidate in (helm-marked-candidates) do 1770 | (persp-remove-buffer candidate))) 1771 | 1772 | (defun persp--helm-add-buffers-to-perspective (_arg) 1773 | (declare-function helm-marked-candidates "helm.el") 1774 | (cl-loop for candidate in (helm-marked-candidates) do 1775 | (persp-add-buffer candidate))) 1776 | 1777 | (defun persp--helm-activate (&rest _args) 1778 | (defvar helm-source-buffers-list) 1779 | (declare-function helm-make-source "helm-source.el") 1780 | (declare-function helm-add-action-to-source "helm.el") 1781 | ;; XXX: Ugly Helm initialization, works around the way 1782 | ;; helm-source-buffers-list is lazily initialized in helm-buffers.el 1783 | ;; helm-buffers-list and helm-mini (copypasta code). 1784 | (require 'helm-buffers) 1785 | (unless helm-source-buffers-list 1786 | (setq helm-source-buffers-list 1787 | (helm-make-source "Buffers" 'helm-source-buffers))) 1788 | ;; actually activate things 1789 | (advice-add 'helm-buffer-list-1 :filter-return #'persp--helm-buffer-list-filter) 1790 | (helm-add-action-to-source 1791 | "Perspective: Add buffer to current perspective" 1792 | #'persp--helm-add-buffers-to-perspective helm-source-buffers-list) 1793 | (helm-add-action-to-source 1794 | "Perspective: Remove buffer from current perspective" 1795 | #'persp--helm-remove-buffers-from-perspective helm-source-buffers-list) 1796 | ;; remove persp--helm-activate advice once it has run 1797 | (advice-remove 'helm-initial-setup #'persp--helm-activate)) 1798 | 1799 | (defun persp--helm-enable () 1800 | ;; We do not know if Helm has been loaded before Perspective is activated, so 1801 | ;; we need a way to activate Perspective-Helm integration once we know for 1802 | ;; certain that Helm is ready. An advice functino should do the trick, which 1803 | ;; will remove itself once it does its job. 1804 | (advice-add 'helm-initial-setup :before #'persp--helm-activate)) 1805 | 1806 | (defun persp--helm-disable () 1807 | (defvar helm-source-buffers-list) 1808 | (declare-function helm-delete-action-from-source "helm.el") 1809 | (if (not (featurep 'helm)) 1810 | (advice-remove 'helm-initial-setup #'persp--helm-activate) 1811 | ;; actual cleanup if Helm-perspective integration has loaded: 1812 | (helm-delete-action-from-source 1813 | #'persp--helm-remove-buffers-from-perspective helm-source-buffers-list) 1814 | (helm-delete-action-from-source 1815 | #'persp--helm-add-buffers-to-perspective helm-source-buffers-list) 1816 | (advice-remove 'helm-buffer-list-1 #'persp--helm-buffer-list-filter))) 1817 | 1818 | 1819 | ;;; --- durability implementation (persp-state-save and persp-state-load) 1820 | 1821 | ;; Symbols namespaced by persp--state (internal) and persp-state (user 1822 | ;; functions) provide functionality which allows saving perspective state on 1823 | ;; disk, and loading it into another Emacs session. 1824 | ;; 1825 | ;; The relevant commands are persp-state-save and persp-state-load (aliased to 1826 | ;; persp-state-restore). 1827 | ;; 1828 | ;; The (on-disk) data structure looks like this: 1829 | ;; 1830 | ;; { 1831 | ;; :files [...] 1832 | ;; :frames [ 1833 | ;; { 1834 | ;; :persps { 1835 | ;; "persp1" { 1836 | ;; :buffers [...] 1837 | ;; :windows [...] 1838 | ;; } 1839 | ;; } 1840 | ;; :order [...] 1841 | ;; :merge-list 1842 | ;; } 1843 | ;; ] 1844 | ;; } 1845 | 1846 | (cl-defstruct persp--state-complete 1847 | files 1848 | frames) 1849 | 1850 | ;; Keep around old version to maintain backwards compatibility. 1851 | (cl-defstruct persp--state-frame 1852 | persps 1853 | order) 1854 | 1855 | (cl-defstruct persp--state-frame-v2 1856 | persps 1857 | order 1858 | merge-list) 1859 | 1860 | (cl-defstruct persp--state-single 1861 | buffers 1862 | windows) 1863 | 1864 | (defun persp--state-complete-v2 (state-complete) 1865 | "Apply this function to persp--state-complete structs to be guaranteed a 1866 | persp--state-complete that is compatible with merge-list saving. Useful for 1867 | maintaining backwards compatibility." 1868 | (let* ((state-frames (persp--state-complete-frames state-complete)) 1869 | (state-frames-v2 1870 | (mapcar (lambda (state-frame) 1871 | (if (persp--state-frame-v2-p state-frame) 1872 | state-frame 1873 | (make-persp--state-frame-v2 1874 | :persps (persp--state-frame-persps state-frame) 1875 | :order (persp--state-frame-order state-frame) 1876 | :merge-list nil))) 1877 | state-frames))) 1878 | (make-persp--state-complete 1879 | :files (persp--state-complete-files state-complete) 1880 | :frames state-frames-v2))) 1881 | 1882 | (defun persp--state-interesting-buffer-p (buffer) 1883 | (and (buffer-name buffer) 1884 | (not (string-match "^[[:space:]]*\\*" (buffer-name buffer))) 1885 | (or (buffer-file-name buffer) 1886 | (with-current-buffer buffer (equal major-mode 'dired-mode))))) 1887 | 1888 | (defun persp--state-file-data () 1889 | (cl-loop for buffer in (buffer-list) 1890 | if (persp--state-interesting-buffer-p buffer) 1891 | collect (or (buffer-file-name buffer) 1892 | (with-current-buffer buffer ; dired special case 1893 | default-directory)))) 1894 | 1895 | (defun persp--state-window-state-massage (entry persp valid-buffers) 1896 | "This is a primitive code walker. It removes references to 1897 | potentially problematic buffers from the data structure created 1898 | by window-state-get and replaces them with references to the 1899 | perspective-specific *scratch* buffer. Buffers are considered 1900 | 'problematic' when they have no underlying file, or are otherwise 1901 | transient. 1902 | 1903 | The need for a recursive walk, and the consequent complexity of 1904 | this function, arises from the nature of the data structure 1905 | returned by window-state-get. That data structure is essentially 1906 | a tree represented as a Lisp list. It can contain several kinds 1907 | of nodes, including properties, nested trees representing window 1908 | splits, and windows (referred to internally as leaf nodes). 1909 | 1910 | For the purposes of preserving window state, we only care about 1911 | nodes in this data structure which refer to buffers, i.e., lists 1912 | with the symbol 'buffer in the first element. These 'buffer lists 1913 | can be deeply buried inside the data structure, because it 1914 | recursively describes the layout of all windows in the given 1915 | frame. They are always nested in lists with the symbol 'leaf in 1916 | the first element. 1917 | 1918 | And so, the walker descends the data structure and preserves 1919 | everything it finds. When it notices a 'leaf, it iterates over 1920 | its properties until it finds a 'buffer. If the 'buffer points to 1921 | a buffer which can be reasonably saved, it leaves it alone. 1922 | Otherwise, it replaces that buffer's node with one which points 1923 | to the perspective's *scratch* buffer." 1924 | (cond 1925 | ;; base case 1 1926 | ((not (consp entry)) 1927 | entry) 1928 | ;; base case 2 1929 | ((atom (cdr entry)) 1930 | entry) 1931 | ;; leaf: modify this 1932 | ((eq 'leaf (car entry)) 1933 | (let ((leaf-props (cdr entry))) 1934 | (cons 'leaf 1935 | (cl-loop for prop in leaf-props 1936 | collect (if (not (eq 'buffer (car prop))) 1937 | prop 1938 | (let ((bn (cadr prop))) 1939 | (if (member bn valid-buffers) 1940 | prop 1941 | (cons 'buffer 1942 | (cons (persp-scratch-buffer persp) 1943 | (cddr prop)))))))))) 1944 | ;; recurse 1945 | (t (cons (car entry) (cl-loop for e in (cdr entry) 1946 | collect (persp--state-window-state-massage e persp valid-buffers)))))) 1947 | 1948 | (defun persp--state-frame-data () 1949 | (cl-loop for frame in (frame-list) 1950 | if (frame-parameter frame 'persp--hash) ; XXX: filter non-perspective-enabled frames 1951 | collect (with-selected-frame frame 1952 | (let ((persps-in-frame (make-hash-table :test 'equal)) 1953 | (persp-names-in-order (persp-names))) 1954 | (cl-loop for persp in persp-names-in-order do 1955 | (unless (persp-killed-p (gethash persp (perspectives-hash))) 1956 | (with-perspective persp 1957 | (let* ((buffers 1958 | (cl-loop for buffer in (persp-current-buffers) 1959 | if (persp--state-interesting-buffer-p buffer) 1960 | collect (buffer-name buffer))) 1961 | (windows 1962 | (cl-loop for entry in (window-state-get (frame-root-window) t) 1963 | collect (persp--state-window-state-massage entry persp buffers)))) 1964 | (puthash persp 1965 | (make-persp--state-single 1966 | :buffers buffers 1967 | :windows windows) 1968 | persps-in-frame))))) 1969 | (make-persp--state-frame-v2 1970 | :persps persps-in-frame 1971 | :order persp-names-in-order 1972 | :merge-list (frame-parameter nil 'persp-merge-list)))))) 1973 | 1974 | (defun persp-purge-exception-p (buffer) 1975 | (if (buffer-live-p buffer) 1976 | (let (result) 1977 | (dolist (exception persp-purge-initial-persp-on-save-exceptions result) 1978 | (setq result (or result (string-match-p exception (buffer-name buffer)))))) 1979 | nil)) 1980 | 1981 | ;;;###autoload 1982 | (cl-defun persp-state-save (&optional file interactive?) 1983 | "Save the current perspective state to FILE. 1984 | 1985 | FILE defaults to the value of persp-state-default-file if it is 1986 | set. 1987 | 1988 | Each perspective's buffer list and window layout will be saved. 1989 | Frames and their associated perspectives will also be saved, 1990 | but not the original frame sizes. 1991 | 1992 | Buffers with * characters in their names, as well as buffers without 1993 | associated files will be ignored. If such buffers are currently 1994 | visible in a perspective as windows, they will be saved as 1995 | '*scratch* (persp)' buffers." 1996 | (interactive (list 1997 | (read-file-name "Save perspective state to file: " 1998 | persp-state-default-file 1999 | persp-state-default-file) 2000 | t)) 2001 | (unless persp-mode 2002 | (message "persp-mode not enabled, nothing to save") 2003 | (cl-return-from persp-state-save)) 2004 | (let ((target-file (if (and file (not (string-equal "" file))) 2005 | ;; file provided as argument, just use it 2006 | (expand-file-name file) 2007 | ;; no file provided as argument 2008 | (if interactive? 2009 | ;; return nil in interactive call mode, since 2010 | ;; read-file-name should have provided a reasonable 2011 | ;; default 2012 | nil 2013 | ;; in non-interactive call mode, we want to fall back to 2014 | ;; the default, but only if it is set 2015 | (if (and persp-state-default-file 2016 | (not (string-equal "" persp-state-default-file))) 2017 | (expand-file-name persp-state-default-file) 2018 | nil))))) 2019 | (unless target-file 2020 | (user-error "No target file specified")) 2021 | ;; overwrite the target file if: 2022 | ;; - the file does not exist, or 2023 | ;; - the file is not the one set in persp-state-default-file, or 2024 | ;; - the user called this function with a prefix argument, or 2025 | ;; - the user approves overwriting the file when prompted 2026 | (when (and (file-exists-p target-file) 2027 | (not (string-equal (if (and persp-state-default-file 2028 | (not (string-equal "" persp-state-default-file))) 2029 | (expand-file-name persp-state-default-file) 2030 | "") 2031 | target-file)) 2032 | (not (or current-prefix-arg 2033 | (yes-or-no-p "Target file exists. Overwrite? ")))) 2034 | (user-error "Cancelled persp-state-save")) 2035 | ;; before hook 2036 | (run-hooks 'persp-state-before-save-hook) 2037 | ;; optionally purge initial perspective of entries 2038 | (when persp-purge-initial-persp-on-save 2039 | (mapc 'kill-buffer (cl-remove-if #'persp-purge-exception-p (persp-all-get persp-initial-frame-name nil)))) 2040 | ;; actually save 2041 | (persp-save) 2042 | (let ((state-complete (make-persp--state-complete 2043 | :files (persp--state-file-data) 2044 | :frames (persp--state-frame-data)))) 2045 | ;; create or overwrite target-file: 2046 | (with-temp-file target-file (prin1 state-complete (current-buffer)))) 2047 | ;; after hook 2048 | (run-hooks 'persp-state-after-save-hook))) 2049 | 2050 | ;;;###autoload 2051 | (defun persp-state-load (file) 2052 | "Restore the perspective state saved in FILE. 2053 | 2054 | FILE defaults to the value of persp-state-default-file if it is 2055 | set. 2056 | 2057 | Frames are restored, along with each frame's perspective list and merge list. 2058 | Each perspective's buffer list and window layout are also 2059 | restored." 2060 | (interactive (list 2061 | (read-file-name "Restore perspective state from file: " 2062 | persp-state-default-file 2063 | persp-state-default-file))) 2064 | (unless (file-exists-p file) 2065 | (user-error "File not found: %s" file)) 2066 | (persp-mode 1) 2067 | ;; before hook 2068 | (run-hooks 'persp-state-before-load-hook) 2069 | ;; actually load 2070 | (let ((tmp-persp-name (format "%04x%04x" (random (expt 16 4)) (random (expt 16 4)))) 2071 | (frame-count 0) 2072 | (state-complete (persp--state-complete-v2 2073 | (read 2074 | (with-temp-buffer 2075 | (insert-file-contents file) 2076 | (buffer-string)))))) 2077 | ;; open all files in a temporary perspective to avoid polluting "main" 2078 | (persp-switch tmp-persp-name) 2079 | (cl-loop for file in (persp--state-complete-files state-complete) do 2080 | (when (file-exists-p file) 2081 | (find-file file))) 2082 | ;; iterate over the frames 2083 | (cl-loop for frame in (persp--state-complete-frames state-complete) do 2084 | (cl-incf frame-count) 2085 | (let ((emacs-frame (if (> frame-count 1) (make-frame-command) (selected-frame))) 2086 | (frame-persp-table (persp--state-frame-v2-persps frame)) 2087 | (frame-persp-order (reverse (persp--state-frame-v2-order frame))) 2088 | (frame-persp-merge-list (persp--state-frame-v2-merge-list frame))) 2089 | (with-selected-frame emacs-frame 2090 | ;; restore the merge list 2091 | (set-frame-parameter emacs-frame 'persp-merge-list frame-persp-merge-list) 2092 | ;; iterate over the perspectives in the frame in the appropriate order 2093 | (cl-loop for persp in frame-persp-order do 2094 | (let ((state-single (gethash persp frame-persp-table))) 2095 | (persp-switch persp) 2096 | (set-frame-parameter nil 'persp-merge-list frame-persp-merge-list) 2097 | (cl-loop for buffer in (persp--state-single-buffers state-single) do 2098 | (persp-add-buffer buffer)) 2099 | ;; XXX: split-window-horizontally is necessary for 2100 | ;; window-state-put to succeed? Something goes haywire with root 2101 | ;; windows without it. 2102 | (split-window-horizontally) 2103 | (window-state-put (persp--state-single-windows state-single) 2104 | (frame-root-window emacs-frame) 2105 | 'safe)))))) 2106 | ;; cleanup 2107 | (persp-kill tmp-persp-name)) 2108 | ;; after hook 2109 | (run-hooks 'persp-state-after-load-hook)) 2110 | 2111 | (defalias 'persp-state-restore 'persp-state-load) 2112 | 2113 | 2114 | ;;; --- perspective merging 2115 | 2116 | (defun persp-get-merge (base-name merged-name &optional frame) 2117 | "Return a merge in FRAME with :base-perspective BASE-NAME and 2118 | :merged-perspective MERGED-NAME." 2119 | (cl-find-if 2120 | (lambda (m) 2121 | (and (string= base-name (plist-get m :base-perspective)) 2122 | (string= merged-name (plist-get m :merged-perspective)))) 2123 | (frame-parameter frame 'persp-merge-list))) 2124 | 2125 | (defun persp-merges-with-base (&optional name frame) 2126 | "Return a list of all merges in FRAME with base perspective NAME." 2127 | (if (null name) (setq name (persp-current-name))) 2128 | (cl-remove-if-not 2129 | (lambda (m) 2130 | (string= name (plist-get m :base-perspective))) 2131 | (frame-parameter frame 'persp-merge-list))) 2132 | 2133 | (defun persp-perspectives-merged-with-base (&optional name frame) 2134 | "Return a list of all perspectives in FRAME that are merged to NAME." 2135 | (if (null name) (setq name (persp-current-name))) 2136 | (mapcar (lambda (m) (plist-get m :merged-perspective)) 2137 | (persp-merges-with-base name frame))) 2138 | 2139 | (defun persp-merge (base-persp-name to-merge-persp-name) 2140 | "Merge the buffer list of TO-MERGE-PERSP-NAME into the buffer list for 2141 | BASE-PERSP-NAME." 2142 | (interactive 2143 | (list (persp-current-name) 2144 | (funcall persp-interactive-completion-function 2145 | "Perspective name: " 2146 | (remove (persp-current-name) (persp-names)) nil t))) 2147 | (cl-assert (member base-persp-name (persp-names))) 2148 | (cl-assert (member to-merge-persp-name (persp-names))) 2149 | (let* ((merge (persp-get-merge base-persp-name to-merge-persp-name)) 2150 | (all-to-merge-persp-buffers (persp-get-buffer-names to-merge-persp-name)) 2151 | (merged-into-to-merge-persp-buffers (cl-loop for m in (persp-merges-with-base to-merge-persp-name) 2152 | append (plist-get m :merged-buffers))) 2153 | (buffers-to-merge (delete-dups 2154 | (cl-remove-if 2155 | (lambda (buf) 2156 | (or (member buf merged-into-to-merge-persp-buffers) 2157 | (string= buf (persp-scratch-buffer to-merge-persp-name)))) 2158 | all-to-merge-persp-buffers)))) 2159 | (with-perspective base-persp-name 2160 | (if merge 2161 | ;; update an existing merge 2162 | (let ((merged-buffers (plist-get merge :merged-buffers))) 2163 | (dolist (buf buffers-to-merge) 2164 | (unless (persp-is-current-buffer (get-buffer buf)) 2165 | (persp-add-buffer buf) 2166 | (push buf merged-buffers))) 2167 | (set-frame-parameter 2168 | nil 2169 | 'persp-merge-list 2170 | (cl-nsubstitute-if (list :base-perspective base-persp-name 2171 | :merged-perspective to-merge-persp-name 2172 | :merged-buffers merged-buffers) 2173 | (lambda (m) (equal merge m)) 2174 | (frame-parameter nil 'persp-merge-list)))) 2175 | ;; create a new merge 2176 | (let ((merged-buffers)) 2177 | (dolist (buf buffers-to-merge) 2178 | (unless (persp-is-current-buffer (get-buffer buf)) 2179 | (persp-add-buffer buf) 2180 | (push buf merged-buffers))) 2181 | (set-frame-parameter 2182 | nil 2183 | 'persp-merge-list 2184 | (push (list :base-perspective base-persp-name 2185 | :merged-perspective to-merge-persp-name 2186 | :merged-buffers merged-buffers) 2187 | (frame-parameter nil 'persp-merge-list)))))))) 2188 | 2189 | (defun persp-unmerge (base-persp-name to-unmerge-persp-name) 2190 | "Unmerge the buffers from TO-UNMERGE-PERSP-NAME from BASE-PERSP-NAME that were 2191 | were merged in from a previous call to `persp-merge'." 2192 | (interactive 2193 | (let* ((base-persp-name (persp-current-name)) 2194 | (persps-merged-with-base (persp-perspectives-merged-with-base base-persp-name)) 2195 | (to-unmerge-persp-name 2196 | (when persps-merged-with-base 2197 | (funcall persp-interactive-completion-function 2198 | "Perspective name: " 2199 | persps-merged-with-base nil t)))) 2200 | (list base-persp-name to-unmerge-persp-name))) 2201 | (let ((merge (persp-get-merge base-persp-name to-unmerge-persp-name))) 2202 | (cond ((null to-unmerge-persp-name) 2203 | (message "No perspectives merged to \"%s\"" base-persp-name)) 2204 | ((null merge) 2205 | (message "\"%s\" is not merged to \"%s\"" to-unmerge-persp-name base-persp-name)) 2206 | (t (with-perspective base-persp-name 2207 | (dolist (buf (plist-get merge :merged-buffers)) 2208 | (persp-remove-buffer buf)) 2209 | (set-frame-parameter 2210 | nil 2211 | 'persp-merge-list 2212 | (remove merge (frame-parameter nil 'persp-merge-list)))))))) 2213 | 2214 | 2215 | ;;; --- ibuffer filter group code 2216 | 2217 | (with-eval-after-load 'ibuffer 2218 | (defvar ibuffer-filtering-alist nil) 2219 | (define-ibuffer-filter persp-name 2220 | "Toggle current view to buffers with persp name QUALIFIER." 2221 | (:description "persp-name" 2222 | :reader (read-regexp "Filter by persp name (regexp): ")) 2223 | (ibuffer-awhen (persp-ibuffer-name buf) 2224 | (if (stringp qualifier) 2225 | (or (string-match-p qualifier (car it)) 2226 | (string-match-p qualifier (cdr-safe it))) 2227 | (equal qualifier it))))) 2228 | 2229 | (defun persp-ibuffer-default-group-name (persp-name) 2230 | "Produce an ibuffer group name string for PERSP-NAME." 2231 | (format "%s" persp-name)) 2232 | 2233 | (defun persp-ibuffer-name (buf) 2234 | "Return a PERSP-NAME of BUF." 2235 | (let ((persp-names (cl-loop for persp-name in (persp-all-names) 2236 | if (memq buf (persp-all-get persp-name nil)) 2237 | collect persp-name))) 2238 | (list (car persp-names)))) 2239 | 2240 | ;;;###autoload 2241 | (defun persp-ibuffer-generate-filter-groups () 2242 | "Create a set of ibuffer filter groups based on the persp name of buffers." 2243 | (unless (featurep 'ibuf-ext) 2244 | (require 'ibuf-ext)) 2245 | (declare-function ibuffer-remove-duplicates "ibuf-ext.el") 2246 | (declare-function ibuffer-push-filter "ibuf-ext.el") 2247 | (declare-function ibuffer-pop-filter "ibuf-ext.el") 2248 | (let ((persp-names (ibuffer-remove-duplicates 2249 | (delq nil (mapcar 'persp-ibuffer-name (buffer-list)))))) 2250 | (mapcar (lambda (persp-name) 2251 | (cons (persp-ibuffer-default-group-name (car persp-name)) 2252 | `((persp-name . ,persp-name)))) 2253 | persp-names))) 2254 | 2255 | ;;;###autoload 2256 | (defun persp-ibuffer-set-filter-groups () 2257 | "Set the current filter groups to filter by persp name." 2258 | (interactive) 2259 | (unless (featurep 'ibuffer) 2260 | (user-error "IBuffer not loaded")) 2261 | (defvar ibuffer-filter-groups) 2262 | (declare-function ibuffer-update "ibuffer.el") 2263 | (setq ibuffer-filter-groups (persp-ibuffer-generate-filter-groups)) 2264 | (message "persp-ibuffer: groups set") 2265 | (let ((ibuf (get-buffer "*Ibuffer*"))) 2266 | (when ibuf 2267 | (with-current-buffer ibuf 2268 | (pop-to-buffer ibuf) 2269 | (ibuffer-update nil t))))) 2270 | 2271 | 2272 | ;;; --- xref code 2273 | 2274 | ;; xref is not available in Emacs 24, so be careful: 2275 | (with-eval-after-load 'xref 2276 | (defvar persp--xref-marker-ring (make-hash-table :test 'equal)) 2277 | (if (boundp 'xref--history) 2278 | ;; Emacs 29: 2279 | (defun persp--set-xref-marker-ring () 2280 | "Set xref--history per persp." 2281 | (defvar xref--history) 2282 | (let ((persp-curr-name (persp-name (persp-curr)))) 2283 | (unless (gethash persp-curr-name persp--xref-marker-ring) 2284 | (puthash persp-curr-name (cons nil nil) 2285 | persp--xref-marker-ring)) 2286 | (setq xref--history (gethash persp-curr-name persp--xref-marker-ring)))) 2287 | ;; Emacs 28 and earlier: 2288 | (defun persp--set-xref-marker-ring () 2289 | "Set xref--marker-ring per persp." 2290 | (defvar xref-marker-ring-length) 2291 | (defvar xref--marker-ring) 2292 | (let ((persp-curr-name (persp-name (persp-curr)))) 2293 | (unless (gethash persp-curr-name persp--xref-marker-ring) 2294 | (puthash persp-curr-name (make-ring xref-marker-ring-length) 2295 | persp--xref-marker-ring)) 2296 | (setq xref--marker-ring (gethash persp-curr-name persp--xref-marker-ring)))))) 2297 | 2298 | 2299 | ;;; --- done 2300 | 2301 | ;;; XXX: Undo nasty kludge necessary for cleanly compiling this source file by 2302 | ;;; restoring saved frame parameters, and removing the variable used to save 2303 | ;;; them. 2304 | (eval-when-compile 2305 | (when (boundp 'persp--kludge-save-frame-params) 2306 | (modify-frame-parameters nil persp--kludge-save-frame-params) 2307 | (makunbound 'persp--kludge-save-frame-params) 2308 | (fmakunbound 'persp--kludge-save-frame-params) 2309 | (unintern 'persp--kludge-save-frame-params nil))) 2310 | 2311 | (provide 'perspective) 2312 | 2313 | ;; Local Variables: 2314 | ;; indent-tabs-mode: nil 2315 | ;; End: 2316 | ;;; perspective.el ends here 2317 | --------------------------------------------------------------------------------