Emacs 的视角
The Perspective 包在 Emacs 中提供多个命名工作区(或“视角”), 类似于 Awesome 和 XMonad 等窗口管理器中的多个桌面, 以及 Mac 上的 Spaces。
每个视角拥有自己的缓冲区列表和窗口布局,以及一些 其他隔离的便利功能,例如 xref 环。这使得在多个独立项目上工作而不会迷失 在所有缓冲区中变得容易。切换到某个视角会激活其窗口 配置,并且在某个视角中,只有其缓冲区可用(默认 情况下)。
每个 Emacs 框架都有独立的视角列表。
Perspective 支持将其状态保存到文件,因此长时间的工作会话可以 根据需要保存和恢复。
终于,这个项目有了 changelog; 请参阅它获取发布说明。
示例用例
多个项目
在长时间运行的 Emacs 会话中,随着多个项目的缓冲区列表相互混杂,组织工作可能会变得困难。如果你知道要搜索什么,按名称搜索缓冲区效果很好,但有时从列表中选择会更简单——在这种情况下,为相关缓冲区保持列表的整洁就成为文件和缓冲区管理的重要效率来源。Perspective 通过让你维护独立的命名缓冲区列表和窗口布局来提供帮助。
此用例与 Projectile 配合使用效果极佳。当项目具有明确定义的根目录时,Projectile 有助于缓冲区导航(以及其他特定于项目的任务)。然后 Perspective 介入,帮助管理无关的缓冲区:shell、REPL、访问项目外部目录的 dired 缓冲区,或与项目相关但不位于其余源代码相同根目录下的文件。它还有助于处理一个项目包含多个源代码仓库的情况,在这种情况下,共享窗口布局或缓冲区列表是有意义的。
Yak Shaving
假设你正在视角 feature-X 中开发功能 X。这让你能够专注于处理一组文件和窗口。随后你意识到,该功能需要你在另一组无关的文件中修复一个 bug。你不想丢失为功能 X 积累的所有上下文,因此你打开一个新视角 bugfix-Y,让你可以在不打扰 feature-X 上工作的情况下打开新的文件和缓冲区。接着,你被要求紧急查看与功能 Z 开发相关的某件事,但同样:你不想丢失上下文。于是你打开一个新视角 feature-Z,并在其中填充大量新的文件和窗口——这一切都不会丢失你在 bug Y 或功能 X 工作上积累的任何上下文。
当你完成对 Z 的查看后,你关闭视角 feature-Z,并返回到 bugfix-Y,恢复其窗口布局和缓冲区列表。当你完成 Y 的工作后,你关闭视角 bugfix-Y 并返回到 feature-X。
(提示:此工作流在将 persp-sort 变量设置为 'created 或 'oldest 时效果最佳——请参阅下文文档。)
视角合并
Yak shaving 对于处理 largely 无关的项目很有用,但有时你正在处理多个高度相关的项目,以至于你希望同时查看来自这两个项目的文件。这正是视角合并发挥作用的地方。
假设你正在处理一个需要开发多个辅助库的项目。从同一视角同时开发主项目和所有库可能会变得杂乱无章,因此你可以将每个库放入各自的视角中,以便独立地处理它们。然而,突然间你希望从主项目的视角查看库代码。与其在库和主项目的视角之间来回切换,你可以运行 M-x persp-merge 并从库的视角导入缓冲区。完成后,你可以运行 M-x persp-unmerge 来移除导入的缓冲区。
视角合并的目的是合并不同视角的缓冲区列表,同时保持对哪些缓冲区属于哪个视角的清晰区分。
- 你可以合并任意数量的视角。
- 合并是单向的,因此如果你将 A 合并到 B,B 的缓冲区在 A 中不可用。
- 合并不具有传递性,因此如果你将 A 合并到 B,然后将 B 合并到 C,A 中的缓冲区在 C 中不可用。
- 使用 persp-state-{save,load} 时,合并状态会在会话之间保存。
类似包
以下 Emacs 包实现了类似的功能:
- persp-mode: A Perspective
fork, which implements perspective sharing between Emacs frames. It also has a
different approach to saving state and different configuration options. There
has been some
interest
expressed in merging the
two projects. Due to conflicting function names,
persp-mode.eland Perspective cannot be installed simultaneously. - Workgroups 2: Similar to Perspective in terms of features. Its original codebase seems to predate Emacs acquiring a native ability to serialize window layouts, so it has custom serialization code.
- eyebrowse: Supports window layouts but not buffer lists.
- wconf: Supports window layouts but not buffer lists.
- ElScreen: Supports window layouts but not buffer lists; seems unmaintained.
- Burly: An approach to persisting window and frame configurations using Emacs bookmarks.
Emacs 27 包含两个新的缓冲区与窗口组织功能:Tab Line
(global-tab-line-mode)和 Tab Bar(tab-bar-mode)。
- Tab Line 维护一个在特定窗口中已打开的缓冲区列表, 并将其锚定到该窗口。它类似于 Web 浏览器 和其他文本编辑器中使用的标签页,因此与 Perspective 正交。
- Tab Bar 维护窗口布局(可选命名)。在这方面,它 类似于 Perspective。与 Perspective 不同,它不支持缓冲区列表。目前 不建议同时使用 Perspective 和 Tab Bar, 因为标签列表是全局的(即,会显示在所有 Perspective 中),并且 可能会造成混淆。让 Perspective 采用标签栏并允许为每个 Perspective 保持一组不同的标签,将是一个有趣的未来功能。
兼容性
Perspective 无法与 Emacs
desktop.el 配合使用。
这是因为 Perspective 状态将缓冲区和窗口信息存储在 frame
参数中,而 desktop-save-mode 不支持保存这些类型的数据。
相反,Perspective 提供了自己的 磁盘保存和 加载 功能,可以干净地保存 Perspective。
安装
你应该从 MELPA 或 MELPA Stable 安装 Perspective。
use-package 的用户可以按以下方式安装 Perspective:
(use-package perspective
:bind
("C-x C-b" . persp-list-buffers) ; or use a nicer switcher, see below
:custom
(persp-mode-prefix-key (kbd "C-c M-p")) ; pick your own prefix key here
:init
(persp-mode))
将 C-x C-b 的绑定(默认的 Emacs 缓冲区切换器)替换为 Buffer
switchers 部分中描述的更优质的实现之一。
如果未使用 use-package,请将此源代码仓库中的 perspective.el
放置在您的加载路径中的某个位置,并使用类似以下内容:
(require 'perspective)
(global-set-key (kbd "C-x C-b") 'persp-list-buffers)
(customize-set-variable 'persp-mode-prefix-key (kbd "C-c M-p"))
(persp-mode)
Debian 9 或更高版本,或 Ubuntu 16.04 或更高版本的用户可以直接 apt-get install elpa-perspective,但请注意,这些
软件仓库中提供的稳定版本可能已经(非常)过时。
入门
假设您按照
安装部分中所述使用 use-package 安装了 Perspective,那么 persp-mode 现在应该处于激活状态,并且您应该处于
main 视角中。您当前打开的任何缓冲区都将与
main 关联。(如果您没有打开任何缓冲区,为了
练习的目的,您应该打开几个。)
现在,按下 C-c M-p s。请注意,C-c M-p 是 Perspective
前缀,并且您必须在 use-package 调用中定义它。如果您
将其更改为其他内容,请使用您自己的前缀!s 是 persp-switch 命令的带前缀
绑定。输入另一个视角的名称
(例如 two)。Emacs 将切换到视角 two。您与
main 关联的缓冲区将被置于后台,您将看到与视角 two 关联的 scratch 缓冲区
(*scratch* two)。您现在打开的任何
缓冲区都将与视角 two 关联。
现在,再次按下 C-c M-p s,并在提示时输入 main。Emacs 将
然后使视角 two 进入后台并调出视角 main,恢复其
窗口布局和缓冲区列表。
用法
要激活 Perspective,请使用 (persp-mode)。这将创建一个单一的默认
main 视角。
:information_source: 自 Emacs 28 发布以来,Perspective 不再 附带默认的命令前缀。用户应选择一个令自己舒适的前缀。在 Emacs 27 及更早版本中,默认前缀为
C-x x。 这与 Emacs 28 内置的绑定冲突。
要为所有 Perspective 命令设置前缀键,请自定义
persp-mode-prefix-key。合理的选择包括 C-x x(适用于不关心此操作所遮蔽的 Emacs 缓冲区相关命令的用户)、C-z(适用于不将 Emacs 挂起到 shell 后台的用户)、C-c C-p(适用于不介意与 org-mode 和 markdown-mode 存在冲突键的用户)、C-c M-p(适用于不介意稍显别扭的组合键的用户),以及 H-p(适用于不介意完全依赖非标准 Hyper 修饰键的用户)。
实际的命令键(前缀之后按下的键)定义在
perspective-map 中。以下是 perspective-map 中定义的主要命令:
s—persp-switch: 查询要切换到的视角,或创建`—persp-switch-by-number: 按编号切换视角,或使用数字1, 2, 3.. 0作为前缀参数快速切换;注意,当persp-sort设置为'created或'oldest时,此功能可能最有用k—persp-remove-buffer: 查询要从当前视角移除的缓冲区c—persp-kill: 查询要终止的视角r—persp-rename: 重命名当前视角a—persp-add-buffer: 查询要添加到当前视角的已打开缓冲区A—persp-set-buffer: 将缓冲区添加到当前视角,并将其从所有其他视角中移除b-persp-switch-to-buffer: 类似于switch-to-buffer;包含所有视角中的所有缓冲区;如有必要会更改视角i—persp-import: 从另一个框架导入指定视角。n,<right>—persp-next: 切换到下一个视角p,<left>—persp-prev: 切换到上一个视角m—persp-merge: 临时将一个视角的缓冲区合并到另一个视角u—persp-unmerge: 撤销persp-merge的效果g—persp-add-buffer-to-frame-global: 将缓冲区添加到框架特定的“全局”视角C-s—persp-state-save: 将所有框架中的所有视角保存到文件C-l—persp-state-load: 从文件加载所有视角
缓冲区切换
由于 Perspective 为每个视角维护独立的缓冲区列表,因此使用支持 Perspective 的方法进行缓冲区切换会很有帮助。
从 Emacs 27.1 开始,可以使用 switch-to-prev-buffer-skip 变量使命令 previous-buffer 和 next-buffer 支持 Perspective,如下所示:
(setq switch-to-prev-buffer-skip
(lambda (win buff bury-or-kill)
(not (persp-is-current-buffer buff))))
使用以下任一缓冲区切换器时,您只会看到当前视角以及特定于框架的“全局”共享视角中的缓冲区。(persp-add-buffer-to-frame-global 命令会将缓冲区添加到这个特殊的特定于框架的视角中,其名称由 persp-frame-global-perspective-name 决定,默认为 GLOBAL。)
当过滤到当前视角时,Perspective 的缓冲区提示及其 BS 和 IBuffer 包装器也会遵循 ido-ignore-buffers。与 Ido 一样,条目可以是正则表达式或函数;函数接收缓冲区名称。
Ido:交互式 Do(Ido,
ido-mode),
特别是其 ido-switch-buffer 命令,在启用 persp-mode 时会自动感知 Perspective。
list-buffers / buffer-menu:Perspective 为类似的
list-buffers 和
buffer-menu 提供了包装器:
persp-list-buffers 和 persp-buffer-menu。(注意 Emacs 默认将 C-x C-b
绑定到 list-buffers。)当这些函数被正常调用时,它们显示按当前视角过滤的缓冲区菜单。带有前缀参数时,它们显示所有视角中所有缓冲区的缓冲区菜单。(
list-buffers 和 buffer-menu 之间的区别:前者调用
display-buffer,即根据 display-buffer-alist 可能分割窗口,
而后者调用 switch-to-buffer,即将当前窗口切换到
缓冲区列表缓冲区。)
bs.el:Perspective 为
bs-show 提供了一个包装器:
persp-bs-show。当此函数被正常调用时,它显示按当前视角过滤的
缓冲区列表。带有前缀参数时,它显示
所有视角中的缓冲区列表。
IBuffer: Perspective 提供了一个包装器,用于
ibuffer:
persp-ibuffer。当此函数正常调用时,它会显示一个由当前视角过滤的缓冲区列表。使用前缀参数时,它会显示所有视角中的缓冲区列表。
如果你想在 ibuffer 缓冲区中按 persp-name 对缓冲区进行分组,请使用
persp-ibuffer-set-filter-groups。或者,将其设为默认值:
(add-hook 'ibuffer-hook
(lambda ()
(persp-ibuffer-set-filter-groups)
(unless (eq ibuffer-sorting-mode 'alphabetic)
(ibuffer-do-sort-by-alphabetic))))
Helm: Perspective 附带了针对 Helm 的缓冲区列表建议,因此当启用 persp-mode
时,Helm 的缓冲区列表代码应自动具备 Perspective 感知能力。(旧版本的 Helm 依赖于 ido-mode 的机制来
列出缓冲区,因此不需要此建议;详见 this Helm commit
和 this Perspective commit
。)如果调用 helm-buffers-list 时带有前缀参数,它将显示所有视角中的缓冲区。此外,Perspective 向
helm-buffers-list 添加了操作,用于将缓冲区添加到当前视角(主要适用于前缀参数版本)以及将缓冲区从当前
视角中移除。
Consult: Perspective 提供了 persp-consult-source 源,它将列出
当前视角中的缓冲区。你可以隐藏默认的缓冲区源,
并将 persp-consult-source 添加到 consult-buffer-sources 中,以便 consult
仅列出当前视角中的缓冲区,如下所示:
(consult-customize consult-source-buffer :hidden t :default nil)
(add-to-list 'consult-buffer-sources persp-consult-source)
请注意,你仍然可以通过
narrowing
使用前缀 b 来访问所有视角中的缓冲区列表。
Ivy / Counsel:Perspective 提供了两个使用
Ivy 和 Counsel 列出缓冲区的命令:persp-ivy-switch-buffer 和 persp-counsel-switch-buffer。
当这些函数被正常调用时,它们会显示一个根据当前视角过滤的缓冲区列表。
如果带有前缀参数,它们会显示所有视角中的缓冲区列表。ivy 和 counsel 版本之间的区别
与 ivy-switch-buffer 和 counsel-switch-buffer 之间的区别相同:后者
显示要切换到的缓冲区的预览,而前者则不显示。
将其中一个这些辅助函数与 :bind 形式的
use-package 绑定是一个好主意。或者,如果你不使用 use-package,它也可以
全局绑定,例如:
(global-set-key (kbd "C-x C-b") (lambda (arg)
(interactive "P")
(if (fboundp 'persp-bs-show)
(persp-bs-show arg)
(bs-show "all"))))
关于 completing-read 增强功能的说明
使用 completing-read 增强框架(例如 Ivy、
Selectrum 或
Vertico)的用户可能希望使用以下两个
函数:
persp-switch-to-buffer*替代switch-to-bufferpersp-kill-buffer*替代kill-buffer
这两个函数的行为与内置函数类似,但直接使用 completing-read。
正常调用时,它们列出由当前 perspective 过滤的缓冲区。
使用前缀参数时,它们列出所有 perspective 中的缓冲区。
persp-switch-to-buffer* 默认将所选缓冲区导入当前
perspective;如果您希望它跳转到已存在的拥有该缓冲区的 perspective,
请将 persp-switch-to-buffer-behavior 设置为 switch。
以下示例 use-package 调用将 Emacs 的默认键绑定
更改为使用替代函数:
(use-package perspective
:bind (("C-x b" . persp-switch-to-buffer*)
("C-x k" . persp-kill-buffer*))
:config
(persp-mode))
将会话保存到磁盘
一对函数,persp-state-save 和 persp-state-load,实现了
磁盘上的视角持久性。当交互式调用时,它们会提示输入
用于保存会话和从中恢复的文件。
一个自定义变量,persp-state-default-file,设置用于
保存和恢复视角的默认文件。当它被设置时,persp-state-save 可以
在非交互模式下不带参数调用,并将保存到该变量
引用的文件。这使得在 Emacs 退出时自动保存
视角会话变得容易:
(add-hook 'kill-emacs-hook #'persp-state-save)
persp-state-save 和 persp-state-load 的一个局限性在于,它们不尝试处理具有非平凡状态的非文件访问缓冲区。不支持保存 shell、REPL 和 compilation-mode 缓冲区。当保存到文件时,指向它们的任何窗口都会更改为指向该视角的 *scratch* 缓冲区。(当然,实时窗口保持不变。)
自定义
Perspective 支持多个自定义变量(参见其在 M-x customize 中的部分)。以下变量可能最值得关注:
persp-sort: 选择调用persp-switch时排序视角的顺序。默认为'name(按字母顺序),但也可使用'access(按最近访问时间)、'created(按创建顺序,降序)和'oldest(按创建顺序,升序)。请注意,当此设置设为'access时,persp-switch-by-number可能会令人困惑,因为与视角关联的数字会不断变化。persp-interactive-completion-function: 用于提示输入视角名称。默认使用completing-read,并在启用ido-mode时激活ido-completing-read。ivy-completing-read具有广泛的兼容性,但不幸的是它按字母顺序排序,因此会破坏persp-sort设置。不幸的是,Helm 开箱即用的实现并不兼容completing-read(helm-completing-read-default-1声称是此实现,但具有不同的&optional默认值)。此处推荐设置为ido-completing-read,除非使用了completing-read增强框架。persp-mode-prefix-key: 更改 Perspective 命令的默认键前缀。persp-state-default-file: 更改用于保存和加载 Perspective 状态的默认文件。persp-show-modestring: 确定 Perspective 是否应在模型行中显示其状态。默认为t,但也可以是nil(关闭模型行状态显示)或'header(使用标题行代替模型行)。persp-modestring-short: 当设置为t时,仅显示当前视角的缩短模型行字符串,而不是完整列表。默认为nil。persp-purge-initial-persp-on-save: 当设置为t时,将终止所有缓冲区 在调用perps-state-save时的初始视角。名称匹配列表persp-purge-initial-persp-on-save-exceptions中某个正则表达式的缓冲区 不会被终止。这使得可以将初始视角用作一种临时空间。
要更改前缀键之后使用的键,使用 use-package 你可以这样做:
;; 将 n 重新映射为 N 以切换到下一个视角
(use-package perspective
:bind (
:map perspective-map
("n" . nil)
("N" . persp-next)))
或者不使用 use-package:
(define-key perspective-map (kbd "n") nil)
(define-key perspective-map (kbd "N") 'persp-next)
关于 Emacs 窗口布局的一些思考
以下讨论超出了记录 Perspective 的需求,但它属于帮助用户学习管理 Emacs 会话的范畴,因此很可能有助于 Perspective 的潜在用户使体验更加顺畅。
Emacs 在窗口处理方面的默认行为不佳:许多命令和模式习惯于分割现有窗口并更改用户精心设计的窗口布局。对于在大显示器上运行 Emacs(可能处于全屏模式)的人来说,这往往是一个更严重的问题:更多的屏幕空间使得将框架分割成许多较小的窗口变得容易,从而使任何意外的更改更具破坏性。
由于看似无差别的窗口分割和现有窗口中的缓冲区切换,新的 Emacs 用户可能会养成期望 Emacs 及其包缺乏对其布局的基本尊重的习惯。因此,像 winner-mode 这样的东西以及像
shackle 这样的包很受欢迎。
这可能会让 Perspective 的价值显得可疑:如果 Emacs 在 M-x compile 时直接丢弃窗口布局,为何还要费心精心保存它们?答案是修复那些有缺陷的默认设置。这相当容易:
(customize-set-variable 'display-buffer-base-action
'((display-buffer-reuse-window display-buffer-same-window)
(reusable-frames . t)))
(customize-set-variable 'even-window-sizes nil) ; avoid resizing
这些设置执行以下操作:
- 告诉
display-buffer尽可能重用现有窗口, 包括其他框架中的窗口。例如,如果可见窗口中已经存在一个*compilation*缓冲区,则切换到该窗口。这意味着 Emacs 通常会在“按我的意思做”的方式下切换窗口, 适用于已预热的工作流 (例如,包含几个源代码窗口、一个编译输出窗口和一个 Magit 窗口)。 - 通过告诉
display-buffer在 当前 窗口中切换到目标缓冲区来防止分割。例如,如果没有可见的*compilation*缓冲区, 那么当运行compile时,当前窗口中的缓冲区 将被替换为*compilation*。这可能会显得侵入性较强,因为它 替换了当前缓冲区,但请记住,以这种方式弹出的大多数缓冲区都很容易关闭, 无论是通过专用键绑定(通常是q)还是通用适用的kill-buffer。这比 恢复窗口布局更容易。对于预先安排好的 窗口布局,这也更容易处理,因为可以在预先准备好的窗口中 简单地运行相应的命令。(如果这一步走得太远,请将display-buffer-same-window替换为display-buffer-pop-up-window。)
(此提示的早期版本修改了 display-buffer-alist 而不是
display-buffer-base-action。这过于
激进,并且可能
影响那些可能合理地想要分割窗口的包。)
负责“弹出”窗口的 Emacs 框架的文档,
display-buffer,
内容密集且难以阅读,因此有人尝试总结
其中最重要的部分: