托管在 savannah

Elisp

Variables

我刚知道 let 可以定义没有绑定的变量

Directory Local Variables

elisp#Directory Local Variables

  • .dir-locals.el 如何组成?

    在目标目录新建 .dir-locals.el;Emacs 会从当前文件夹往上查找这个文件并把设置扩散到所有子目录。
    文件内容是一个列表,每个元素形如 (matcher . 变量表):匹配器可以是主模式符号(如 python-mode)、字符串子目录(如 “tests”),或 nil(全局默认)。
    变量表是一个 ((变量 . 值) …) 的关联列表;值写成普通 Lisp 表达式。特殊键:mode / eval / unibyte、auto-mode-alist 也能用。
    保存后重新打开文件或执行 M-x normal-mode 让设置生效;如遇到 unsafe 变量会提示确认。

    • matcher

      只有 nil major-mode string 三种

      nil 表示无条件

      major-mode 就是在对应主模式下应用

      string 表示在对应子目录下,接受的不是一个变量映射表,是一整套“匹配器 → 变量表”的列表

      除此之外就没有其它匹配器类型;如果需要更复杂的条件,只能在变量表里用 eval、再结合 when / cond 等 Lisp 逻辑自己判断。
      elisp - Emacs .dir-locals.el - setting key bindings - Stack Overflow

    • Special Keys

      mode:把某个 minor / major 模式符号放在这里会在目录内缓冲区启用该模式,例如 (mode . auto-fill)。

      eval:执行任意 Lisp 形式,常用来调用函数或设置无法单靠变量完成的配置;仍需注意安全提示。

      unibyte:设为 t 表示按单字节方式处理缓冲区(多用于旧项目编码兼容)。

      subdirs:不是变量,而是特殊标记;若设成 (subdirs . nil),当前条目仅作用于本目录,不会递归到子目录。

      顶层 auto-mode-alist:允许直接在 .dir-locals.el 为特定文件名或扩展映射主要模式,语法与全局变量同;需 Emacs 28.1 及以上。

      coding:与文件局部变量不同,不能在目录局部变量中设置该键。

  • 我非常理解 .dir-locals 的优点,但是如果我不能修改对应目录的结构、内容,如何得到相似的体验呢?

    如果你没法在目标目录里放 .dir-locals.el(比如仓库只读或受版本控制不便改动),可以把目录变量定义放进自己的 Emacs 初始化文件,通过 目录类 机制达到同样效果。套路如下:

    1. 在 init.el(或等效配置文件)里用 dir-locals-set-class-variables 定义一个“目录类”,里面照样写每种主模式 / 子目录对应的变量表。例如:

         (dir-locals-set-class-variables
          'my-proj
          '((nil . ((fill-column . 90)
                    (indent-tabs-mode . nil)
                    (dir-locals-file . "hhh.el")
                    ))
            (python-mode . ((python-indent-offset . 4)))
            ("src/imported" . ((nil . ((change-log-default-name . "ChangeLog.local")))))
            ))
    2. 用 dir-locals-set-directory-class 把真实路径绑定到这个类。路径必须是绝对路径并以目录分隔符结尾:

         (dir-locals-set-directory-class
          (expand-file-name "~") 'my-proj)

      需要针对多个目录时重复这一步。

    3. 重新打开相关文件或运行 M-x normal-mode 让设置生效。

    这样 Emacs 在访问那些目录时,会像读到了 .dir-locals.el 一样应用你在 init 文件里写的变量。文件本身保持不变,也不会污染共享仓库。

    补充提示:

    • 如果只想给自己加额外设置而又允许仓库里存在公共 .dir-locals.el,可以把共享设置放在仓库的 .dir-locals.el,个人加成放在 ~/.emacs.d/.dir-locals-2.el(或任何你在 init 里 load 的文件)里再用上述目录类绑定。
    • customize-dirlocals 也支持生成这些定义;调用时给它一个不能写的目录,Emacs 会提示把结果写到你的 init 文件里。
    • 如果你本来就依赖 EditorConfig,可以继续用 editorconfig-mode;但要在 Emacs 里实现“目录本地变量”,目录类是唯一完全在本地配置就能覆盖整个目录树的官方方法。

Data Types

Special Read Syntax

elisp#Special Read Syntax

Control Structures

Iteration

elisp#Iteration

  • dolist

    Macro: dolist (var list [result]) body…
    This construct executes BODY once for each element of LIST, binding
    the variable VAR locally to hold the current element. Then it
    returns the value of evaluating RESULT, or ‘nil’ if RESULT is
    omitted. For example, here is how you could use ‘dolist’ to define
    the ‘reverse’ function:

          (defun reverse (list)
            (let (value)
              (dolist (elt list value)
                (setq value (cons elt value)))))

    它把输出模块作为参数,这个设计和 Verilog 很像

  • doitems

    Macro: dotimes (var count [result]) body…
    This construct executes BODY once for each integer from 0
    (inclusive) to COUNT (exclusive), binding the variable VAR to the
    integer for the current iteration. Then it returns the value of
    evaluating RESULT, or ‘nil’ if RESULT is omitted. Use of RESULT is
    deprecated. Here is an example of using ‘dotimes’ to do something
    100 times:

          (dotimes (i 100)
            (insert "I will not obey absurd orders\n"))

Pattern-Matching Conditional

Lists

Building Lists

  • Modifying List Variables

    elisp#List Variables

    • push

      Macro: push element listname
      This macro creates a new list whose CAR is ELEMENT and whose CDR is
      the list specified by LISTNAME, and saves that list in LISTNAME.
      In the simplest case, LISTNAME is an unquoted symbol naming a list,
      and this macro is equivalent to
      ‘(setq LISTNAME (cons ELEMENT LISTNAME))’.

      (setq l ‘(a b))
      ⇒ (a b)
      (push ‘c l)
      ⇒ (c a b)
      l
      ⇒ (c a b)

      More generally, ‘listname’ can be a generalized variable. In that
      case, this macro does the equivalent of
      ‘(setf LISTNAME (cons ELEMENT LISTNAME))’. *Note Generalized
      Variables::.

      For the ‘pop’ macro, which removes the first element from a list,
      *Note List Elements::.

Association List

elisp#Association Lists

Alist(关联列表)是一种用 链表结构表示键值对 的通用数据结构。
常见于 Lisp 程序和早期程序语言

主要使用的是这三个函数

assoc rassoc

assq rassq

alist-get

还有其他函数等用到再说

Property Lists

Extract a value from a property list.

Searching and Matching

Regular Expressions

M-x re-builder 很好用

  • rx

    elisp#Rx Notation

    这是一个 DSL !

    有很多内置“函数”(宏语法),我们叫他 contructs

Regular Expression Searching

Markers

A “marker” is a Lisp object used to specify a position in a buffer relative to the surrounding text.
A marker changes its offset from the beginning of the buffer automatically whenever text is inserted or deleted, so that it stays with the two characters on either side of it.

Functions

Advising Emacs Lisp Functions

这是非常重要的功能

在 describe-function 里能看到函数绑定的 advice

Declare Form

elisp#Declare Form

  • 支持一些特殊形式
    • advertised-calling-convention
    • debug
    • doc-string
    • indent
    • interactive-only
    • obsolete
    • compiler-macro
    • gv-expander
    • gv-setter
    • completion
    • modes
    • interactive-args
    • pure
    • side-effect-free
    • important-return-value
    • speed
    • safety
    • ftype
    • no-font-lock-keyword

Debugging Lisp Programs

debug

elisp#Debugger

Edebug

交互(断点)式调试方案

在函数说明下面就有 Enable edebug 的选项

Debugging
Enable edebug Enable tracing
Disassemble Forget

要使用 Edebug 调试 Lisp 程序,你必须首先对要调试的 Lisp 代码进行添加调试功能。
一种简单的方法是首先将光标移动到函数或宏的定义中,然后执行 SPC m e d (使用前缀参数的 eval-defun )。
取消调试就是不添加前缀参数的 eval-defun

  • edebug-mode-map

    下面是在 normal-state 下的绑定
    n next
    c continue
    i step in
    o step out

    b set breakpoint
    u unset breakpoint
    x set conditional breakpoint
    X set global break condition

    e eval

    S stop

    W W where

  • Edebug and Macros

    codex resume 019abee1-b1cf-7eb0-a2d6-210cbaf6ac1

    • Instrumenting Macro Calls

      elisp#Instrumenting Macro Calls

      当 Edebug 对调用 Lisp 宏的表达式进行加注时,需要关于该宏的额外信息才能正确完成工作。
      这是因为无法先验地判断宏调用的哪些子表达式是要被求值的形式(form)。
      求值可能直接发生在宏体内,或在展开结果被求值时,或者任何更晚的时刻1

      因此,你必须为 Edebug 可能遇到的每个宏定义一个 Edebug 规范,说明该宏调用的格式。
      做法是在宏定义中添加一个 debug 声明。下面是一个简单示例,展示了示例宏 for 的规范(参见 “Argument Evaluation”)。

      (defmacro for (var from init to final do &rest body)
        "Execute a simple \"for\" loop.
      For example, (for i from 1 to 10 do (print i))."
        (declare (debug (symbolp "from" form "to" form "do" &rest form)))
        ...)

      Edebug 规范说明宏调用的哪些部分是要被求值的形式。
      对简单宏而言,规范常与宏定义的形式参数列表很相似,但规范比宏参数更通用。
      关于 declare 形式的更多解释,参见 “Defining Macros”。

      在为代码加注时,要确保 Edebug 已知这些规范。
      如果你要为使用其他文件中定义的宏的函数加注,可能需要先求值该文件中的 require 表达式,或显式加载包含该宏的文件。
      如果宏定义被 eval-when-compile 包裹,可能需要显式对其求值。

      你也可以用 def-edebug-spec 独立于宏定义来为宏指定 Edebug 规范。对 Lisp 中的宏而言,添加 debug 声明更常见也更方便,但 def-edebug-spec 让我们可以为用 C 实现的特殊形式定义 Edebug 规范。

      — 宏:def-edebug-spec macro specification
      指定对宏 MACRO 的调用中哪些表达式是要被求值的形式。SPECIFICATION 应是 Edebug 规范。两个实参都不会被求值。

      MACRO 实参实际上可以是任意符号,不仅限于宏名。

      下面的表格列出了 SPECIFICATION 的可能取值以及它们如何指示实参的处理方式。

      ‘t’
      所有实参都被加注求值。它是 (body) 的简写。

      a symbol
      该符号必须已有一个 Edebug 规范,会使用那个规范。此间接可重复,直到找到另一种类型的规范为止。这样可以从别的宏继承规范。

      a list
      列表元素描述调用形式各实参的类型。规范列表的可用元素将在后续小节中说明。

      如果宏没有 Edebug 规范(既没有 debug 声明也没有 def-edebug-spec 调用),变量 edebug-eval-macro-args 就会起作用。

      — 用户选项:edebug-eval-macro-args
      控制 Edebug 对没有显式 Edebug 规范的宏实参的处理方式。如果为 nil(默认),实参都不会被加注求值;否则,所有实参都会被加注。

Scripts

  • 用 advice 来给函数手动加输出

    快捷的输出式调试方案

    (advice-add 'org-roam-db-update-file :around
                (lambda (orig file &rest args)
                  (message "org-roam updating: %s" file)
                  (let ((res (apply orig file args)))
                    (message "org-roam updated: %s" file)
                    res)))
  • 这可以用来调试 warning

    (defun my/display-warning-advice (type message &optional level buffer-name)
      (let ((bt (with-output-to-string
                  (print (backtrace)))))
        (with-current-buffer (get-buffer-create "*Warning Traces*")
          (goto-char (point-max))
          (insert (format "\n[%s] %s\n%s\n" type message bt)))))
     
    (advice-add 'display-warning :around
                (lambda (orig type message &optional level buffer-name)
                  (funcall orig type message level buffer-name)
                  (my/display-warning-advice type message level buffer-name)))
  • Chat: 调试卡死的 Emacs Lisp 代码

    先确认一个常识:在 Emacs 里“强行中断当前操作”的按键是 C-g,不是 C-c。C-c 在 Emacs 里是很多命令的前缀键;
    如果你在终端里按 C-c,它会给 Emacs 发 SIGINT,但通常并不能让 Emacs Lisp 代码“跳出”,而 C-g 会触发“quit”信号。

    调试卡死的 Emacs 函数(以 org-roam 重建数据库为例)可以按下面步骤:

    一、拿到卡死时的 Lisp 调用栈

    • M-x toggle-debug-on-quit 或在 scratch 里执行:(setq debug-on-quit t)
    • 运行你会卡死的操作(org-roam-db-sync 或 org-roam-db-build)
    • 卡住后按 C-g。若卡在 Lisp 层,会弹出 *Backtrace*,把这个调用栈保存出来;这能直接告诉你卡在了哪个函数,比如 org-element-parse-buffer、emacsql 等。

    二、看是否卡在子进程或 C 层

    • M-x list-processes 看有没有 emacsql-sqlite 或 emacsql-sqlite3 子进程一直处于 run 或 stopped 状态。如果有,可能是 SQLite 子进程卡住或管道堵塞。

    • 如果 C-g 没反应,Emacs 完全不处理事件,说明可能卡在 C 层或子进程同步等待。此时可以在系统层抓栈:

      • Linux:gdb -p <Emacs 的 PID>,进入后输入 bt 或 thread apply all bt
      • macOS:lldb -p <PID>,输入 bt all

      这能看出是否卡在 sqlite、文件 IO 或其他原生库。

    三、隔离是否是某个 org 文件导致的卡死
    org-roam 构建数据库会逐个解析文件,某些特殊 / 损坏的 org 文件会让 org-element-parse-buffer 卡得很厉害。可以打印正在处理的文件,定位哪一个是“最后一条”:

    • scratch 里执行:
      (advice-add ‘org-roam-db-update-file :around
      (lambda (orig file &rest args)
      (message “org-roam updating: %s” file)
      (let ((res (apply orig file args)))
      (message “org-roam updated: %s” file)
      res)))
    • 再运行重建,观察 Messages 缓冲区。最后一个只出现“updating”而没有“updated”的文件就是嫌疑文件。
    • 打开该文件运行 M-x org-lint,修复报告的问题;或者临时把它移出 org-roam 目录,确认是否不再卡。

    四、排查 SQLite / Emacsql 后端问题

    • 只保留一个 Emacs 实例访问同一个 org-roam.db。多个 Emacs 同时写 DB 很容易锁死。
    • 删除旧的数据库及其 WAL / SHM 文件后重建:关闭 Emacs,删掉 org-roam.db、org-roam.db-wal、org-roam.db-shm(org-roam-db-location 变量可查看路径),再重启重建。
    • Emacs 29 及以上可以使用内置 sqlite,通常更稳定;如果你用的是 emacsql-sqlite3 外部可执行,确认系统里 sqlite3 可执行存在且版本新,或者改用内置 sqlite 后端(不同版本的 org-roam / Emacsql 选择方式不完全一致,无法在此给出统一变量名,如果不确定,可告知你的 Emacs / org-roam 版本我帮你给出具体设置)。
    • 更新 emacsql、org-roam 到最新版本,很多“重建卡住”问题在新版里修过。

    五、避免自动重建反复卡死

    • 先关闭自动同步:M-x org-roam-db-autosync-mode 再次执行它切换到禁用状态(-1)
    • 手工重建:M-x org-roam-db-clear 后 M-x org-roam-db-sync

    六、性能 / 热点定位(非硬卡死时)

    • M-x profiler-start,选择 cpu(或 mem),重建一次,然后 M-x profiler-report 看最耗时的函数。
    • 对 org-element-parse-buffer 大文件卡顿,尝试优化:减少超大标题 / 属性抽屉、规避极端嵌套结构、把非 Org 内容移出。

    如果你愿意进一步排查,请提供:

    • M-x emacs-version 的输出
    • org-roam 的版本(M-x describe-package org-roam 或 M-x org-roam-version)
    • 你的系统平台(Linux / macOS / Windows)
    • org-roam-db-location 的路径,以及是否存在 .db-wal / .db-shm 文件
    • list-processes 中 emacsql 相关进程的状态
    • 按上面步骤得到的 Backtrace 或 gdb / lldb 的堆栈

    先试试 debug-on-quit + C-g 的栈和“打印正在处理的文件”的办法,通常能很快锁定根因是某个文件、org-element 解析,还是 emacsql / sqlite 子进程。

Keymaps

基本绑定机制

  1. 全局键映射 (Global Keymap)

    • global-set-key - 设置全局快捷键
    • global-unset-key - 取消全局快捷键
  2. 模式键映射 (Mode Keymap)

    • define-key - 在特定 keymap 中定义键
    • local-set-key - 设置当前 buffer 的本地键
    • local-unset-key - 取消本地键
  3. Minor Mode 键映射

    • 通过 define-minor-mode 创建
    • 优先级高于 major mode

键映射层次结构

优先级从高到低:

  1. Overriding keymaps (overriding-terminal-local-map, overriding-local-map)
  2. keymap text / overlay property (at point)
  3. Minor mode keymaps
    • emulation-mode-map-alists
    • minor-mode-overriding-map-alist
    • minor-mode-map-alist
  4. Buffer’s local keymap (current buffer)
    • local-map text / overlay property (if any)
    • buffer’s default local keymap (usually major mode keymap)
  5. Global keymap

Active Keymaps 详解

根据 Emacs 官方文档,在任意时刻只有少数 keymaps 是 active 的。当 Emacs 接收用户输入时,会按以下顺序搜索活跃的 keymaps:

  • 通常的活跃 keymaps(按优先级)

    1. keymap property - 由 keymap text 或 overlay property 指定

      • 对于鼠标事件,使用事件位置而非 point
    2. Minor mode keymaps - 由启用的 minor modes 指定

      • emulation-mode-map-alists
      • minor-mode-overriding-map-alist
      • minor-mode-map-alist
    3. Buffer’s local keymap - 包含 buffer 特定的键绑定

      • 如果 point 处有 local-map text / overlay property,则使用该 keymap
      • 否则使用 buffer 的默认 local keymap(通常是 major mode 的 keymap,之所以说是通常是因为 current-local-map 是可以被替换的)
      • minibuffer 也有自己的 local keymap
    4. Global keymap - 包含与当前 buffer 无关的键绑定

      • 绑定到变量 global-map
      • 总是活跃的
  • 特殊的覆盖机制

    • overriding-local-map - 替换通常的活跃 keymaps(除了 global keymap)
    • overriding-terminal-local-map - 优先级高于所有其他 keymaps
      • 通常用于模态 / 临时键绑定
      • set-transient-map 函数提供便捷接口
  • 重要概念

    Local keymap 的共享性质:

    • Local keymap 通常由 buffer 的 major mode 设置
    • 相同 major mode 的所有 buffer 共享同一个 local keymap
    • 使用 keymap-local-set 改变一个 buffer 的 local keymap 会影响所有相同 major mode 的 buffer

    其他用途:

    • Keymaps 不仅用于激活,还用于其他方式
    • 如在 read-key-sequence 中用于事件转换
  • Minor Mode Keymaps 冲突解决

    当多个 minor mode 有相同键绑定时,按以下规则解决冲突:

    内部优先级顺序:

    1. emulation-mode-map-alists(最高优先级)
    2. minor-mode-overriding-map-alist(中等优先级)
    3. minor-mode-map-alist(最低优先级,最常见)

    同级别内的优先级:

    • 在 minor-mode-map-alist 中, 后启用的 minor mode 优先级更高
    • 列表从前往后搜索,先找到的键绑定生效
    ;; 示例:两个 minor modes 冲突
    (define-key foo-mode-map (kbd "C-c x") 'foo-command)
    (define-key bar-mode-map (kbd "C-c x") 'bar-command)
     
    ;; 启用顺序决定优先级
    (foo-mode 1)  ; 先启用
    (bar-mode 1)  ; 后启用,bar-command 生效

    解决冲突的策略:

    1. 调整启用顺序 - 让期望的模式后启用
    2. 使用 minor-mode-overriding-map-alist - 强制优先级
    3. 在相关 hook 中重新绑定键
    ;; 强制优先级示例
    (add-to-list 'minor-mode-overriding-map-alist
                 (cons 'my-important-mode my-important-mode-map))

    调试工具:

    • (describe-variable 'minor-mode-map-alist) - 查看所有活跃 minor mode keymaps
    • (key-binding (kbd "C-c x")) - 查看特定键的绑定
    • (describe-key (kbd "C-c x")) - 查看键绑定来源

Buffer 管理

elisp#Buffers

Current Buffer

elisp#Current Buffer

缓冲区存在的目的其实不只是让它们显示在窗口里
让程序使用缓冲区才是 emacs 的特别之处

current-buffer 返回 current buffer

默认情况下选中窗口中显示的 buffer 就是 current buffer
我们可以使用 set-buffer 来修改 current-buffer
但是用的更多的还是和 save-current-buffer
配合的 with-current-buffer 和 with-temp-buffer

commands

clone-indirect-buffer
clone-indirect-buffer-other-window
说实话克隆 buffer 没什么意义

ibuffer

EmacsWiki: Ibuffer Mode
18.buffer管理和ibuffer的介绍与使用 - 知乎

SPC b b +vertico/switch-workspace-buffer
SPC b B consult-buffer 我觉得区别就是这个可以显示那些“不正常”的 buffer
SPC b i ibuffer
SPC b I +ibuffer/open-for-current-workspace

minibuffer

elisp#Minibuffers

Modes

elisp#Modes

Major、minor mode 都是普通的 Lisp 函数:
执行时负责设置当前缓冲区的局部变量、局部键盘映射等,最后一步会“运行钩子”。
因此:

  • 每个 major mode 提供 FOO-mode-hook(普通 hook)。
    major mode 函数初始化完自己的状态后调用 run-hooks,依次执行用户在该 hook 里登记的函数。
    钩子里的函数可以继续修改缓冲区的局部变量、绑定键等,实现对该模式的定制。
  • 多数 minor mode 同样提供 FOO-mode-hook 或类似命名的钩子(也常是普通 hook),在启用 / 禁用时运行,让用户针对该 minor mode 做额外处理。
  • hook 本身只是变量,用来保存函数列表;mode 的职责是调用相应的 hook。
    换言之,mode 负责“触发事件”,hook 存放“事件发生时要执行的附加代码”。
  • 因为 major mode 钩子通常在切换模式后立刻运行,且缓冲区局部变量已设好,你可以在 hook 里覆盖这些设置;
    必要时还可以使用 add-hook 的 local 参数把某个 hook 函数限制在当前缓冲区。

总结:
mode 提供生命周期节点;
hook 是用户 / 扩展在这些节点插入自定义逻辑的机制,两者通过“mode 在适当时机运行 hook”这一步结合在一起。

Hooks

elisp#Hooks

add-hook
(add-hook HOOK FUNCTION &optional DEPTH LOCAL)
这里的 DEPTH 可以理解为优先级,深度小的优先,在 -100 到 100 间
如果两个函数的 DEPTH 相同就后设置的放在后面
函数名相同的函数不会重复添加
DEPTH 为 nil 默认为 0;若不为数字就默认 90

然后 local 就是说是不是 buffer-local
这里的 buffer 是在说什么 buffer?就是 current buffer
如果我们想要对其他 buffer 操作,可以用 with-current-buffer

Window 管理

elisp#Windows

emacs 的窗口控制其实和正常的平铺式窗口管理器很像了

窗口设置是一颗树(不一定是二叉树)

live window 是树的叶子
internal window 是树的非叶子节点

窗口分裂很好理解,就是把当前点转换为 internal window 然后加两个 live window
那么窗口删除呢?这不是一个简单的问题,看 Recombining Windows 章节

我们无法自己构造一个 live window 对象!
窗口对象是由 Emacs 的窗口管理器在 C 层维护的,只有通过诸如 split-window, display-buffer, pop-to-buffer 等高层 API 去请求调整布局,Emacs 才会创建 / 销毁窗口,并把对应的 window 对象返回给你。
make-window 这样的底层构造器不存在于对外的 Lisp 接口里,文档也强调窗口没有 read syntax,就是为了避免直接构造。
实际上你能做的是操控已有窗口:选择、分割、删除、调整大小、交换 buffer 等,但不能凭空生成一个“裸窗口”实例。

但是我们可以自己构造一个 internal window !

  • 窗口树结构

    每个 frame 对应一棵树,=(window-tree frame)= 产出的就是 =(ROOT MINIBUF)=,其中 MINIBUF 是单独的 minibuffer 窗口(可能是 nil)。
    ROOT 代表 frame 的 root window;它要么是 live window(frame 只有一个可见窗格),要么是 internal window(组合窗口)。

    internal window 的打印结构典型是 (DIR (LEFT TOP WIDTH HEIGHT) CHILD1 CHILD2 …):
    DIR = t 表示垂直组合(上下排列);DIR = nil 表示水平组合(左右排列)。
    坐标四元组描述这个组合在 frame 中的字符区域。
    后续每个 CHILD 要么是 live window(形如 #<window … on BUFFER>),要么又是下一层 internal window。

    遍历常用函数:window-top-child / window-left-child 分别取垂直 / 水平组合的第一个子节点;window-child 是方向无关的“第一个”;window-next-sibling、window-prev-sibling 可横向走;window-parent 回到上层。
    由于 internal window 只是组合节点,操作它通常意味着对其子树做分割、调整等——接口还是围绕现有对象,不是新建。

基础函数

window-tree 得到当前的窗口树

windowp 一个对象是否是窗口

window-live-p 一个对象是否是 live window

window-frame

window-list 得到所有 live window

frame-root-window

window-parent

window-top-child
前提:window 是一个“内部窗口”(combination window),并且它的子窗口是竖直组合(即 top / bottom 分布)。
返回:位于最上方的那个子窗口。
如果 window 是 live window,或者它的子窗口不是竖直组合,就返回 nil。

window-left-child
前提:window 是内部窗口,且子窗口是水平组合(即 left / right 分布)。
返回:最左边的那个子窗口。
其他情况返回 nil。

window-child
前提:window 是内部窗口。
返回:第一个子窗口——如果是竖直组合就是最上面的,如果是水平组合就是最左的。
若传入的是 live window,则返回 nil。

window-next-sibling

window-prev-sibling

window-combined-p

window-normalize-window 帮助函数,主要的作用是默认设置为选中窗口

窗口构成

        ____________________________________________
       |________________ Tab Line _______________|RD| ^
       |______________ Header Line ______________|  | |
     ^ |LS|LM|LF|                       |RF|RM|RS|  | |
     | |  |  |  |                       |  |  |  |  | |
Window |  |  |  |                       |  |  |  |  | Window
Body | |  |  |  |      Window Body      |  |  |  |  | Total
Height |  |  |  |                       |  |  |  |  | Height
     | |  |  |  |<- Window Body Width ->|  |  |  |  | |
     v |__|__|__|_______________________|__|__|__|  | |
       |_________ Horizontal Scroll Bar _________|  | |
       |_______________ Mode Line _______________|__| |
       |_____________ Bottom Divider _______________| v
        <---------- Window Total Width ------------>

原来一个窗口有这么多元素啊,下面这些东西是我之前不知道东西
elisp#Fringes
elisp#Display Margins
elisp#Scroll Bars
elisp#Window Dividers

Windows and Frames

window-frame

window-list

  • minibuffer 的特殊性

    Minibuffer window 本质上也是个 live window,只是专用于读命令参数、补全等等。
    它通常位于 frame 底部,显示的是 Minibuf-N 这样的 buffer。

    和普通窗口不同,minibuffer window 在窗口树里没有父节点:
    它始终贴在 frame 上,Emacs 把它当成 root window 的“同级兄弟”而不是树的成员。
    所以 window-tree 会在根组合之后额外列出 minibuffer,例如 ((…root…) #<window 4 on *Minibuf-0*>),表明它与根同层但不参与内部切分。

    如果 frame 是 minibuffer-only(专门的 minibuffer frame),这时 root 就是 minibuffer,自然仍是 live window。

    快捷地跳转或查询上下文时,可用 minibuffer-window 拿到它;
    active-minibuffer-window 则在 minibuffer 正被使用时返回它。

    不需要用 split / delete 之类 API 来管理它,Emacs 会在需要时自动显示 / 隐藏。

Buffers and Windows

elisp#Buffers and Windows

window-buffer 得到窗口显示的 buffer
set-window-buffer 给窗口设置 buffer
buffer-display-count 当前 buffer 被显示的次数
buffer-display-time 当前 buffer 上次被显示的时间戳(不知道格式)
get-buffer-window 得到显示特定 buffer 的第一个窗口
get-buffer-window-list 特定 buffer 的窗口列表
把所有窗口中显示某个 buffer 的窗口都替换为另一个 buffer

Recombining Windows

elisp#Recombining Windows

这两个是全局变量,表示默认参数
window-combination-limit
window-combination-resize

这是作用在单个 window 上的设置
set-window-combination-limit

  • combination-limit

    window-combination-limit 控制“拆分窗口时要不要先造一个新的父 internal window”。各枚举值的触发条件不同,具体是针对哪些 API 在拆分时才这么做:

    window-size(默认):只有当 display-buffer 被调用,并且它的 action alist 里带了 window-height 或 window-width(也就是请求特定尺寸)时,才会新建父窗口;其他拆分和 nil 一样。
    display-buffer:凡是 display-buffer 拆分窗口(哪怕没指定尺寸),一律新建父窗口;其他拆分仍按 nil。
    temp-buffer-resize:当 with-temp-buffer-window 拆分窗口,并且 temp-buffer-resize-mode 处于开启状态时才会新建父窗口;其余情况同 nil。
    temp-buffer:只要 with-temp-buffer-window 为显示临时 buffer 而拆分窗口,就总是新建父窗口;其余拆分和 nil 一样。

    换句话说,这些枚举值是在告诉 Emacs:“只有某些特定的函数在拆分时,请额外插入一层 internal window 来保护原有布局”。其他情况下则仍旧沿用共享父节点的默认策略。

Switching Buffers

elisp#Switching Buffers

  • switch-to-buffer

    最直接、侵入性最强

    Display buffer BUFFER-OR-NAME in the selected window.

    它不做额外的判断,也不会自动拆窗或复用其它窗口

    常见模式:先 select-window 到目标窗,再 switch-to-buffer。例如

    (select-window origin-window)
    (switch-to-buffer buffer)

    优点:可预测、简单。
    缺点:完全由调用方负责选择窗口;如果想在别处显示或拆窗,就得提前准备好窗口。

    可以受变量 switch-to-buffer-obey-display-actions 影响:
    设为非 nil 时,switch-to-buffer 会走 display-buffer 规则,结果就不再是“完全原地覆盖”。默认 nil 才是你描述的“最直接”行为。

    与 dedicated window 的关系:
    在专用窗口中切换会受 switch-to-buffer-in-dedicated-window 控制

  • pop-to-buffer

    面向常规交互的“带策略”方案

    内部调用 display-buffer,默认策略是“能 reuse 已有窗口就不拆窗;如果当前只有一个窗口,就 split-window-sensibly 再显示”。
    我现在的 pop-to-buffer 分屏行为有点怪,要基本上占满那个小屏幕才能触发横向 split,然后逻辑就单纯是宽度大于某个宽度后 vsplit
    它和目前画面显示的字体大小无关,之和 frame 的大小有关!也就是之和分辨率有关

    会自动 select-window 到最终显示 buffer 的窗口,所以用户立刻可以开始编辑。

    你可以用 pop-to-buffer 搭配 switch-to-buffer:
    例如先 pop-to-buffer 让 Emacs 用默认策略找到 / 拆出窗口;
    再用 switch-to-buffer 替换内容。

    pop-to-buffer 会先解析 buffer-or-name:
    若为 nil 用 other-buffer,若为不存在的名字则新建缓冲区(主模式由 major-mode 变量决定),然后把该缓冲区设为当前缓冲区并返回。
    之后它调用 display-buffer 显示该缓冲区,尽量不用当前选中的窗口;成功显示后会选中那个窗口,如果在别的图形框架上则尝试把输入焦点交给该框架。
    action 参数若为动作表(alist)就直接传给 display-buffer;若为非 nil 且不是表,则强制改用当前窗口以外的窗口,即便缓冲区已经在当前窗口里。
    除非 norecord 为非 nil,它和 switch-to-buffer 一样会更新缓冲区列表(影响 “previous / next buffer” 顺序等)。

    主要区别在于是否主动切换窗口 / 焦点:

    • display-buffer 负责按照显示规则把目标缓冲区放到某个窗口里,但执行完后仍然停留在原窗口,光标和焦点都不会跳走,只是把内容展示出来。
    • pop-to-buffer 先调用 display-buffer 做展示,随后一定会选中那个窗口:如果窗口在其他 frame,还会把焦点切过去;除非传了 norecord,否则该窗口也会被记录到最近
      使用窗口的历史里。交互调用时带前缀参数,它还会强制不要复用当前窗口。

    还有 pop-to-buffer-same-window 这个函数
    find-file 就是用的这个东西跳转的

Displaying Buffers

  • Choose Window

    elisp#Choosing Window

    display-buffer
    底层调度引擎,几乎所有“显示 buffer”函数最终都走到这里

    在某个窗口显示特定 buffer,不切换焦点
    需要注意的是窗口不能作为参数!不会切换焦点!

    Emacs 的“窗口调度”其实指的是在同一框架(frame)里如何选择、拆分和切换 window 来展示 buffer 的一套机制。理解它可以从几个层次入手:

    • 基本概念:一个 frame(通常就是一个 Emacs GUI 窗口或终端里的一个 Emacs 实例)里可以包含多个 window。每个 window 显
      示一个 buffer(或 buffer 的一部分)。用户操作(比如 switch-to-buffer、display-buffer、pop-to-buffer)会触发 Emacs
      去决定“哪个 window 来显示哪一个 buffer”,这就是调度。

    display-buffer 流程:
    大多数命令最终都会调用 display-buffer,它按照
    display-buffer-alist
    display-buffer-base-action
    display-buffer-fallback-action
    的规则依次尝试各种“action”:

    • 直接复用当前 window。
    • 尝试使用已有其他 window。
    • 根据规则拆分窗口(水平 / 垂直)。
    • 如果实在找不到合适的 window,再“pop up”一个新的 frame 或使用 minibuffer 等。

    你可以通过自定义这些 action(比如 display-buffer-alist 中写条件和 handler 函数)来控制“某类 buffer 要放在什么位置”。

    这其中只有 display-buffer-fallback-action 是有默认值的:

    ((display-buffer--maybe-same-window
      display-buffer-reuse-window
      display-buffer--maybe-pop-up-frame-or-window
      display-buffer-in-previous-window
      display-buffer-use-some-window
      display-buffer-pop-up-frame))

    注意这是一个 alist ,因为条件是空的所以套了两个括号

    • 窗口配置(window-configuration):Emacs 记得当前 frame 的窗口布局,你可以用 current-window-configuration 保存它;
      后续用 set-window-configuration 恢复。很多包在打开临时 buffer 时先保存配置,关闭时恢复,这样不会破坏用户原来的布局。

    • server / with-editor 的特殊处理:像 server-switch-buffer、with-editor 等机制,本质上也是在调用 display-buffer,但它
      们会在调用前后调整一些变量:

      • server-window:指定 server 需要显示 buffer 时优先使用哪个窗口(默认是 pop-to-buffer)。
      • with-editor-server-window-alist:允许基于文件名模式指定“显示这个文件的 buffer 时用哪一种窗口选择函数”。Magit
        就是利用这个把 COMMIT_EDITMSG 之类的文件固定在 Magit 的弹出窗口里。
      • 这些 hook 主要是为了让“外部命令要求 Emacs 打开一个临时文件”时,能落在用户期望的位置,并在编辑完成后恢复先前窗口配置。
    • 常用变量 / 函数:

      • display-buffer-alist:最常用的自定义入口,可以写匹配 buffer 名称 / 模式的规则,再指定 Action,比如“总是在右侧分
        屏”、“总是用现有的 log window”等。
      • split-window-sensibly、split-window-right 等:决定拆分策略。
      • quit-window、delete-window、bury-buffer:关闭临时窗口时的后续操作。
      • winner-mode、window-configuration-to-register:提供更高级的窗口布局撤销 / 恢复。

    用这些概念,你就可以像 Magit 那样为 Majutsu 设计窗口策略:在打开编辑缓冲区时走你自己的 display-buffer 规则,或在
    with-editor 回调里设置 with-editor-server-window-alist,让 Jujutsu 的描述文件总是占用 log 窗口,并在结束时调用 set-
    window-configuration 恢复布局。

  • Buffer Display Action Functions

    elisp#Buffer Display Action Functions

    接受一个“action” 参数(单个函数或列表),决定如何挑选 / 创建窗口;常见 action 有:
    display-buffer-same-window 强制在当前窗口显示
    display-buffer-reuse-window 尽量复用已有窗口(可以配 display-buffer-reuse-mode-window)
    display-buffer-pop-up-window 显式拆一个新窗(默认下方)
    display-buffer-below-selected
    display-buffer-in-side-window

    • 翻译

      Action Functions for Buffer Display(缓冲区显示动作函数)

      “动作函数”是 display-buffer 用来选择窗口以显示缓冲区的函数。动作函数接受两个参数:BUFFER,即要显示的缓冲区,以及 ALIST,即动作关联表(参见 Buffer Display Action Alists::)。成功时返回一个显示 BUFFER 的窗口,失败时返回 nil。

      以下列出了 Emacs 中定义的基本动作函数。

      display-buffer-same-window buffer alist
      尝试在 selected window 中显示 BUFFER。
      如果选中 window 是回显区 window,或者已 dedicated 于其他缓冲区(参见 Dedicated Windows::),则会失败。
      如果 ALIST 中的 inhibit-same-window 条目为非 nil,也会失败。

      display-buffer-reuse-window buffer alist
      尝试通过寻找已经显示 BUFFER 的 window 来显示它。
      优先选择选中 frame 上的 window,而不是其他 frame 上的 window。
      如果 ALIST 的 inhibit-same-window 条目为非 nil,则选中 window 不具备复用资格。
      可通过 reusable-frames 条目指定要搜索已有 window 的 frame 集合;
      如果 ALIST 不包含该条目,则仅搜索选中 frame。
      如果此函数选中了其他 frame 上的 window,会让该 frame 可见,并且除非 ALIST 含有 inhibit-switch-frame 条目,否则会在必要时提升该 frame。
      如果 ALIST 的 reuse-indirect 条目为非 nil,且未找到显示 BUFFER 的 window,此函数会尝试寻找与 BUFFER 间接关联的 window——即 window-indirect-buffer-p 以该 window 和 BUFFER 为参数返回非 nil 的 window。
      如果找到了这样的 window,且 reuse-indirect 条目的 CDR 等于符号 buffer,那么它不会把该 window 的缓冲区替换为 BUFFER,而是保持原缓冲区并返回该 window。
      否则,它会把 BUFFER 放入该 window 并返回它。

      display-buffer-reuse-mode-window buffer alist
      尝试通过寻找显示特定模式的缓冲区的 window 来显示 BUFFER。
      如果 ALIST 包含 mode 条目,其值应为一个主模式符号或主模式列表;
      否则使用 BUFFER 当前的主模式。若某 window 显示的缓冲区的主模式派生自这些模式之一,则该 window 符合条件。
      ALIST 中的 inhibit-same-window、reusable-frames 和 inhibit-switch-frame 条目也会像在 display-buffer-reuse-window 中那样影响行为。

      display-buffer-pop-up-window buffer alist
      尝试通过拆分最大的 window 或最近最少使用的 window(通常位于选中 frame 上)来显示 BUFFER。
      它通过调用 split-window-preferred-function(参见 Choosing Window Options::)执行拆分。
      可通过 ALIST 中的 window-height 和 window-width 调整新 window 大小;
      若 ALIST 含有 preserve-size 条目,Emacs 还会尝试在后续的尺寸调整中保持新 window 大小(参见 Preserving Window Sizes::)。
      如果没有 window 可以拆分,此函数会失败,通常是因为所有 window 都不够大。
      降低 split-height-threshold 或 split-width-threshold 的值可能有所帮助。
      当选中 frame 的 unsplittable frame 参数为真时,也无法拆分(参见 Buffer Parameters::)。

      display-buffer-in-previous-window buffer alist
      尝试在之前显示过 BUFFER 的 window 中再次显示它。
      如果 ALIST 的 inhibit-same-window 条目为非 nil,则选中 window 不可用。
      Dedicated window 仅在已经显示 BUFFER 的情况下才可用。
      如果 ALIST 含有 previous-window 条目,即便该 window 从未显示过 BUFFER,也可使用。
      如果 ALIST 含有 reusable-frames 条目(参见 Buffer Display Action Alists::),其值决定应在哪些 frame 中寻找合适 window。
      若 ALIST 没有该条目,而 display-buffer-reuse-frames 与 pop-up-frames 又都为 nil,则只搜索 selected frame;
      若两者任一为非 nil,则搜索当前终端上的所有 frame。
      如果有多个 window 符合条件,按以下优先级选择:
      • previous-window 条目指定的 window,前提是它不是选中 window。
      • 之前显示过 BUFFER 的 window,前提是它不是选中 window。
      • 如果选中 window 被 previous-window 指定或曾显示过 BUFFER,则选择该 window。

      display-buffer-use-some-window buffer alist
      尝试在现有 window 中显示 BUFFER。
      首先在 ALIST 的 lru-frames 条目所指定的 frame 中,寻找最近不常用的 window(参见 Cyclic Window Ordering::);
      若无该条目,则回退到选中 frame。
      它也会优先挑选满足 window-min-width 和 window-min-height 条目的 window;
      若未提供 window-min-width,则偏好全宽 window。
      最后,它不会返回使用时间高于 ALIST 中 lru-time 条目值的 window。
      如果找不到较不常用的 window,它会尝试使用其他 window,优先考虑可见 frame 上的大 window。
      如果所有 window 都 dedicated 于其他缓冲区,则可能失败(参见 Dedicated Windows::)。
      上述描述适用于 some-window 条目为 lru 或 nil(默认值)的情况。
      另一个可能值是 mru。
      例如,如果把 display-buffer-base-action 自定义为 (nil . ((some-window . mru))),该函数会偏好最近使用过的 window,从而让连续几次 display-buffer 的结果显示在同一 window 中。
      设想用户在至少三个 window 的布局中,希望在非选中 window 中依次查看查询结果所对应的多个缓冲区。
      若使用 lru 策略,Emacs 可能因“最近最少使用 window”不断变化而不停换 window;
      采用 mru 策略,则总会选择同一个 window,从而带来更可预测的体验。

      display-buffer-use-least-recent-window buffer alist
      类似 display-buffer-use-some-window,但更努力避免使用最近用过的 window,尤其不会使用选中 window。
      它会优先复用已经显示 BUFFER 的 window,并且仅根据 window 的使用时间决定是否使用显示其他缓冲区的 window;若找不到可用 window,则会弹出新 window。
      此函数还会提升所返回 window 的使用时间(参见 Selecting Windows::),以避免后续调用再次把其他缓冲区放到这个 window。
      如果应用需要连续显示多个缓冲区,可以在 ALIST 中 提供一个 lru-time 条目,初始值设为选中 window 的使用时间。
      这样每次调用都会把返回 window 的使用时间提高到略高于该值,从而防止后续调用再次选中之前的 window。

      display-buffer-in-direction buffer alist
      尝试按 ALIST 指定的位置显示 BUFFER。
      ALIST 应包含 direction 条目,值为 left、above(或 up)、right、below(或 down)之一,其他值通常被当作 below。
      若 ALIST 还包含 window 条目,其值指定参考 window。
      它可以是特殊符号,如 main(代表选中 frame 的主 window,参见 Side Window Options and Functions::)或 root(代表选中 frame 的根 window,参见 Windows and Frames::),也可以是任意有效 window。
      若省略 window 条目或给出其他值,则默认使用选中 window 作为参考 window。
      函数首先尝试复用指定方向上已显示 BUFFER 的 window;若没有,则尝试拆分参考 window,在指定方向上创建新 window;若仍失败,则尝试使用该方向上的现有 window。
      无论哪种方式,所选 window 都会出现在参考 window 指定方向的一侧,并至少共享一条边。
      如果参考 window 是“活的”,所选 window 与其共享的边总是 direction 条目所指定方向的反方向。
      例如,direction 为 left 时,所选 window 的右边缘坐标(参见 Coordinates and Windows::)会等于参考 window 的左边缘坐标。
      如果参考 window 是“内部 window”,复用的 window 必须与其共享 direction 指出的一侧边。
      例如,当参考 window 是 frame 根 window 而 direction 为 left 时,复用 window 必须位于 frame 左侧,其左边缘坐标与参考 window 相同。
      若需要创建新 window,则通过拆分参考 window 产生,使得新 window 与参考 window 共享与 direction 相反的一侧边。
      继续上述例子,将创建带有新活 window 和参考 window 的根组合,所选 window 的右边缘坐标等于参考 window 的左边缘坐标,左边缘坐标则等于新根 window 的左边缘坐标。
      direction 条目的四个特殊值可以隐式指定选中 frame 的主 window 为参考 window:leftmost、top、rightmost、bottom。
      例如,可以用 (direction . leftmost) 代替 (direction . left) (window . main);此时任何 window 条目都会被忽略。

      display-buffer-below-selected buffer alist
      尝试在选中 window 下方的 window 显示 BUFFER。若下方存在 window 且已显示 BUFFER,就复用它。
      如果没有这样的 window,则尝试拆分选中 window 并在新 window 中显示 BUFFER。
      若 ALIST 含有合适的 window-height 或 window-width 条目,它还会尝试调整该 window 大小。
      如果拆分失败且选中 window 下方有一个非 dedicated window 显示其他缓冲区,则尝试使用该 window 显示 BUFFER。
      如果 ALIST 含有 window-min-height,则保证所用 window 的高度至少达到该值。
      但这只是保证;若要真正调整 window 高度,还需同时提供合适的 window-height 条目。

      display-buffer-at-bottom buffer alist
      尝试在选中 frame 底部的 window 显示 BUFFER。
      它要么尝试拆分 frame 底部的 window 或根 window,要么复用选中 frame 底部的现有 window。

      display-buffer-pop-up-frame buffer alist
      创建一个新 frame,并在该 frame 的 window 中显示 BUFFER。
      新 frame 通过调用 pop-up-frame-function(参见 Choosing Window Options::)创建。
      若 ALIST 含有 pop-up-frame-parameters 条目,其值会附加到新 frame 的参数中。

      display-buffer-full-frame buffer alist
      在当前 frame 上显示 BUFFER,并删除所有其他 window,使其占满整个 frame。

      display-buffer-in-child-frame buffer alist
      尝试在选中 frame 的子 frame(参见 Child Frames::)中显示 BUFFER,可以复用已有子 frame 或创建新子 frame。
      若 ALIST 的 child-frame-parameters 条目为非 nil,其值是要赋给新子 frame 的参数关联表。
      默认会提供 parent-frame 参数,值为选中 frame。
      若希望子 frame 属于其他父 frame,则必须在 ALIST 中增加相应条目。
      子 frame 的外观在很大程度上取决于通过 ALIST 提供的参数。
      建议至少用比率来指定子 frame 的尺寸(参见 Size Parameters::)和位置(参见 Position Parameters::),并添加 keep-ratio 参数(参见 Frame Interaction Parameters::),以确保子 frame 保持可见。
      更多建议参数参见 Child Frames::。

      display-buffer-use-some-frame buffer alist
      尝试通过查找符合某个谓词的 frame(默认是除选中 frame 外的任意 frame)来显示 BUFFER。
      如果选中其他 frame 上的 window,会让该 frame 可见,并且除非 ALIST 含有 inhibit-switch-frame 条目,否则在必要时提升该 frame。
      若 ALIST 的 frame-predicate 条目为非 nil,其值应为函数,接受一个 frame 参数并在该 frame 符合条件时返回非 nil;此函数将替换默认谓词。
      若 ALIST 的 inhibit-same-window 条目为非 nil,则不会使用选中 window。
      如果 selected frame 只有一个 window,也不会使用该 frame。

      display-buffer-no-window buffer alist
      若 ALIST 的 allow-no-window 条目为非 nil,此函数不会显示 BUFFER,而是返回符号 fail。
      这是动作函数返回值约定中的唯一例外:通常动作函数返回 nil 或显示 BUFFER 的 window。
      如果没有该条目,此函数返回 nil。
      若返回 fail,display-buffer 会跳过之后的显示动作并立即返回 nil。
      若返回 nil,display-buffer 会继续执行后续动作(若存在)。
      假定只要调用者给 display-buffer 指定了非 nil 的 allow-no-window 条目,它也能处理 nil 返回值。

      另外两个动作函数在相应章节中介绍:display-buffer-in-side-window(参见 Displaying Buffers in Side Windows::)和 display-buffer-in-atom-window(参见 Atomic Windows::)。

  • Buffer Display Action Alists

    elisp#Buffer Display Action Alists

    我理解半天没看懂这个 alist 到底应该长什么样

    • 翻译

      Action Alists for Buffer Display(缓冲区显示动作关联表)

      “动作关联表”是一种关联表,把预定义的符号映射到相应的值;动作函数识别这些符号并按约定解释其值。
      在每次调用中,display-buffer 都会构造一份新的、可能为空的动作关联表,并把整张表传递给它要调用的任何动作函数。

      按设计,动作函数可以自由解释动作关联表中的条目。
      事实上,像 allow-no-window 或 previous-window 这样的条目只有一个或少数几个动作函数会使用,其余的函数会忽略它们。
      其他条目,比如 inhibit-same-window 或 window-parameters,则应当被大多数动作函数尊重,包括应用程序和外部扩展所提供的动作函数。

      上一小节已经详细说明了各个动作函数如何解释它们关心的条目。
      这里给出所有已知动作关联表条目的参考列表,按照它们的符号、取值,以及识别这些符号的动作函数(参见 Buffer Display Action Functions::)。
      下面列表中,“缓冲区”指 display-buffer 要显示的缓冲区,“值”指条目的取值。

      inhibit-same-window
      如果值为非 nil,表示不能使用当前选中的窗口来显示缓冲区。所有(重新)使用已有窗口的动作函数都应当尊重这个条目。

      previous-window
      值必须指定一个窗口,该窗口可能之前显示过该缓冲区。display-buffer-in-previous-window 会优先考虑这个窗口,只要它仍然有效并且没有被专用给其他缓冲区。

      mode
      值要么是一个主模式,要么是主模式列表。display-buffer-reuse-mode-window 当窗口的缓冲区主模式与值匹配时,可以复用该窗口。其他动作函数会忽略此条目。

      frame-predicate
      值必须是一个接受单个参数(框架)的函数,返回非 nil 时表示该框架可以作为显示缓冲区的候选。display-buffer-use-some-frame 会用到此条目。

      reusable-frames
      值指定了为了复用已有显示该缓冲区的窗口,要遍历的框架集合。可以设置为:

      • `nil`:只考虑选中框架上的窗口。(实际上是最后一个使用的、非仅限回显区的框架。)
      • `visible`:考虑所有可见框架上的窗口。
      • 0:考虑所有可见或图标化的框架上的窗口。
      • 一个框架:只考虑该框架上的窗口。
      • `t`:考虑所有框架上的窗口。(注意这通常不是正确的选择——它也可能返回一个工具提示框架。)

      注意这里 `nil` 的含义与 `next-window` 的 ALL-FRAMES 参数稍有不同(参见 *Cyclic Window Ordering::*)。

      主要的使用者是 `display-buffer-reuse-window`,但所有试图复用窗口的动作函数也会受到影响。`display-buffer-in-previous-window` 在查找曾在其他框架上显示该缓冲区的窗口时也会参考它。

      inhibit-switch-frame
      非 nil 值会阻止因为 display-buffer 选中的窗口位于其他框架而提升或选中该框架。主要影响 display-buffer-use-some-frame 和 display-buffer-reuse-window。理想情况
      下 display-buffer-pop-up-frame 也应受到影响,但窗口管理器未必会配合。

      window-parameters
      值指定了要赋给所选窗口的窗口参数关联表。所有选择窗口的动作函数都应处理此条目。

      window-min-width
      值指定所用窗口的最小宽度,以标准的框架列计。特殊值 full-width 表示选中窗口必须在其框架中左右都没有其他窗口。

      当前 `display-buffer-use-some-window` 和 `display-buffer-use-least-recent-window` 会尊重此条目,它们会尽力避免返回达不到该宽度要求的、不常用窗口。

      注意仅提供此条目不一定真的能把窗口调到指定宽度。若要调整现有窗口或新建窗口使其达到指定宽度,还应提供一个指定该值的 `window-width` 条目。不过 `window-width` 也可以指定完全不同的值,或者让窗口宽度适应其缓冲区;此时 `window-min-width` 提供了窗口的最低保证宽度。

      window-min-height
      值指定所用窗口的最小高度,以标准的框架行计。特殊值 full-height 表示选中窗口必须在其框架中上下都没有其他窗口。

      当前 `display-buffer-below-selected` 会尊重此条目,不会使用达不到高度要求的窗口。`display-buffer-use-some-window` 和 `display-buffer-use-least-recent-window` 也会尽量避免返回不满足此约束的不常用窗口。

      注意仅提供此条目不一定真的能把窗口调到指定高度。若要调整现有窗口或新建窗口使其达到指定高度,还应提供一个指定该值的 `window-height` 条目。不过 `window-height` 也可以指定完全不同的值,或者让窗口高度适应其缓冲区;此时 `window-min-height` 提供了窗口的最低保证高度。

      window-height
      值指定是否以及如何调整所选窗口的高度,可以是:

      • `nil`:保持窗口高度不变。
      • 整数:指定窗口目标总高度(行数)。
      • 浮点数:指定窗口目标总高度占框架根窗口总高度的比例。
      • 形如 `(body-lines . N)` 的 cons 单元,指定窗口正文高度为 N 行。
      • 函数:以所选窗口为唯一参数调用,函数负责调整窗口高度,返回值被忽略。常用函数包括 `fit-window-to-buffer` 和 `shrink-window-if-larger-than-buffer`(参见 *Resizing Windows::*)。

      按惯例,仅当窗口属于垂直组合(参见 *Windows and Frames::*)时才调整其高度,以避免影响其他不相干的窗口。此外,此条目只应在紧接着列表后面所述的特定条件下处理。

      window-width
      类似于上述 window-height,但用于调整窗口宽度。值可以是:

      • `nil`:保持窗口宽度不变。
      • 整数:指定窗口目标总宽度(列数)。
      • 浮点数:指定窗口目标总宽度占框架根窗口总宽度的比例。
      • 形如 `(body-columns . N)` 的 cons 单元,指定窗口正文宽度为 N 列。
      • 函数:以所选窗口为唯一参数调用,函数负责调整窗口宽度,返回值被忽略。

      window-size
      结合前两个条目,用于同时调整窗口高度与宽度。由于窗格只能在一个方向上调整尺寸而不影响其他窗格,window-size 只有在窗口独占整个框架时才有效。值可以是:

      • `nil`:保持窗口尺寸不变。
      • 两个整数构成的 cons 单元:指定窗口目标总宽度和高度(列、行),对应调整框架大小。
      • 形如 `(body-chars . (COLUMNS . LINES))` 的 cons 单元:指定窗口正文的目标宽度与高度,单位分别是框架列和行,对应调整框架大小。
      • 函数:以所选窗口为唯一参数调用,函数负责调整窗口所在框架的尺寸,返回值被忽略。

      此条目只应在紧接着列表后面所述的特定条件下处理。

      dedicated
      非 nil 时,指示 display-buffer 将新建的窗口标记为专用窗口(参见 Dedicated Windows::)。它通过调用 set-window-dedicated-p,将所选窗口作为第一个参数、条目的值作为第二个参数。侧窗口默认以值 side 被标记为专用(参见 Side Window Options and Functions::)。

      preserve-size
      非 nil 时,指示 Emacs 保持所选窗口大小(参见 Preserving Window Sizes::)。值应为 (t . nil)(保持宽度)、(nil . t)(保持高度)或 (t . t)(保持宽高)。此条目只应在紧接着列表后的特定条件下处理。

      lru-frames
      值指定要在其中寻找可以显示缓冲区的窗口的框架集合。display-buffer-use-some-window 和 display-buffer-use-least-recent-window 在尝试找到显示其他缓冲区的较不常用窗口时会使用它。其取值与前述 reusable-frames 相同。

      lru-time
      值用于指定一个使用时间(参见 Selecting Windows::)。display-buffer-use-some-window 和 display-buffer-use-least-recent-window 在尝试寻找较不常用窗口时会考虑它。如果某窗口的使用时间高于此值,这些动作函数将不会考虑该窗口来显示缓冲区。

      bump-use-time
      非 nil 时,会让 display-buffer 提升所用窗口的使用时间(参见 Selecting Windows::)。这样可以避免 display-buffer-use-some-window 和 display-buffer-use-least-recent-window 等动作函数随后再用此窗口显示其他缓冲区。

      使用此条目与调用 `display-buffer-use-least-recent-window` 之间的差别在于,调用该动作函数只会提升它用于显示缓冲区的窗口的使用时间,而这里的条目会让`display-buffer` 提升任何用于显示缓冲区的窗口的使用时间。

      pop-up-frame-parameters
      值指定在新建框架时要赋予的框架参数关联表。只有 display-buffer-pop-up-frame 使用它。

      pop-up-frames
      值控制 display-buffer 是否可以通过新建框架来显示缓冲区。其含义与变量 pop-up-frames 相同,但当条目存在时,它具有更高优先级。它主要用于在特定缓冲区上覆盖变量的非 nil 值,方便用户把这些缓冲区保留在选中框架中。

      parent-frame
      值指定在缓冲区显示在子框架时所用的父框架。仅 display-buffer-in-child-frame 使用该条目。

      child-frame-parameters
      值指定缓冲区显示在子框架时要使用的框架参数关联表。仅 display-buffer-in-child-frame 使用该条目。

      side
      值表示在框架或窗口的哪一侧创建新的窗口来显示缓冲区。display-buffer-in-side-window 使用它来确定在框架的哪一侧放置新的侧窗口(参见 Displaying Buffers in Side Windows::)。display-buffer-in-atom-window 也会用它来表示在已有窗口的哪一侧放置新窗口(参见 Atomic Windows::)。

      slot
      非 nil 时,值指定侧窗口中的哪个槽位用于显示缓冲区。仅 display-buffer-in-side-window 使用该条目。

      direction
      值指定一个方向,结合 window 条目可以让 display-buffer-in-direction 确定显示缓冲区的窗口位置。

      window
      值指定一个窗口,该窗口与 display-buffer 所选的窗口有某种关系。当前 display-buffer-in-atom-window 使用它来表示在哪个窗口一侧创建新窗口;display-buffer-in-direction 使用它来指定结果窗口相对于哪个参考窗口出现。

      allow-no-window
      值为非 nil 时,display-buffer 不一定要显示该缓冲区,调用者可以接受这种情况。此条目并非为用户自定义准备,因为不能保证任意调用 display-buffer 的代码都能处理“没有窗口显示缓冲区”的情况。只有 display-buffer-no-window 会关心该条目。

      some-window
      当值为 nil 或 lru 时,display-buffer-use-some-window 会优先选择最久未使用的窗口,同时避免选择非全宽窗口以及其他框架上的窗口。值为 mru 时,它会优先选择最近使用过的窗口,但不考虑当前选中的窗口或其他框架上的窗口。若值为函数,则以缓冲区和关联表两个参数调用该函数,函数应返回用于显示缓冲区的窗口。

      body-function
      值必须是一个接受单个参数(已显示的窗口)的函数。该函数可以用来填充窗口正文的内容,这些内容可能依赖于窗口的尺寸。它在缓冲区显示之后、处理 window-height、
      window-width 和 preserve-size 条目之前调用,因为这些条目可能调整窗口大小以适应插入的内容。

      post-command-select-window
      值为非 nil 时,display-buffer 所显示的缓冲区会在当前命令执行完后被选中,具体通过运行 post-command-hook(参见 Command Overview::)。
      值为 nil 时,诸如 pop-to-buffer 等函数所选中的缓冲区会被取消选中,并且在调用该函数之前所选的窗口会保持选中状态,无论在该命令过程中曾选中过哪些窗口。

      category
      如果 display-buffer 的调用者在其 ACTION 参数里传入了 (category . SYMBOL),那么你可以在 display-buffer-alist 条件部份里用同一个符号来匹配该缓冲区(参见 buffer-match-p: Buffer List)。因此,如果某个 Lisp 程序在调用 display-buffer 时使用特定的 SYMBOL 作为类别,用户就可以通过在 display-buffer-alist 中包含相应
      条目来自定义这些缓冲区的显示方式。

      按照惯例,window-height、window-width 和 preserve-size 这些条目只会在所选窗口的缓冲区已设置完毕、并且该窗口从未显示过其他缓冲区的情况下应用。更准确地说,窗口要么是在当前 display-buffer 调用中创建的,要么是在过去由 display-buffer 创建来显示该缓冲区、并且此后从未用于显示其他缓冲区,当前调用再次复用它。

      如果没有指定 window-height、window-width 或 window-size 条目,当缓冲区是临时的并且启用了 temp-buffer-resize-mode 时,窗口仍可能被自动调整大小(参见 Temporary Displays::)。在这种情况下,window-height、window-width 或 window-size 条目的 CDR 可以用来禁止或覆盖 temp-buffer-resize-mode 针对特定缓冲区或特定 display-buffer 调用的默认行为。

  • Choosing Window Options

    split-window-sensibly

  • Buffer Display 指南

    最朴素的情况:
    一个 frame 里始终只有一个窗口,因此最近一次 display-buffer 调用总能把目标 buffer 放进去。

    实际使用中,默认的 split-height-threshold / split-width-threshold 允许 frame 拆分成多个窗口,display-buffer 会复用或拆分窗口来展示新 buffer。

    只要用户自定义了上述阈值、手动改过布局,或调用 display-buffer 时传了非 nil 的 ACTION / 调整了前文提到的选项,默认行为就会失效。
    此时各种显示动作叠加,布局复杂,很容易令人挫败。

    但完全放弃 display-buffer、只靠手动拆分/删除窗口也不可取;
    display-buffer 提供统一框架给用户与 Lisp 程序协商需求,并在后续关闭 buffer 时部分恢复布局。

    • 写 display action 时放轻松

      ACTION 需要把动作函数和动作 alist 塞在同一个列表里,可记住几种基本模式来组合:

      '(nil (inhibit-same-window . t)) ;只有 alist 条目,用于禁止 display-buffer-same-window。
      '(display-buffer-below-selected) ;只有动作函数。
      '(display-buffer-below-selected (inhibit-same-window . t)) ;函数 + alist。
      '((display-buffer-below-selected display-buffer-at-bottom) (inhibit-same-window . t)) ;多个函数。
      '((display-buffer-below-selected display-buffer-at-bottom) (inhibit-same-window . t) (window-height . fit-window-to-buffer)) ;再加一个 alist 条目。

      调用示例:

      (display-buffer
       (get-buffer-create "*foo*")
       '((display-buffer-below-selected display-buffer-at-bottom)
         (inhibit-same-window . t)
         (window-height . fit-window-to-buffer)))

      配置 display-buffer-alist:

      (setopt
       display-buffer-alist
       '(("\\*foo\\*"
          (display-buffer-below-selected display-buffer-at-bottom)
          (inhibit-same-window . t)
          (window-height . fit-window-to-buffer))))

      为第二个 buffer 添加配置:

      (setopt
       display-buffer-alist
       '(("\\*foo\\*"
          (display-buffer-below-selected display-buffer-at-bottom)
          (inhibit-same-window . t)
          (window-height . fit-window-to-buffer))
         ("\\*bar\\*"
          (display-buffer-reuse-window display-buffer-pop-up-frame)
          (reusable-frames . visible))))
    • 彼此尊重

      • display-buffer-alist、display-buffer-base-action 属于用户选项,Lisp 程序不应设置或重绑定;display-buffer-overriding-action 供应用在确有必要时谨慎使用。
      • 旧版实现里,用户和应用常为 pop-up-frames、pop-up-windows 等选项争执,这也是 display-buffer 重新设计的原因之一。
      • Lisp 程序必须允许用户自定义导致“布局出乎意料”;不可假定 buffer 一定按 ACTION 里的要求被显示。
      • 用户也不要施加过多限制,否则就失去特定场景(如左右对比两个版本)的布局特性。一般偏好设置放在 display-buffer-base-action,具体 buffer 的特殊要求再用
        display-buffer-alist。
    • 优先复用已经显示该 buffer 的窗口

      • 无论用户还是程序,都应考虑目标 buffer 已有窗口时直接复用,否则可能不停弹新 frame。
      • 只有在必须展示不同位置时才避免复用。
      • 因此 display-buffer-reuse-window 应尽量常用;若只想避免选中的那个窗口,可配合 =(inhibit-same-window . t)=。
    • 让目标窗口吸引注意力

      • 多 frame 环境中,只要不设 =(inhibit-switch-frame . t)=,展示 buffer 的 frame 会自动获得焦点。
      • 单 frame 用户更棘手,display-buffer-pop-up-window、display-buffer-use-some-window 可能拆分或选中“看似随机”的窗口,打断注意力。
      • 一些程序会优先选底部窗口,让 buffer 靠近 minibuffer(用户接下来要输入)。
        非输入场景下 display-buffer-below-selected 更合适,因为被选中的窗口通常正受关注。
    • 注意哪个窗口被选中

      注意 doom 修改过了 display-buffer-alist ,预先规定了 Messages 窗口的位置,所以下面的代码都不起作用

      • 很多程序在 with-selected-window 或 select-window (NORECORD 非 nil) 的窗口游历 (excursion) 中调用 display-buffer,这几乎总是糟糕的做法:游历内选中的窗口并非用户实际看到的那个。
      • 如果用户加了 =(inhibit-same-window . t)=,那它会避开游历里选中的窗口,而不是结果布局中用户所选窗口,行为会很怪。
      • 示例:
      (progn
        (split-window)
        (display-buffer "*Messages*"))

      在单窗口 frame 上评估会在底部显示 *Messages*,并保持上方窗口被选中。而:

      (with-selected-window (split-window)
        (display-buffer "*Messages*"))

      则会在顶部显示 Messages 并选中它,违背预期。

      • 同理,
      (progn
        (split-window)
        (pop-to-buffer "*Messages*"))

      会正确选中 *Messages*;但

      (progn
        (split-window)
        (with-selected-window (selected-window)
          (pop-to-buffer "*Messages*")))

      就不会了。

      • 依赖“当前选中窗口使用次数最大”这一假设的动作函数(如 display-buffer-use-some-window 、 display-buffer-use-least-recent-window )也可能失败。
      • 因此,如果必须做窗口游历,应该尽量等游历结束再调用 display-buffer。

Dedicated Windows

elisp#Dedicated Windows

toggle-window-dedicated C-x w d

这东西需要和 Choose Window 一起理解
如果一个 window 被标记为 dedicated 它就倾向于不被其他 buffer 覆盖

暂时不看

Quitting Windows

elisp#Quitting Windows

quit-restore-window

退出一共有四种,我觉得我们可以把他们一次按四类退出来命名

  1. 删除 window 和 frame
  2. 删除 window 不删除 frame
  3. 恢复到这个 window 之前显示的 buffer
  4. 让这个 window 显示其他 buffer
  • 翻译

    Quitting Windows(退出窗口)

    当某个命令用 display-buffer 把缓冲区放到屏幕上后,用户可能想要把它隐藏起来,并恢复 Emacs 先前的显示配置。
    我们把这称为“退出窗口”。
    实现方法是:在用于显示该缓冲区的窗口处于选中状态时调用 quit-window。

    应该如何恢复先前的显示配置,取决于为了显示现在的缓冲区,对那个窗口做了什么处理。
    也许需要删除该窗口、删除其所在的框架,或只是把该窗口改为显示另一个缓冲区。
    复杂之处在于:从显示该缓冲区到现在,用户可能已经更改了窗口配置,而我们不应该撤销用户明确要求的那些修改。

    为了让 quit-window 做到恰当的处理,display-buffer 会把它对窗口所做的事情记录在该窗口的 quit-restore 参数里(参见 Window Parameters)。

    &optional kill window
    此命令会退出 WINDOW 并把它的缓冲区埋藏(bury)。(也可能改为 kill 缓冲区,取决于 quit-window-kill-buffer,见下文。)
    参数 WINDOW 必须是一个活动窗口,默认是当前选中窗口。
    带前缀参数 KILL 为非 nil 时,会 kill 缓冲区而不是 bury。
    quit-window 会先运行 quit-window-hook,然后调用下面介绍的 quit-restore-window 来完成主要工作。

    下面这个选项用于指定当 KILL 参数为 nil 时,quit-window 应该 kill 还是 bury WINDOW 的缓冲区。

    quit-window-kill-buffer
    此变量控制 quit-window 是否 kill 它的 WINDOW 参数的缓冲区。
    若值为 nil(默认值),quit-window 仅在 KILL 参数非 nil 时才会 kill 该缓冲区。
    若值为 t,quit-window 会尝试在不管 KILL 参数为何值的情况下都 kill 缓冲区。
    否则,该值应为主模式列表;若缓冲区的主模式在此列表中或派生自其中任意成员,quit-window 会无视 KILL 的值而 kill 缓冲区。
    其他情况下,只有当 KILL 非 nil 时才 kill,否则 bury。

    相比调用 quit-window,直接调用 quit-restore-window 可以获得更多控制。

    quit-restore-window &optional window bury-or-kill
    此函数在退出之后处理 WINDOW 及其缓冲区。
    可选参数 WINDOW 必须是活动窗口,默认是当前选中窗口。
    函数会参考该窗口的 quit-restore 参数。
    可选参数 BURY-OR-KILL 指定如何处理 WINDOW 的缓冲区。有效取值如下:

    `nil`
    表示不对缓冲区做特殊处理。因此,如果 WINDOW 未被删除,之后调用 `switch-to-prev-buffer` 通常会再次显示该缓冲区。

    `append`
    如果 WINDOW 未被删除,就把其缓冲区放到 WINDOW 之前缓冲区列表的末尾(参见 *Window History::*),从而降低将来调用 `switch-to-prev-buffer` 时切回它的概率。
    同时,该缓冲区会被移到该框架缓冲区列表的末尾(参见 *Buffer List::*)。

    `bury`
    如果 WINDOW 未被删除,就把其缓冲区从 WINDOW 的历史列表中移除,并把它移到框架缓冲区列表末尾。
    这是除 kill 缓冲区之外,最能防止 `switch-to-prev-buffer` 再次切回该缓冲区的方法。

    `kill`
    kill 掉 WINDOW 的缓冲区。

    `killing`
    与 `kill` 类似,但假定窗口的缓冲区会在其他地方被 kill。
    `replace-buffer-in-windows` 和 `quit-windows-on` 会使用这个值。

    `burying`
    与 `bury` 类似,但假定窗口的缓冲区会在其他地方被 bury。
    `quit-windows-on` 会使用这个值。

    当 WINDOW 应被删除且它是所在框架上的唯一窗口,同时该终端上还有其他框架时,BURY-OR-KILL 还会决定对该框架的处理方式。
    若 BURY-OR-KILL 为 `kill`,则意味着删除该框架。否则,通过调用 `frame-auto-hide-function`(见下文),以该框架作为唯一参数,来决定该框架的命运。

    窗口的 quit-restore 与 quit-restore-prev 参数(参见 Window Parameters::)会指导 quit-restore-window 如何处理其 WINDOW 参数。
    如果这些参数不存在或为 nil,通常意味着窗口是通过 split-window-below 或 split-window-right(参见 (emacs)Split Window)这类命令创建的,quit-restore-window 只有在该窗口专用于其缓冲区时才会删除它。

    若某个参数非 nil,它的形式为一个四元素列表:

    (METHOD OBUFFER OWINDOW THIS-BUFFER)

    第一个元素 METHOD 是 window、frame、same、other 四个符号之一。frame 与 window 控制是否删除 WINDOW;same 与 other 则控制在其中显示别的缓冲区。

    具体来说,window 表示该窗口是 display-buffer 专门创建的;frame 表示创建了单独的框架;same 表示该窗口只显示过这个缓冲区;other 表示之前显示过其他缓冲区。

    第二个元素 OBUFFER 要么是符号 window 或 frame,要么是形如

    (PREV-BUFFER PREV-WINDOW-START PREV-WINDOW-POINT HEIGHT)

    的列表,表示之前在 WINDOW 中显示的缓冲区、当时的窗口起始位置(参见 Window Start and End::)、窗口点位置(参见 Window Point::),以及 WINDOW 当时的高度。如果退出时 PREV-BUFFER 仍然存活,就可能复用 WINDOW 来显示它。

    第三个元素 OWINDOW 是当初显示之前选中的窗口。如果退出时删除了 WINDOW,则会尝试选中 OWINDOW。

    第四个元素 THIS-BUFFER 是设置该 quit-restore 参数时显示的缓冲区。只有当 WINDOW 仍在显示这个缓冲区时,退出操作才会利用该参数中的信息。

    当满足以下条件时,quit-restore-window 会尝试删除 WINDOW:(1) METHOD 为 window 或 frame;(2) 该窗口没有之前显示过其他缓冲区的历史;(3) THIS-BUFFER 与当前窗口
    中显示的缓冲区相同。如果 WINDOW 是原子窗口(参见 Atomic Windows::)的一部分,它会尝试删除那个原子窗口的根窗口。不论哪种情况,它都会尽量避免在无法删除时抛出
    错误。

    如果 OBUFFER 是列表并且 PREV-BUFFER 仍然存活,退出操作会按照 OBUFFER 里的信息,在 WINDOW 中显示 PREV-BUFFER,其中包括在显示 THIS-BUFFER 时临时调整的高度 HEIGHT。
    否则,如果 WINDOW 之前显示过其他缓冲区(参见 Window History::),就会显示历史中最近的那个缓冲区。

    概念上,quit-restore 参数用于撤销某窗口的第一次缓冲区显示操作;quit-restore-prev 参数用于撤销该窗口上一系列显示操作中的最后一次。
    发生在两者之间的显示操作,通过显示这个窗口之前的缓冲区来撤销。

    display-buffer 在使用某个窗口时,如果该窗口是它创建的,或不存在非 nil 的 quit-restore 参数,就会设置该参数;如果窗口已经有 quit-restore 参数,则会增加一个
    quit-restore-prev 参数,其 METHOD 元素为 same 或 other。无论哪种情况,只要窗口已有 quit-restore-prev 或 quit-restore 参数,它都可能更新参数内容。

    quit-restore-window 会先尝试查找合适的 quit-restore-prev 参数,以确定要显示哪个缓冲区;若找不到,再查找合适的 quit-restore 参数,决定删除窗口或在其中显示另一个缓冲区。

    一旦使用了其中一个参数,quit-restore-window 会把它设为 nil;未使用的参数保持不变。
    当这些参数引用了即将被 kill 的缓冲区(无论是 PREV-BUFFER 还是 THIS-BUFFER)时,replace-buffer-in-windows(参见 Buffers and Windows::)也会把它们重置。
    所有这些操作都应保持这样的不变式:如果窗口的 quit-restore-prev 参数为非 nil,则它的 quit-restore 参数也必须为非 nil。

    下面这个选项可以让 quit-restore-window 更积极地删除窗口。

    quit-restore-window-no-switch
    若该选项为 nil,除非窗口是 display-buffer 创建的,否则 quit-restore-window 总会调用 switch-to-prev-buffer。
    若为 t,它会尽力仅切换到这个窗口之前显示过的缓冲区。
    若为符号 skip-first,则只有在窗口至少拥有两个之前的缓冲区时才切换。
    不管取哪种值,只要 quit-restore-window 没有切换到之前的缓冲区,它就会尝试删除窗口(以及可能的框架)。
    注意若窗口是专用窗口,它通常也不会在其中切换到之前的缓冲区。

    下面这个选项指定当退出某个窗口时,如果该窗口所在框架只含一个窗口,该如何处理该框架。

    frame-auto-hide-function
    此选项指定的函数用于自动隐藏框架,调用时只传入一个框架参数。
    当选中窗口是专用窗口并显示要 bury 的缓冲区时,bury-buffer(参见 Buffer List::)会调用它。quit-restore-window 也会调用它(见上文),前提是该窗口所在的框架是
    专门为显示该缓冲区而创建的,并且缓冲区没有被 kill。
    默认函数是 iconify-frame(参见 Visibility of Frames::)。你也可以改为 delete-frame(参见 Deleting Frames::)以移除该框架、make-frame-invisible 以让其不可
    见、ignore 以保持框架不变,或任意其他能接受一个框架参数的函数。
    注意:只有在该框架仅有一个活动窗口,并且同一终端上还有其他框架时,才会调用此函数。
    对于某个特定框架,其 auto-hide-function 框架参数(参见 Frame Interaction Parameters::)可以覆盖这里设定的值。

Window Parameters

elisp#Window Parameters

window-parameter
set-window-parameter
window-parameters

window-persistent-parameters
ignore-window-parameters

  • quit-restore

    若某个参数非 nil,它的形式为一个四元素列表:

    (METHOD OBUFFER OWINDOW THIS-BUFFER)

    第一个元素 METHOD 是 window、frame、same、other 四个符号之一。
    frame 与 window 控制是否删除 WINDOW;same 与 other 则控制在其中显示别的缓冲区。

    具体来说,window 表示该窗口是 display-buffer 专门创建的;
    frame 表示创建了单独的框架;
    same 表示该窗口只显示过这个缓冲区;
    other 表示之前显示过其他缓冲区。

    第二个元素 OBUFFER 要么是符号 window 或 frame,要么是形如

    (PREV-BUFFER PREV-WINDOW-START PREV-WINDOW-POINT HEIGHT)

    的列表,表示之前在 WINDOW 中显示的缓冲区、当时的窗口起始位置(参见 Window Start and End::)、窗口点位置(参见 Window Point::),以及 WINDOW 当时的高度。

    如果退出时 PREV-BUFFER 仍然存活,就可能复用 WINDOW 来显示它。

    第三个元素 OWINDOW 是当初显示之前选中的窗口。
    如果退出时删除了 WINDOW,则会尝试选中 OWINDOW。

    第四个元素 THIS-BUFFER 是设置该 quit-restore 参数时显示的缓冲区。
    只有当 WINDOW 仍在显示这个缓冲区时,退出操作才会利用该参数中的信息。

  • quit-restore-prev

Frame 管理

elisp#Frames

minibuffer frame

Elisp Info: minibuffer-only frame
典型用法是在工作区旁常驻一个迷你输入面板:主 frame 保持全屏编辑,旁边的 minibuffer-only frame 处理命令提示/补全信息,避免主窗口因 minibuffer 弹出而抖动。

有的 tiling/compositor 配置会把这个小 frame 固钉在另一屏或某个角落,让所有“命令交互”都在那里发生;
Emacs 可以设置 default-minibuffer-frame 指定它。

当主 frame 禁用本地 minibuffer (minibuffer frame parameter 设成 nil) 时,
所有 minibuffer 交互都会重定向到这个专用 frame,也方便外部窗口管理器在视觉上保持稳定。

对远程/嵌入场景也很实用,比如将 minibuffer frame 映射到别的终端或浮动层,以便把输入焦点与主编辑界面分离。

Input Focus

elisp#Input Focus

select-frame-set-input-focus

Batch Mode

elisp#Batch Mode

批处理模式允许在非交互环境下运行 Emacs,常用于自动化脚本、CI/CD 和单元测试。

基本用法

emacs --batch [选项] [文件]

常用参数

  • -Q 或 =—quick=:不加载用户配置(init.el),保证干净环境
  • =—batch=:以批处理模式运行,不启动图形界面
  • -L DIR--directory=DIR=:将 DIR 添加到 =load-path
  • -l FILE 或 =—load=FILE=:加载 Elisp 文件
  • -f FUNC 或 =—funcall=FUNC=:调用函数 FUNC
  • =—eval EXPR=:求值表达式 EXPR
  • =—script FILE=:执行脚本文件(自动进入批处理模式)

运行单元测试示例

下面是用批处理模式运行 ERT 单元测试的典型命令:

emacs -Q --batch -L . -L test \
  -l majutsu.el \
  -l test/majutsu-test.el \
  -f ert-run-tests-batch-and-exit

各参数说明:

  • =-Q=:不加载用户配置,保证测试环境干净可复现
  • =—batch=:批处理模式,不启动图形界面,适合脚本/CI
  • =-L . -L test=:将当前目录和 test 目录加入 =load-path=,方便后续加载文件
  • =-l majutsu.el=:加载被测试的源码文件
  • -l test/majutsu-test.el=:加载测试文件(内含 =ert-deftest 定义的测试)
  • =-f ert-run-tests-batch-and-exit=:调用 ERT 批量测试运行器
    • 运行所有已加载的测试
    • 测试结束后退出
    • 全部通过返回退出码 0,否则返回非零并打印失败详情

其他批处理用例

  • 字节编译

    emacs --batch -f batch-byte-compile *.el

    更加一般的情况是需要把依赖 load 一下

    emacs -Q --batch -L . -L test \
        -l majutsu-template.el \
        -f batch-byte-compile test/majutsu-template-test.el
  • 执行简单脚本

    emacs --batch --eval '(message "Hello from batch mode!")'
  • 处理文件

    emacs --batch input.txt \
      --eval '(replace-string "old" "new")' \
      -f save-buffer

注意事项

  • 批处理模式下没有交互式 minibuffer,需要确保代码不需要用户输入
  • 输出会直接打印到标准输出/标准错误
  • 可以通过退出码判断执行结果(0 表示成功)
  • 适合集成到 Makefile、CI 配置中

Processes

sentinels

哨兵
elisp#Sentinels

非常重要的异步功能!

set-process-sentinel 用来给异步进程绑定一个“哨兵”回调。
当进程状态变化时(例如正常结束、异常退出、被杀掉,或网络连接打开/关闭),Emacs 会调用你提供的函数。

要点:

  • 回调形态:函数会收到两个参数 (process event),分别是进程对象和描述状态变化的字符串。
  • 作用场景:进程结束后做清理、提示用户、根据成功/失败决定后续步骤、链式启动下一个进程等。
  • 默认行为:sentinel 传 nil 时使用默认哨兵,它只把状态变化消息写到进程的缓冲区。
  • 区分过滤器:哨兵处理“状态变化”,不是处理输出;进程输出应用 set-process-filter。
  • 实务建议:不要依赖解析 event 字符串,用 process-status 和 process-exit-status 判断结果;回调里避免长时间阻塞或抛错。

Display

Overlays

elisp#Overlays

用来叠加修改文本属性

useless whitespace

emacs#Useless Whitespace

delete-trailing-whitespace
SPC c w

还有一个 doom 自带的
doom/delete-trailing-newlines
SPC c W

Microsoft Windows

emacs#Microsoft Windows
</home/disk/Dev/emacs/nt/>

Emacs Manual

Files

Comparing Files

emacs#Comparing Files

TRAMP

tramp#Top
emacs#Remote Files

TRAMP extends the Emacs file name syntax by adding a remote component.
A remote file name always looks like /method:user@host:/path/to/file.

这是相当好用的一个功能,只是我目前还没有使用它的需求

现在有了 VPS 终于是有机会用上这个东西了

Buffers

emacs#Buffers

Multiple Windows

emacs#Windows
elisp 文档里讲的比这里的详细
这里更多程度上是操作上的文档

Editing Programs

Program Modes

emacs#Program Modes
verilog-mode

  • emacs-lisp-mode

    emacs-lisp-mode

Document View

我应该不会用到它?

Text

Filling

我发现在对处理纯文本电子书的时候非常好用!!!

auto-fill-mode
这不是中文用户用得到的东西,它的逻辑是当当前行号超过 fill-column 之后,
把空格转换为回车,实际上我们根本不打空格

fill-column
set-fill-column C-x f

fill-paragraph
M-q 这个快捷键相当顺手

fill-region

  • TODO 没有对 footnote-reference 元素的适配

    应该只需要修改一下 fill-nobreak-predicate 就行了

Text Based Tables

emacs#Text Based Tables

并不能在 org-mode 下很好地渲染

Outline Mode

emacs#Outline Mode

outline-hide-body
z B
只显示标题
这个 org-mode 下也能正常工作

Frames and Graphical Displays

SPC o f 打开新 frame
SPC o F 选择 frame

Ediff

ediff#Top
Ediff 是 Emacs 内置的强大文件比较与合并工具,支持多种操作模式。

基本用法

  • 启动 Ediff

    ediff - 比较两个文件
    ediff3 - 三向比较(常用于合并冲突)
    ediff-buffers - 比较两个 buffer
    ediff-directories - 比较两个目录
    ediff-merge - 合并两个文件
    ediff-merge-with-ancestor - 三向合并(有共同祖先)
    ediff-revision - 比较文件与其版本控制历史

  • 在 Dired 中使用

    dired buffer 中:
    dired-diff =

核心快捷键

Ediff 使用单字符命令(在 Ediff Control Panel 中):

  • 导航与显示

    • n / SPC - 下一个差异块
    • p / DEL - 上一个差异块
    • j - 跳转到指定差异块
    • v / V - 垂直滚动上/下
    • < / > - 水平滚动左/右
    • | - 切换垂直/水平分割
    • m - 宽屏模式切换
    • @ - 切换是否自动精细比较(细到单词/字符)
  • 应用差异

    • a - 从 A buffer 复制当前差异块到 B
    • b - 从 B buffer 复制当前差异块到 A
    • r a / r b - 恢复 buffer A/B 的当前差异块
    • ! - 更新差异区域(重新计算)
  • 合并操作(ediff-merge 模式)

    • a - 选择 A 版本的差异
    • b - 选择 B 版本的差异
    • c - 选择 C 版本的差异(三向合并)
    • + - 合并 A 和 B 到结果中
    • d a / d b - 选择 A/B 的差异并标记为”默认”
  • 编辑与精细控制

    • w a / w b - 保存 buffer A/B
    • * a / * b - 手动编辑 buffer A/B 的差异区域
    • & - 切换是否忽略空白差异
  • 会话控制

    • h - 显示帮助
    • ? - 快速帮助摘要
    • q - 退出 Ediff 会话
    • z - 挂起 Ediff(保留状态)
    • Q - 强制退出(不保存)

高级特性

  • 忽略特定差异

    ;; 忽略大小写
    (setq ediff-ignore-case-option t)
     
    ;; 忽略空白字符
    (setq ediff-ignore-whitespace t)
     
    ;; 忽略特定正则表达式匹配的行
    (setq ediff-ignore-similar-regions t)
  • 目录比较

    M-x ediff-directories 可以递归比较两个目录:

    • 显示差异文件列表
    • 可以逐个打开文件进行 ediff
    • 支持过滤文件类型
  • 三向合并(解决 Git 冲突)

    ;; 自动在 Git 冲突时使用 Ediff
    (setq ediff-window-setup-function 'ediff-setup-windows-plain)
    (setq ediff-split-window-function 'split-window-horizontally)

    在 Git 冲突时:

    1. M-x vc-resolve-conflicts 或直接 M-x ediff-merge-with-ancestor
    2. 选择 yours/mine/theirs 或手动编辑
    3. 完成后保存并退出
  • 版本控制集成

    • M-x ediff-revision - 比较当前文件与某个版本
    • Magit 中按 e 进入 Ediff 模式比较提交

窗口布局配置

;; 在当前 frame 中显示 Ediff(而非新建 frame)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
 
;; 水平分割窗口(并排显示)
(setq ediff-split-window-function 'split-window-horizontally)
 
;; 垂直分割(上下显示)
(setq ediff-split-window-function 'split-window-vertically)

实用技巧

  1. *批量文件比较*:在 Dired 中标记多组文件,逐个比较
  2. *配合版本控制*:快速查看文件历史变化
  3. *代码审查*:用三向合并解决复杂的合并冲突
  4. *文档对比*:比较不同版本的文档修订

与其他工具对比

工具特点适用场景
EdiffEmacs 内置,功能强大深度集成 Emacs
vimdiffVim 内置,轻量Vim 用户
meld图形化,直观独立 GUI 工具
diff/patch命令行,脚本友好自动化、CI

Dired, the Directory Editor

Resource

emacs#Dired
Emacs From Scratch #10 - Effortless File Management with Dired - YouTube

Insight

dired C-x d 打开 dired buffer
dired-jump C-x C-j SPC o - 跳到当前文件对应的 dired buffer 项

然后和 dired-do-shell-command 配合,我们就可以方便地用其他程序打开当前文件了
目前我觉得比较实用的是用 helix 编辑当前文件,需要运行的是 kitty helix
但是跳转到当前位置这个功能又没有,就有点鸡肋

dired-listing-switches 现在默认是 “-ahl -v —group-directories-first”

出现了三个编辑权限控制层级,在正常的 normal mode 之上还有一个 readonly 模式,保持了 iESC 的切换方式

E 用 xdg-open 打开,用来打开视频之类的
J jump to file

如何排序?
o 可以切换根据 name or date 排序

如何在当前目录下打开命令行?
+vterm/toggle SPC o t
原本就支持这个功能!

Editing the Dired Buffer

这是极其 nb 的功能!

EmacsWiki: WDired

dirvish

国人开发者

默认的 dired 少了一些预览功能,dirvish 弥补了这一点

使用和 yazi 一样的三列设计

视频预览有 ffmpegthumbnailer 的依赖

在 dired buffer 按 F 打开

Minibuffers

elisp#Minibuffers

Completion

elisp#Completion

completing-read
(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)

completing-read-multiple
(completing-read-multiple PROMPT TABLE &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)

第七个 def 就是默认选项

def 是直接传给 read-from-minibuffer
Sixth arg DEFAULT-VALUE, if non-nil, should be a string, which is used
as the default to read if READ is non-nil and the user enters
empty input. But if READ is nil, this function does not return
DEFAULT-VALUE for empty input! Instead, it returns the empty string.

Whatever the value of READ, DEFAULT-VALUE is made available via the
minibuffer history commands. DEFAULT-VALUE can also be a list of
strings, in which case all the strings are available in the history,
and the first string is the default to read if READ is non-nil.

这个 comleting-read-multiple 对 DEF 的支持不太行

Calendar/Diary

Diary

默认情况下这是一个特殊的文件

Advanced Calendar/Dairy Usage

  • Sexp Diary Entries

    emacs#Sexp Diary Entries
    %%(diary-xxx) text 这样的语法

    最初设计用于 ~/diary 文件
    org-mode 借用了这套语法用于 agenda
    只在 org-agenda 中被特殊处理

    date 变量会自动附加到环境中,格式是 (month day year)

    • TODO 如果放在第一个标题前面会报错!!!

      可以绑个 org-roam 解析?

Shell

emacs#Shell
它内置的 term mode 是如何实现的?

Single Shell Commands

shell-command
M-!

shell-command-on-region
M-|
会显示在 minibuffer
evil-shell-command 行为不同

async-shell-comand
M-&

Help

Misc Help

emacs#Misc Help

  • Info 系统

    • 基本操作

      SPC h i info 打开 Info 手册
      J Info-menu 选择菜单项
      s Info-search 搜索当前手册
      i Info-index 索引搜索(最有效)
      g , Info-index-next 下一个索引匹配项

    • 导航

      g j - 下一节点
      g k - 上一节点
      u - 上级节点
      C-o - 返回上次查看的节点
      C-s - 增量搜索
      g L - 查看访问历史记录

    • TODO 发现一个问题 evil-mode 绑到了同一个快捷键 TAB

      按理说应该绑到 C-i 才对

Commands for Fixing Typos

emacs#Fixit

ispell

emacs#Spelling

ispell-word
z =
M-$

ispell-buffer
可能写英语作文的时候有点用处

Mantaining

VC

emacs#Version Control

  • Log Edit Mode

    emacs#Log Buffer
    The major mode for the ‘*vc-log*’ buffer is Log Edit mode, a variant
    of Text mode (*note Text Mode::). On entering Log Edit mode, Emacs runs
    the hooks ‘text-mode-hook’ and ‘vc-log-mode-hook’ (*note Hooks::).

    有什么用?
    可以解析 header (现在可能已经是没用的功能了)

International Character Set Support

emacs#International

mule-util.el

String manipulations while paying attention to multibyte characters.

  • truncate-string-to-width

    • Chat

      下面给你一组常用、可复制的使用示例(注意:END-COLUMN 是“结束列”的绝对值,不是长度;要按显示列区间 [start, end) 取子串,就用 (truncate-string-to-width s end start …))。

      基础(按显示宽度截取)

      • (truncate-string-to-width “你好, hello” 5) => “你好,” ; 2+2+1=5
      • (truncate-string-to-width “你好, hello” 6) => “你好, ”
      • (truncate-string-to-width “你好” 3) => “你” ; 3 落在“好”的中间,不能半个字,结果宽度会小于 3

      指定起始列 START-COLUMN(取中间一段)

      • (truncate-string-to-width “你好, hello” 6 2) => “好, ” ; 取显示列区间 [2, 6)
      • (truncate-string-to-width “你好, hello” 9 4) => ”, hel” ; 取显示列区间 [4, 9)

      PADDING 补齐(避免中间或末尾落在宽字符中间时宽度变窄)

      • (truncate-string-to-width “你” 1 nil ?.) => ”.” ; END 落在“你”的中间,用 ’.’ 补齐
      • (truncate-string-to-width “你好” 3 1 ?-) => “-好” ; START=1 落在“你”中间,前面用 ’-’ 补齐
      • (truncate-string-to-width “hi” 5 nil ?·) => “hi···” ; 原串不够宽,用 ’·’ 补到 5 列

      ELLIPSIS 省略号

      • (truncate-string-to-width “magit-diff: elisp-demos” 18 nil nil t)
        => “magit-diff: eli…” ; 默认省略号,宽度恰好 18
      • (truncate-string-to-width “你好 hello 世界” 8 nil nil ”…“)
        => “你好 h…” ; 先取宽度 8-1=7,再加 “…”

      ELLIPSIS-TEXT-PROPERTY(不改变字符串长度,用 display 属性遮盖)

      • (let ((s (truncate-string-to-width “你好, hello world” 8 nil nil ”…” t)))
        (list s (length s)))
        => 显示出来是“你好…”;(length s) 仍是原串长度(例如 13)

      结合“按显示列范围 [start, end)”包装一个便捷函数

      • (defun substring-by-display-columns (s start end &optional padding ellipsis)
        (truncate-string-to-width s end start padding ellipsis))
      • (substring-by-display-columns “你好, hello” 2 6) => “好, ”
      • (substring-by-display-columns “你好” 1 3 ?-) => “-好”

      一些边界与注意

      • 结果按 Emacs 的显示宽度规则(CJK 宽 2,ASCII 宽 1,组合符、emoji 等通常处理良好)。east-asian-ambiguous-width 会影响某些“模糊宽度”的字符。
      • 若启用了字符组合(composition),宽度可能与单字符相加不完全一致(docstring 已提醒)。
      • 如果你需要“按像素”截取而非按列宽,需要走 window-text-pixel-size 等更底层 API。

Coding Systems

emacs#Coding Systems

Emacs编码总结

Customization

Variables

Maintaining Large Programs

这就是为什么emacs lisp 不需要 lsp

Xref

emacs#Xref

ChangeLog

emacs#Change Log
gnu 竟然还有 changelog 的标准
详见 Change Logs

Outside Manual

smtpmail

smtpmail#Top

treesit.el

Better Syntax Highlighting: treesit vs tree-sitter discrepancy

How to Get Started with Tree-Sitter - Mastering Emacs
这是相当重要的资源

Emacs: Feature/tree-sitter merged into master | Hacker News

doomemacs 现在用的是 elisp-tree-sitter
虽然说有这句话吧,但是不管
#+begin_quote
For Emacs 29+, please use the built-in integration instead of this package.
#+end_quote`

一些常识

下载的 parser 文件放在 ~/.config/emacs/.local/cache/tree-sitter
另外还会在 treesit-extra-load-path 下寻找

so-long-mode

非常惊喜的是,evil-mode 在这个模式下会默认进行 evil-next-lineevil-next-visual-line 的对调

Comprehensive guide on handling long lines in Emacs

message mode

message-mode

写邮件用的

client

Gnus

mu4e

配置

mail host address

Message Mode 概览

compose-mail C-x m(或 C-x 4 m / C-x 5 m)会打开默认的 Message mode 缓冲区 *unsent mail*,在其中编辑头部与正文,C-c C-s/C-c C-c 发送或发送后埋藏缓冲区,因此它是 Emacs 内置邮件写作的核心接口。
(gnu.org (https://www.gnu.org/s/emacs/manual/html_node/emacs/Sending-Mail.html?utm_source=openai))

Message mode 继承 Text mode 的排版/填充行为,并由 Emacs/Gnus 等程序统一调用来写信或回帖,因此大多数常见的文本编辑命令仍然可用。
(gnu.org (https://www.gnu.org/software/emacs/manual/html_node/message/))

进入 Message mode 时会先运行 text-mode-hook,再执行 message-mode-hook 与 message-setup-hook,方便你批量启用拼写、模板等定制逻辑。
(gnu.org (https://www.gnu.org/software/emacs/manual/html_node/emacs/Mail-Misc.html?utm_source=openai))

footnote-mode

footnote-mode

是为 message-mode 做准备的

Using Emacs as a Server

Emacs Server 模式允许你启动一个持久的 Emacs 进程(daemon),然后通过轻量级的 emacsclient 快速打开文件,避免每次启动 Emacs 的开销。

核心概念

  • Daemon: 后台运行的 Emacs 进程,不显示任何窗口
  • emacsclient: 连接到 daemon 的客户端工具
  • Frame: Emacs 窗口实例(GUI 或终端)
  • Socket: daemon 和 client 之间的通信通道

emacs —daemon

  • 基本用法

    # 启动默认 daemon
    emacs --daemon
     
    # 启动命名 daemon(可以同时运行多个)
    emacs --daemon=work
    emacs --daemon=personal
     
    # 前台启动(调试用)
    emacs --fg-daemon
     
    # 后台启动(默认行为)
    emacs --bg-daemon[=NAME]
  • 管理 daemon

    # 查看运行中的 daemon
    sudo ps aux | grep "emacs.*daemon"
     
    # 停止 daemon(从 Emacs 内部)
    M-x kill-emacs
     
    # 停止 daemon(从命令行)
    emacsclient -e "(kill-emacs)"
     
    # 停止指定名称的 daemon
    emacsclient -s work -e "(kill-emacs)"
  • systemd 集成(Linux)

    # ~/.config/systemd/user/emacs.service
    [Unit]
    Description=Emacs daemon
    Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/
     
    [Service]
    Type=notify
    ExecStart=/usr/bin/emacs --fg-daemon
    ExecStop=/usr/bin/emacsclient --eval "(kill-emacs)"
    Environment=SSH_AUTH_SOCK=%t/keyring/ssh
    Restart=on-failure
     
    [Install]
    WantedBy=default.target
    # 启用开机自启
    systemctl --user enable emacs
    systemctl --user start emacs
     
    # 查看状态
    systemctl --user status emacs

emacsclient

  • 完整帮助文档

    Usage: emacsclient [OPTIONS] FILE...
    Tell the Emacs server to visit the specified files.
    Every FILE can be either just a FILENAME or [+LINE[:COLUMN]] FILENAME.
     
    The following OPTIONS are accepted:
    -V, --version		Just print version info and return
    -H, --help    		Print this usage information message
    -nw, -t, --tty, --no-window-system
    			Open a new Emacs frame on the current terminal
    -c, --create-frame    	Create a new frame instead of trying to
    			use the current Emacs frame
    -r, --reuse-frame	Create a new frame if none exists, otherwise
    			use the current Emacs frame
    -F ALIST, --frame-parameters=ALIST
    			Set the parameters of a new frame
    -e, --eval    		Evaluate the FILE arguments as Elisp expressions
    -n, --no-wait		Don't wait for the server to return
    -w, --timeout=SECONDS	Seconds to wait before timing out
    -q, --quiet		Don't display messages on success
    -u, --suppress-output   Don't display return values from the server
    -d DISPLAY, --display=DISPLAY
    			Visit the file in the given display
    --parent-id=ID          Open in parent window ID, via XEmbed
    -s SOCKET, --socket-name=SOCKET
    			Set filename of the UNIX socket for communication
    -f SERVER, --server-file=SERVER
    			Set filename of the TCP authentication file
    -a EDITOR, --alternate-editor=EDITOR
    			Editor to fallback to if the server is not running
    			If EDITOR is the empty string, start Emacs in daemon
    			mode and try connecting again
    -T PREFIX, --tramp=PREFIX
                            PREFIX to prepend to filenames sent by emacsclient
                            for locating files remotely via Tramp
     
    Report bugs with M-x report-emacs-bug.
  • 常用场景与示例

    • 基本文件编辑

      # GUI 窗口中打开文件
      emacsclient -c file.txt
       
      # 终端中打开(类似 vim)
      emacsclient -nw file.txt
      emacsclient -t file.txt
       
      # 重用已有 frame(如果没有则创建)
      emacsclient -r file.txt
       
      # 打开文件并跳转到指定行列
      emacsclient +42 file.txt        # 第 42 行
      emacsclient +42:10 file.txt     # 第 42 行第 10 列
    • 后台操作(不等待)

      # 打开文件后立即返回(不阻塞终端)
      emacsclient -n file.txt
       
      # 在后台执行 elisp 表达式
      emacsclient -n -e "(message \"Hello from command line\")"
       
      # 批量打开文件(不等待)
      emacsclient -n *.org
    • 执行 Elisp 代码

      # 获取当前打开的 buffer 数量
      emacsclient -e "(length (buffer-list))"
       
      # 保存所有 buffer
      emacsclient -e "(save-some-buffers t)"
       
      # 执行复杂操作
      emacsclient -e '
        (progn
          (find-file "/tmp/test.txt")
          (insert "Hello World\n")
          (save-buffer))'
       
      # 获取变量值
      emacsclient -e "user-emacs-directory"
    • 多 daemon 管理

      # 连接到指定名称的 daemon
      emacsclient -s work -c work-file.txt
      emacsclient -s personal -c personal-notes.org
       
      # 使用 socket 文件路径
      emacsclient -s ~/.emacs.d/server/work -c file.txt
       
      # 使用 TCP 认证文件
      emacsclient -f ~/.emacs.d/server/work -c file.txt
    • fallback 策略(-a)

      # 如果 daemon 未运行,使用 vim
      emacsclient -a vim file.txt
       
      # 如果 daemon 未运行,自动启动 daemon 并重试
      emacsclient -a "" -c file.txt
       
      # 如果 daemon 未运行,使用普通 Emacs
      emacsclient -a "emacs" file.txt
    • 与其他工具集成

      # 设置为 Git 默认编辑器
      git config --global core.editor "emacsclient -t"
       
      # 设置为环境变量
      export EDITOR="emacsclient -t"
      export VISUAL="emacsclient -c"
       
      # sudoedit 集成
      export SUDO_EDITOR="emacsclient -t"
       
      # 作为 diff 工具
      git config --global diff.tool emacsclient
      git config --global difftool.emacsclient.cmd \
        'emacsclient -c --eval "(ediff-files \"$LOCAL\" \"$REMOTE\")"'

在 Emacs 内部启动 server

如果你已经启动了常规 Emacs 实例,可以手动启动 server:

;; 启动 server(如果未运行)
(require 'server)
(unless (server-running-p)
  (server-start))
 
;; 或者直接
M-x server-start
 
;; 停止 server
M-x server-force-delete
 
;; 配置:自动启动 server
(add-hook 'after-init-hook #'server-start)

实用技巧与最佳实践

  • shell 别名

    # ~/.bashrc 或 ~/.zshrc
    alias ec='emacsclient -nw'           # 终端编辑
    alias ecc='emacsclient -c'           # GUI 编辑
    alias ecn='emacsclient -n'           # 后台打开
    alias e='emacsclient -t -a ""'       # 自动启动 daemon
  • 文件管理器集成

    # 添加到右键菜单(以 Thunar 为例)
    # ~/.local/share/applications/emacsclient.desktop
    [Desktop Entry]
    Name=Open in Emacs
    Exec=emacsclient -c %F
    Terminal=false
    Type=Application
    MimeType=text/plain;
  • 性能优化

    • 使用 daemon 可以将启动时间从 1-2 秒降至 50-100 毫秒
    • 预加载常用包:在 daemon 启动时加载,而非打开文件时加载
    • 使用命名 daemon 隔离不同工作环境(避免包冲突)
  • 调试技巧

    # 查看 daemon 输出日志
    tail -f ~/.emacs.d/server/server.log
     
    # 前台启动 daemon(查看错误)
    emacs --fg-daemon
     
    # 测试连接
    emacsclient -e "t"  # 应该返回 t

常见问题

  • daemon 未启动

    # 检查是否有 daemon 运行
    ps aux | grep "emacs.*daemon"
    emacsclient -e "t"  # 如果失败说明未运行
     
    # 手动启动
    emacs --daemon
  • Socket 权限问题

    # Socket 位置(默认)
    ls -la /tmp/emacs$(id -u)/
     
    # 删除损坏的 socket
    rm -rf /tmp/emacs$(id -u)/server
  • 多个 Emacs 版本

    # 指定 Emacs 版本启动 daemon
    /usr/local/bin/emacs --daemon=emacs29
     
    # 连接时也指定 socket
    emacsclient -s emacs29 -c

Doom Emacs 集成

Doom 提供了便捷的 daemon 管理命令:

# 启动 Doom daemon
doom run
 
# 重启 daemon
doom restart-server
 
# emacsclient 别名(Doom 自带)
doom +everywhere  # 配置 emacsclient 为默认编辑器

winner.el

winner-mode

Winner mode is a global minor mode that records the changes in
the window configuration (i.e. how the frames are partitioned
into windows) so that the changes can be “undone” using the
command winner-undo. By default this one is bound to the key
sequence \C-c <left>. If you change your mind (while undoing),
you can press \C-c <right> (calling winner-redo).

eieio.el

/usr/local/share/emacs/31.0.50/lisp/emacs-lisp 目录里是放什么东西的?

/usr/local/share/emacs/31.0.50/lisp/emacs-lisp/ 这个目录里放的是 Emacs 自带的、用 Emacs Lisp 写的一部分“核心库”,主要是一些比较通用、底层的 Lisp 支持代码,比如:

  • 各种 Emacs Lisp 语言本身相关的工具
    • 字节编译器(=bytecomp.el= 等)
    • 宏展开、调试、解释器辅助等
  • Lisp 开发/调试辅助工具
    • eldoc.el=、=checkdoc.el
  • 通用的 Lisp 数据结构/算法/实用函数
    • 比如映射、哈希表、序列操作等的扩展
  • Emacs 自身运行所需要的一些基础 Lisp 文件

简单说:这是 Emacs 官方发行版中,和“Emacs Lisp 语言本身及其工具”相关的标准库所在目录,而不是用户配置或第三方包的目录。用户自己的配置通常在 ~/.emacs.d/~/.config/emacs/ 等地方。

eieio.el 主要是用来干什么的?

eieio.el 是 Emacs 里提供 面向对象编程(OOP)机制 的核心库,相当于 “Emacs Lisp 版的 CLOS/小型类系统”。它的主要作用有:

  1. 定义类和对象

    • defclass 定义类(包含槽/slot,相当于字段/属性)
    • make-instance 创建对象实例
    • 支持继承(单继承、多继承)、默认值、类型约束等
  2. 定义方法(generic functions)

    • defmethod 为不同类定义同名方法(多重派发)
    • 支持基于参数类型(类)的动态分派,类似 Common Lisp 的 CLOS
  3. 封装与抽象

    • 可以把数据和操作封装成一个类,对外只暴露方法接口
    • 便于构建较大、结构清晰的 Emacs 扩展/插件
  4. 与 Emacs 生态集成

    • CEDET、EDE、Semantic 等复杂包大量使用 eieio
    • 许多新一点的包如果用到 OOP 结构,也会依赖它

简单理解:
eieio.el = 在 Emacs Lisp 里提供 “类 + 对象 + 方法 + 继承” 这一套面向对象编程工具,使得写大型、结构化的 Emacs 扩展更方便。

Emacs Packages

emacs#Packages
这部分是在太大,就不放到 manual 章节里了

包管理方案

Emacs 的包管理经历了从手动管理到现代化自动安装的演进过程。

包源(Package Archives)

内置包管理器 (package.el)

从 Emacs 24 开始内置,提供基本的包安装、更新、删除功能。

  • 基本操作

    ;; 配置包源
    (require 'package)
    (setq package-archives
          '(("gnu"    . "https://elpa.gnu.org/packages/")
            ("nongnu" . "https://elpa.nongnu.org/nongnu/")
            ("melpa"  . "https://melpa.org/packages/")))
     
    ;; 初始化包系统
    (package-initialize)
     
    ;; 更新包列表
    (unless package-archive-contents
      (package-refresh-contents))
     
    ;; 安装包
    (unless (package-installed-p 'use-package)
      (package-install 'use-package))
  • 常识

    读取 ELPA/MELPA 等仓库元数据,把每个包解压到 ~/.emacs.d/elpa/PACKAGE-VERSION/,字节编译 .el 并加进 load-path。
    它通过 package-alist 记录已装版本,可在 list-packages 里升级/卸载。

  • 常用命令

    • M-x package-list-packages - 列出所有可用包(类似 apt/yum 的界面)
    • M-x package-install - 安装指定包
    • M-x package-delete - 删除已安装的包
    • M-x package-refresh-contents - 刷新包索引
    • M-x package-autoremove - 删除未被依赖的包
  • 优缺点

    优点:

    • 内置,无需额外配置
    • 简单直观
    • 支持包依赖自动安装

    缺点:

    • 没有声明式配置
    • 不保证可重现性(版本可能不一致)
    • 包更新时可能破坏配置
    • 无法方便地管理包的启用/禁用

use-package - 声明式包配置

由 John Wiegley 开发(Emacs 29+ 内置),是目前最流行的包配置方式。

  • 核心理念

    • 声明式:用一个宏描述包的安装、加载、配置、按键绑定
    • 延迟加载:只在真正需要时加载包,提升启动速度
    • 统一接口:把分散的配置集中到一处
  • 常识

    只是配置宏,不负责下载;搭配 :ensure t 时调用 package.el 安装。
    其他关键字(:bind、:config 等)用于组织加载逻辑。

  • 基本用法

    (use-package magit
      :ensure t                    ; 自动安装(如果未安装)
      :defer t                     ; 延迟加载
      :bind (("C-x g" . magit-status))  ; 按键绑定
      :config                      ; 包加载后执行
      (setq magit-diff-refine-hunk 'all))
     
    (use-package org
      :mode ("\\.org\\'" . org-mode)    ; 文件关联
      :hook (org-mode . visual-line-mode)  ; 钩子
      :custom                             ; 自定义变量
      (org-startup-indented t)
      (org-hide-leading-stars t))
  • 高级特性

    ;; 条件加载
    (use-package pdf-tools
      :if (display-graphic-p)
      :ensure t
      :config (pdf-tools-install))
     
    ;; 加载本地包(不从 ELPA 安装)
    (use-package my-custom-pkg
      :load-path "~/.emacs.d/lisp/")
     
    ;; 按需触发加载
    (use-package company
      :commands company-mode
      :init
      (add-hook 'prog-mode-hook #'company-mode))
     
    ;; 包依赖(确保先安装/加载依赖)
    (use-package lsp-ui
      :after lsp-mode
      :ensure t)
  • 性能优化

    ;; 延迟 N 秒后加载(适用于非紧急的包)
    (use-package dashboard
      :ensure t
      :defer 2)
     
    ;; 空闲时加载
    (use-package which-key
      :ensure t
      :defer 3
      :diminish which-key-mode
      :config (which-key-mode))

package-vc - 内置的 VC 包管理器(Emacs 29+)

Emacs 29 引入的官方解决方案,允许直接从 Git/VC 仓库安装包,无需依赖第三方工具。

  • 核心特性

    • 内置于 Emacs 29+,无需额外配置
    • 直接从 Git 仓库克隆和安装包
    • 支持从 ELPA 的包规格(package specification)安装
    • 与 package.el 共享基础设施
    • 比 straight.el 更轻量,但功能相对简单
  • 基本用法

    • 从 Git URL 安装

      交互式安装
      M-x package-vc-install RET https://github.com/user/repo.git RET

      ;; 在代码中安装
      (package-vc-install "https://github.com/magit/magit")
       
      ;; 指定包名(如果仓库名和包名不一致)
      (package-vc-install "https://github.com/foo/bar" nil nil 'my-package-name)
    • 从 ELPA 包规格安装

      从 NonGNU/GNU ELPA 的 VC 元数据安装
      M-x package-vc-install-from-checkout RET package-name RET

      例如:安装 org-mode 的开发版

      (package-vc-install 'org)
    • 指定分支(或 commit)

      ;; 使用特定分支
      (package-vc-install
       '(my-package
         :url "https://github.com/user/my-package"
         :branch "develop"))

      如果你想锁定到特定 commit,需要手动操作:

      1. 先正常安装包:
           (package-vc-install "https://github.com/user/my-package")
      2. 然后手动进入包的目录并切换 commit:
           cd ~/.emacs.d/elpa/my-package-0/
           git checkout <specific-commit-hash>
      3. (可选)锁定,防止被 package-vc-upgrade 更新:
           # 创建一个不跟踪远程的本地分支
           git checkout -b locked-version <commit-hash>
  • 配置与管理

    • 声明式配置(use-package 集成)

      Emacs 30+ 的 use-package 原生支持 :vc 关键字:

      ;; Emacs 30+ 的语法
      (use-package foo
        :vc (:url "https://github.com/user/foo"
             :branch "main"))
       
      ;; 从 ELPA 规格安装
      (use-package org
        :vc t)  ; 自动使用 ELPA 的 VC 元数据
       
      ;; 指定更多选项
      (use-package my-package
        :vc (:fetcher github
             :repo "user/my-package"
             :rev "feature-branch"))
    • 更新 VC 包

      更新单个包
      M-x package-vc-upgrade RET package-name RET

      更新所有 VC 安装的包
      M-x package-vc-upgrade-all

      ;; 在代码中更新
      (package-vc-upgrade 'my-package)
      (package-vc-upgrade-all)
    • 查看已安装的 VC 包

      在 package-list-packages 中,VC 包会标记为 “vc”
      M-x package-list-packages

      ;; 检查某个包是否是 VC 安装的
      (package-vc-p 'my-package)  ; 返回 t 或 nil
       
      ;; 查看 VC 包的元数据
      (alist-get 'my-package package-vc-selected-packages)
  • 实际使用示例

    • 场景 1:安装 GitHub 上的未发布包

      ;; 某个包还未发布到 MELPA
      (package-vc-install "https://github.com/author/new-package")
    • 场景 2:使用包的开发版修复 bug

      ;; 先卸载 MELPA 版本
      (package-delete (cadr (assq 'org-roam package-alist)))
       
      ;; 安装 Git 最新版
      (package-vc-install "https://github.com/org-roam/org-roam")
    • 场景 3:贡献包时测试本地修改

      # 克隆到本地
      cd ~/.emacs.d/
      git clone https://github.com/user/package package-dev
       
      # 在 Emacs 中从本地目录安装
      (package-vc-install-from-checkout "~/.emacs.d/package-dev" 'package)
  • 与 straight.el 对比

    特性package-vcstraight.el
    内置Emacs 29+ 内置需要额外安装
    版本锁定不支持(无 lockfile)支持(straight-freeze)
    本地修改支持完全支持
    MELPA 集成通过 ELPA 规格直接使用 MELPA recipes
    复杂度简单较复杂
    配置重现性弱(无版本锁定)强(lockfile + Git commit)
    fork 和 patch 支持基本支持强大支持
    启动性能与 package.el 相当略慢(需要检查 Git 状态)
  • 适用场景

    推荐使用 package-vc 的情况:

    • Emacs 29+ 用户,想要简单的 Git 包安装
    • 偶尔需要测试包的开发版
    • 不需要严格的版本锁定
    • 想要内置方案,避免第三方依赖

    推荐使用 straight.el 的情况:

    • 需要可重现的环境(团队协作、多机器同步)
    • 频繁使用 fork/本地修改
    • 需要精确控制每个包的版本
    • 习惯声明式配置并追求完全自动化
  • 常见问题

    • package-vc 安装的包位置

      ;; VC 包安装在与 package.el 相同的目录
      ;; ~/.emacs.d/elpa/package-name-0/  (注意版本号为 0)
       
      ;; 查看某个 VC 包的路径
      (package-desc-dir (cadr (assq 'my-package package-alist)))
    • 如何切换回 MELPA 版本

      ;; 1. 删除 VC 安装的包
      M-x package-delete RET my-package RET
       
      ;; 2. 刷新包列表并从 MELPA 安装
      M-x package-refresh-contents
      M-x package-install RET my-package RET
    • package-vc 能否替代 straight.el?

      部分场景可以,但不是完全替代:

      • 如果只是偶尔从 Git 安装包,package-vc 足够
      • 如果需要精确的版本管理和团队协作,straight.el 仍是更好选择
      • package-vc 更像是 “package.el + Git clone”,而 straight.el 是完整的包管理生态
  • 最佳实践

    ;; 1. 大部分包仍用 package.el + MELPA
    (setq package-archives
          '(("gnu"    . "https://elpa.gnu.org/packages/")
            ("nongnu" . "https://elpa.nongnu.org/nongnu/")
            ("melpa"  . "https://melpa.org/packages/")))
     
    ;; 2. 少数包需要开发版时用 package-vc
    (use-package org
      :vc t)  ; 使用 Org 的开发版
     
    (use-package my-experimental-package
      :vc (:url "https://github.com/author/experimental"))
     
    ;; 3. 定期更新 VC 包(不像 MELPA 每天自动更新)
    ;; 可以在 init.el 中添加:
    (when (fboundp 'package-vc-upgrade-all)
      ;; 每周检查一次更新(可选)
      (run-with-idle-timer (* 7 24 60 60) nil #'package-vc-upgrade-all))

straight.el - 现代包管理器

由社区开发的下一代包管理器,解决 package.el 的痛点。

  • 核心优势

    1. 可重现性:锁定包的 Git commit,确保所有机器上版本一致
    2. 直接从源码安装:绕过 ELPA,直接从 GitHub/GitLab 克隆
    3. 支持本地修改:可以 fork 包并使用自己的版本
    4. 与 use-package 完美集成
  • 基本安装

    ;; Bootstrap straight.el
    (defvar bootstrap-version)
    (let ((bootstrap-file
           (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
          (bootstrap-version 6))
      (unless (file-exists-p bootstrap-file)
        (with-current-buffer
            (url-retrieve-sync
             "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
             'silent 'inhibit-cookies)
          (goto-char (point-max))
          (eval-print-last-sexp)))
      (load bootstrap-file nil 'nomessage))
     
    ;; 集成 use-package
    (straight-use-package 'use-package)
    (setq straight-use-package-by-default t)
  • 使用示例

    ;; 从 MELPA 配方安装
    (use-package magit)
     
    ;; 从指定 Git 仓库安装
    (use-package some-package
      :straight (:host github :repo "user/repo"))
     
    ;; 使用本地 fork
    (use-package my-forked-package
      :straight (:local-repo "~/projects/my-fork"))
     
    ;; 锁定到特定 commit
    (use-package org
      :straight (:type built-in :ref "release_9.6"))
  • 版本锁定与重现

    生成版本锁定文件
    M-x straight-freeze-versions
    写入 ~/.emacs.d/straight/versions/default.el

    在新机器上恢复相同版本
    M-x straight-thaw-versions

Borg - Git 子模块方式

极简主义者的选择,把所有包作为 Git submodules 管理。

  • 理念

    • ~/.emacs.d 本身作为 Git 仓库
    • 每个包是一个 submodule
    • 利用 make 编译字节码
    • 没有”包管理器”,全手动控制
  • 适用人群

    • 喜欢完全控制依赖的开发者
    • 习惯 Git 工作流
    • 配置较稳定,不频繁尝试新包

quelpa - 从任意源构建包

允许从任意 Git 仓库、本地目录、URL 构建 ELPA 风格的包。

;; 安装 quelpa
(use-package quelpa
  :ensure t)
 
;; 从 GitHub 安装未在 ELPA 上的包
(quelpa '(some-package :fetcher github :repo "user/some-package"))
 
;; 从本地目录构建
(quelpa '(my-package :fetcher file :path "~/my-elisp/my-package.el"))

el-get - 老牌第三方包管理器

历史悠久但现已较少使用,支持多种源(Git、ELPA、GitHub、Emacswiki 等)。

Cask - 项目级依赖管理

类似 npm 的 package.json=,用 =Cask 文件声明依赖。

;; Cask 文件示例
(source gnu)
(source melpa)
 
(depends-on "magit")
(depends-on "company")
(depends-on "flycheck")

适合插件开发者管理测试依赖,普通用户较少使用。

预配置发行版的包管理

  • Doom Emacs

    • 使用 straight.el 作为后端
    • 通过 packages.el 声明包
    • doom sync 安装/更新
    • 模块化设计,自动处理依赖
    ;; ~/.doom.d/packages.el
    (package! some-package)
    (package! another-package :pin "commit-hash")
     
    ;; 禁用内置包
    (package! built-in-package :disable t)
  • Spacemacs

    • 基于 package.el
    • 通过 layer(功能层)组织包
    • dotspacemacs/layers 声明启用的 layer

包管理方案对比

方案优点缺点适用人群
package.el内置、简单不可重现、功能有限新手
use-package声明式、延迟加载、流行依赖 package.el 的缺点大多数用户
straight.el可重现、直接源码、版本锁定初次下载慢、概念稍复杂进阶用户、团队
Borg极简、完全控制手动维护成本高极简主义者
quelpa支持任意源小众、文档少特殊需求
Doom/Spacemacs开箱即用、社区配置抽象层多、定制受限想快速上手的用户

Completion in Emacs

corfu.el and company-mode

Code Completion In Emacs - Everything You Need To Know - YouTube

Diff in Emacs

diff-mode

Ediff
smerge-mode

magit 中的组件

emacs 中有什么主流(强大)的文件比较解决方案?需要支持目录比较

主流且强大的方案(含目录比较):

  • Ediff(内置,最强大)

    • 文件/缓冲区/区域/三方合并/VC 版本均支持。
    • 目录比较:M-x ediff-directories(两方),M-x ediff-directories3(三方)。按 n/p 逐文件浏览差异。
    • 优点:对比/合并体验好,内置稳定;可与 VC 集成。
    • 小贴士:避免弹出新 frame
      • (setq ediff-window-setup-function ‘ediff-setup-windows-plain
        ediff-split-window-function ‘split-window-horizontally)
  • Dired 比较(内置,轻量)

    • 两个 Dired buffer 上:M-x dired-compare-directories 标记差异/独有文件。
    • 单文件比较:在两个 Dired 窗口中用 =(dired-diff)比较同名文件。
    • 优点:操作直观;适合快速筛差异与批量操作。
  • Ztree(包,目录树式可视化)

    • M-x ztree-diff 递归比较两目录,树状展示,支持过滤、同步浏览、可调用 Ediff 进入文件级 diff。
    • 优点:目录级对比体验最佳;适合大型树。
  • Dirdiff(包,递归目录比较)

    • 传统方案,递归比较并支持复制/同步。
    • 优点:专注目录同步;界面朴素但实用。
  • Magit(Git 场景)

    • 仓库/子目录差异、提交/分支比较;M-x magit-ediff-dwim 可在变更处用 Ediff 深度对比。
    • 优点:版本控制下最强工作流。
  • Vdiff(包,文件级现代 diff UI)

    • vdiff、vdiff-magit;更易读的 side-by-side 界面。
    • 说明:偏文件/补丁级,对目录需配合其他工具。

推荐:

  • 仅用内置:Ediff + Dired 已可满足绝大多数文件/目录比较。
  • 要目录树可视化与递归:用 Ztree。
  • 在 Git 项目中:Magit + magit-ediff 搭配 Ediff。

My Emacs packages

majutsu
neoscroll.el
cp-cpp.el

如何从源码编译 Emacs

configure

我当前使用的编译选项

./configure -C --with-pgtk --with-xft --with-harfbuzz \
    --with-cairo --with-imagemagick --with-webp --with-rsvg --with-sqlite3 \
    --with-sound-alsa --with-tree-sitter --with-native-compilation

make

make -j$(nproc)

install

sudo make install

Others’ Emacs Config

doomemacs

spacemacs

Steve Purcell’s config

他的配置貌似是非常有名的,有 7k 的 star

Emacs Writing Studio

作家的 emacs 配置,放在这里只是说有这种东西

Nasy Emacs

Nasy Emacs 配置

如何对 Emacs 做出贡献?

emacs 的开发流程是怎样的? chat

好的,我来详细解释一下 Emacs 的开发流程。Emacs 是一个 GNU 项目,由全球开发者协作维护,核心通过 GNU SavannahGNU ELPA 等平台,以及邮件列表来进行协作,而不是像很多项目一样以 GitHub Issues/PR 为主。

1. 代码管理

  • 代码仓库 :Emacs 的主代码仓库托管在 Savannah

      https://savannah.gnu.org/projects/emacs/

    Git 仓库地址:

      https://git.savannah.gnu.org/git/emacs.git
  • 开发者通过 Git 克隆代码进行修改。GNU 联系人/维护者会负责审核合并。

2. 开发沟通与提交流程

  1. 邮件列表沟通

    • 开发中的讨论主要通过邮件列表进行:
      • =[email protected]=:开发者邮件列表(主要代码、设计讨论)
      • =[email protected]=:提交 bug 报告和补丁
      • 还有 emacs-tangents=、=info-gnu-emacs 等列表
    • 没有 GitHub Issue,所有问题追踪依赖邮件和 Debbugs(GNU 的 Bug
      跟踪系统)。
  2. 提交流程

    • 普通贡献者:
      • 在本地修改代码
      • 使用 git format-patch 生成补丁
      • 发送邮件到相关邮件列表
      • 维护者或有 commit 权限的开发者会在邮件中 review
    • 有 commit 权限的开发者可以直接在 master 分支提交(遵守 GNU commit
      policy)。

3. 补丁审查

  • 核心通过邮件审查代码(patch review)。

  • 补丁和 bug 通过 GNU 的 Debbugs 系统管理:

      https://debbugs.gnu.org/

    每封补丁邮件会自动形成一个 issue 编号(bug)。

4. 分支与发布流程

  • 主分支(master) 通常用于下一个 major/minor 版本的开发。
  • emacs-x.y 分支 用于当前稳定版本的维护(bug fix / 安全更新)。
  • 发布时会:
    1. 冻结功能(feature freeze)

    2. 进入 bug 修复阶段

    3. 预发布版本 (x.y-rc) 发布

    4. 最终发布到 GNU FTP:

           https://ftp.gnu.org/gnu/emacs/

5. 外部包(ELPA/MELPA)

  • GNU ELPA 部分 Emacs 官方支持的包通过 GNU ELPA 发布。GNU ELPA
    仓库也在 Savannah:

      https://git.savannah.gnu.org/git/emacs/elpa.git

    这些包和 Emacs 有较严格的版权要求(必须 FSF copyright assignment)。

  • 非官方(MELPA 等) 社区自行维护,直接从 GitHub/GitLab
    等抓取源码构建。

6. FSF 的版权分配要求

  • Emacs 作为 GNU 核心项目,要求核心代码的贡献者将版权分配(copyright
    assignment)给自由软件基金会(FSF)。
  • 意味着在你的补丁可以被合并进 Emacs
    主仓库之前,必须先完成签署协议(这可以通过邮件或电子签名完成)。

7. 一个典型的贡献流程

假设你发现了 Emacs 中的一个 bug:

  1. 先在本地用 M-x report-emacs-bug 提交报告(会发送到 =bug-gnu-emacs=)。
  2. 开发邮件列表中会有人跟进讨论。
  3. 你准备好了补丁后: bash git format-patch HEAD^
  4. 把生成的 .patch 文件作为附件发到对应 bug 的邮件线程中。
  5. 经过 review 后,有 commit 权限的维护者会将补丁合并到合适的分支。
  6. 后续会自动进入 nightly 构建和下一个版本发布中。

8. 关键特点总结

  • 没有 GitHub PR/Issue ,主要通过邮件和 GNU Debbugs 系统进行。
  • 版权转让要求 :对核心代码贡献必须先签 FSF 的版权协议。
  • 审查以邮件为中心,patch 流程是通过邮件发送的。
  • 发布分为 master (开发)和 emacs-x.y (维护)分支,稳定性优先。
  • 社区协作文化偏向邮件讨论,强调长期维护与 GNU 哲学。

emacs-devel

emacs-devel Archives

如何向别人推荐 Emacs?

对文科生

思路:先别谈“黑客神器”,而是把 Emacs 当作“面向写作与研究的纸和笔”。

打动点(贴近文科生日常)

  • 写长文:大纲—段落—脚注一体化(Org-mode),随时折叠 / 展开结构,专注排版最后再做。
  • 引用与文献:对接 Zotero(citar / oc-csl / BetterBibTeX),一键插入引用,导出时自动格式化文献表。
  • 笔记网络:Zettelkasten / 卡片盒(org-roam),把读书笔记自然地互相链接。
  • 阅读—标注—回收:在 Emacs 里看 PDF(pdf-tools),用 org-noter 把书页和摘录对齐到笔记里。
  • 导出给导师:同一份源文件导出为 Word(docx) / PDF / 幻灯(Pandoc / LaTeX / Reveal),版式统一、可回溯。
  • 时间与写作进度:待办、日程与番茄(Org agenda),论文拆解为任务,避免拖延。
  • 可持续与可移植:纯文本、免费、跨平台、离线、隐私友好,不被某个平台锁定。

10 分钟演示脚本(最能“见效”的环节)

  1. 大纲写作:在 test.org 里用标题、列表、脚注写一段;用 Tab 折叠 / 展开;M-RET 新建同级条目。
  2. 任务与日程:把某条目标记为 TODO,设定截止,打开 Agenda 看周视图。
  3. 引用:从 Zotero 选一条文献插入 cite,导出为 docx,看到格式化的参考文献表。
  4. PDF 标注回收:在论文 PDF 上高亮,自动生成对应的 org 笔记。
  5. 链接知识:在另一个笔记里引用刚才的概念,展示反向链接图谱(org-roam)。

极简入门(不折腾也能用)

  • 安装 Emacs 与 Pandoc、Zotero(+BetterBibTeX)。
  • 只学这几招:
    • 打开 / 保存:C-x C-f / C-x C-s;撤销:C-/;命令:M-x
    • Org 常用:Tab 折叠;S-Tab 全局折叠;M-RET 新条目;C-c C-t 切换 TODO;C-c C-s 日程;C-c C-d 截止;C-c C-e 导出
    • 搜索:C-s;取消:C-g
  • 用内置包即可开始:Org-mode、Org agenda;需要时再加:
    • 引用:citar 或 oc-csl(配 Zotero / CSL)
    • 笔记网络:org-roam
    • PDF:pdf-tools、org-noter
    • 写作体验:olivetti(沉浸)、ispell / 飞书词典或本地拼写、writegood

懒人方案(开箱即用)

  • 装 Doom Emacs 或 Spacemacs,启用 org、pdf、spell、completion 模块;自带更友好的菜单和搜索。

坦诚预期

  • 学习曲线有,但只需先掌握十来个快捷键;随时可导出成 Word 给合作者;不想折腾时保持极简配置即可。
  • 不推荐:只用协作文档、从不做本地资料管理,或完全排斥快捷键的人。

一句话总结

  • 如果你每天要读文献、做笔记、写长文并与文献管理打交道,Emacs(尤其 Org-mode)能把这整条链路打通,并且稳、快、可持续。

Footnotes

  1. 解释:
    用几个具体宏来说明为什么 Edebug 需要“额外信息”(debug 规范)才能正确插桩。

    • 条件求值的宏(什么时候求值?)

        (defmacro my-when (test &rest body)
          (declare (debug (form body)))
          `(if ,test (progn ,@body)))

      只在 test 为真时才会求值。规范告诉 Edebug:第 1 个参数是一个待求值的 form,后面是一个 body(零个或多个 form)。
      否则 Edebug 可能不在 body 里插桩,导致你无法单步调试其中的 (print …) 等。
      body 只在 test 为真时才会求值。规范告诉 Edebug:第 1 个参数是一个待求值的 form,后面是一个 body(零个或多个 form)。
      否则 Edebug 可能不在 body 里插桩,导致你无法单步调试其中的 (print …) 等。

    • 求值次数的宏(会被求值几次?)

        (defmacro twice (expr)
          (declare (debug (form)))
          `(progn ,expr ,expr))

      这里 expr 被求值两次。规范至少让 Edebug 给 expr 插桩,否则你看不到它被重复求值的过程(比如副作用会执行两次)。

    • 延迟求值的宏(稍后才求值)

        (defmacro later (expr)
          (declare (debug (form)))
          `(lambda () ,expr))

      expr 不在宏调用时执行,而是被包进 lambda,可能很久以后才 funcall。如果没有规范且 edebug-eval-macro-args 还是默认 nil,Edebug 不会给 expr 插桩;当你日后调用 (funcall (later (message “hi”))) 时,message 里就无法单步调试。

    • 宏体内手动 eval(显式求值)

        (defmacro eval-now (form)
          (declare (debug (form)))
          `(eval ,form))

      这里参数通过 eval 立即求值。规范告诉 Edebug:form 是需要插桩的可求值表达式,否则 form 里的逻辑都看不到。

    要点:宏可以选择不求值、条件求值、延迟求值或多次求值参数,Edebug无法“先验”知道这些。通过 declare (debug …) 或 def-edebug-spec 把“哪些参数是 form、会不会重复、是否是 body”告诉 Edebug,它才能在正确位置插入调试钩子并让你单步观察整个求值
    过程。