name: emacs-lisp description: | 专家指导编写专业的Emacs Lisp代码,遵循Bozhidar Batsov的社区驱动Emacs Lisp风格指南。涵盖布局、命名约定、语法偏好、宏、文档和最佳实践。
使用此技能当编写Emacs配置、创建Emacs包、编写交互命令、开发主/次模式或重构Emacs Lisp代码时。
强调:词法绑定、正确命名、文档标准、自动加载和社区公约。 allowed-tools:
- Read
- Edit
- Write
- Bash
- Grep
- Glob
Emacs Lisp 风格指南技能
此技能提供专家指导,用于编写专业、可维护的Emacs Lisp代码,遵循社区驱动的Emacs Lisp风格指南。
何时使用此技能
使用此技能当:
- 编写Emacs配置文件(~/.emacs.d/init.el)
- 创建Emacs包或库
- 开发主或次模式
- 编写交互命令和函数
- 创建键绑定和钩子
- 重构现有Emacs Lisp代码
- 设置包元数据和自动加载
- 编写Emacs Lisp宏
- 创建自定义变量和面孔
核心原则
1. 源代码布局
缩进和间距
使用空格,永不使用制表符:
;; 好 - 使用空格缩进
(defun my-function (arg1 arg2)
(let ((result (+ arg1 arg2)))
(message "Result: %s" result)))
;; 坏 - 混合制表符和空格(永不这样做)
垂直对齐函数参数:
;; 好 - 参数对齐
(defun long-function-name (arg1
arg2
arg3)
(body))
;; 好 - 第一个参数在新行,与函数名对齐
(defun long-function-name
(arg1 arg2 arg3)
(body))
;; 坏 - 不一致的对齐
(defun long-function-name (arg1
arg2
arg3)
(body))
特殊形式缩进:
;; 特殊形式使用4空格缩进特殊参数,2空格缩进主体
;; 好 - if 使用正确缩进
(if some-condition
(do-something) ; 4空格用于 then-子句
(do-else)) ; 2空格用于 else-子句
;; 好 - let 绑定
(let ((var1 value1)
(var2 value2)) ; 4空格用于绑定
(body-form-1) ; 2空格用于主体
(body-form-2))
;; 好 - when(无特殊参数)
(when condition
(do-something)
(do-more))
最大行长度:
;; 在合理情况下,目标为每行80字符
;; 逻辑地换行长行
;; 好
(defun my-function ()
"简短描述。"
(let ((long-variable-name
(some-function-that-returns-value)))
long-variable-name))
;; 可接受用于长字符串
(message "这是一个非常长的消息,超过80字符,但保持在一行以提高可读性")
括号:
;; 好 - 文本和开括号之间有空格
(defun my-function (arg)
(list arg))
;; 坏 - 函数名后无空格
(defun my-function(arg)
(list arg))
;; 好 - 所有闭括号在同一行
(defun my-function ()
(when condition
(let ((x 1))
(+ x 2))))
;; 坏 - 闭括号在单独行
(defun my-function ()
(when condition
(let ((x 1))
(+ x 2)
)
)
)
垂直空白:
;; 好 - 顶级形式之间有空行
(defvar my-var1 "value1")
(defvar my-var2 "value2")
(defun my-function ()
"做某事。")
;; 例外:相关定义可以分组
(defvar my-var1 "value1")
(defvar my-var2 "value2")
(defvar my-var3 "value3")
2. 语法和惯用法
偏好高级构造
使用 when 代替单分支 if:
;; 好
(when condition
(do-something)
(do-more))
;; 坏
(if condition
(progn
(do-something)
(do-more)))
使用 unless 用于否定条件:
;; 好
(unless condition
(do-something))
;; 坏
(when (not condition)
(do-something))
;; 坏
(if (not condition)
(do-something))
使用 not 代替 null:
;; 好 - 检查 nil/假
(not value)
;; 好 - 专门检查空列表
(null my-list)
;; 坏 - 使用 null 用于一般 nil 检查
(null value)
利用比较函数:
;; 好 - 直接比较
(< 5 x 10)
;; 坏 - 嵌套条件
(and (> x 5) (< x 10))
;; 好 - 多个比较
(= x y z)
;; 坏 - 嵌套比较
(and (= x y) (= y z))
使用 cond 以 t 作为捕获全部:
;; 好
(cond
((eq x 'foo) (handle-foo))
((eq x 'bar) (handle-bar))
(t (handle-default)))
;; 坏 - 使用 else 在末尾
(cond
((eq x 'foo) (handle-foo))
((eq x 'bar) (handle-bar))
(else (handle-default)))
增量和减量:
;; 好
(1+ x)
(1- x)
;; 坏
(+ x 1)
(- x 1)
偏好 with-eval-after-load:
;; 好 - 现代方法
(with-eval-after-load 'company
(setq company-idle-delay 0.2))
;; 可接受但较旧
(eval-after-load 'company
'(setq company-idle-delay 0.2))
3. 命名约定
通用命名规则
使用烤肉串式(Lisp式):
;; 好
(defun my-function-name ()
(let ((my-variable 10))
my-variable))
;; 坏 - 使用蛇形命名法
(defun my_function_name ()
(let ((my_variable 10))
my_variable))
;; 坏 - 使用驼峰命名法
(defun myFunctionName ()
(let ((myVariable 10))
myVariable))
库名前缀:
;; 对于名为 'my-package' 的库,前缀所有公共符号
(defun my-package-do-something ()
"公共函数。")
(defvar my-package-option t
"公共变量。")
;; 私有函数使用双连字符
(defun my-package--internal-helper ()
"私有函数。")
;; projectile 包的示例
(defun projectile-find-file ()
"公共命令。")
(defun projectile--get-project-root ()
"私有助手。")
未使用变量:
;; 好 - 前缀下划线
(lambda (x _y _z)
(* x 2))
;; 好 - 在解构中
(pcase-let ((`(,first . ,_rest) my-list))
first)
谓词
单词语词:
;; 好 - 以 'p' 结尾
(defun evenp (n)
(zerop (mod n 2)))
(defun emptyp (list)
(null list))
多词语词:
;; 好 - 以 '-p' 结尾
(defun buffer-live-p (buffer)
(and buffer (buffer-name buffer)))
(defun my-package-valid-state-p (state)
(member state '(ready active complete)))
特殊命名
面孔名称:
;; 好 - 无 -face 后缀
(defface my-package-error
'((t :foreground "red"))
"错误消息的面孔。")
;; 坏 - 冗余 -face 后缀
(defface my-package-error-face
'((t :foreground "red"))
"错误消息的面孔。")
4. 函数
Lambda 函数
使用指南:
;; 好 - lambda 用于本地使用
(mapcar (lambda (x) (* x 2)) '(1 2 3))
;; 坏 - lambda 在钩子中(使用命名函数)
(add-hook 'emacs-lisp-mode-hook
(lambda () (eldoc-mode 1)))
;; 好 - 钩子中使用命名函数
(defun my-elisp-mode-setup ()
"Emacs Lisp 模式设置。"
(eldoc-mode 1))
(add-hook 'emacs-lisp-mode-hook #'my-elisp-mode-setup)
永不硬引用 Lambdas:
;; 坏 - 硬引用 lambda
(mapcar '(lambda (x) (* x 2)) list)
;; 好 - 未引用 lambda
(mapcar (lambda (x) (* x 2)) list)
;; 好 - 函数引用(尖引号)
(mapcar #'(lambda (x) (* x 2)) list)
不包装现有函数:
;; 坏 - 不必要的包装
(add-hook 'prog-mode-hook
(lambda () (linum-mode 1)))
;; 好 - 直接函数引用
(add-hook 'prog-mode-hook #'linum-mode)
函数引用
使用尖引号作为函数名:
;; 好 - 函数引用有助于字节编译
(mapcar #'upcase '("foo" "bar"))
(add-hook 'text-mode-hook #'turn-on-auto-fill)
(global-set-key (kbd "C-c f") #'find-file)
;; 可接受但较不优化
(mapcar 'upcase '("foo" "bar"))
;; 坏 - 未引用(仅适用于交互命令)
(global-set-key (kbd "C-c f") 'find-file)
参数限制
避免太多参数:
;; 坏 - 太多位置参数
(defun create-user (name email age address city state zip country)
...)
;; 好 - 使用 plist 或 alist 作为多个参数
(defun create-user (name email &rest properties)
"创建用户,带 NAME 和 EMAIL。
PROPERTIES 是 plist,可能包括 :age, :address, :city 等。"
(let ((age (plist-get properties :age))
(address (plist-get properties :address)))
...))
;; 好 - 使用 cl-defun 带关键字参数
(cl-defun create-user (name email &key age address city state)
"创建用户,带 NAME 和 EMAIL 和可选属性。"
...)
5. 宏
何时使用宏
仅当函数无法工作时使用宏:
;; 好 - 宏需要特殊求值
(defmacro with-timing (label &rest body)
"执行 BODY 并以 LABEL 打印执行时间。"
(declare (indent 1))
(let ((start (make-symbol "start")))
`(let ((,start (current-time)))
(prog1 (progn ,@body)
(message "%s: %.2fs" ,label
(float-time (time-since ,start)))))))
;; 坏 - 函数可工作于此
(defmacro double (x)
`(* 2 ,x))
;; 好 - 改用函数
(defun double (x)
(* 2 x))
宏设计
首先提取示例:
;; 在编写宏前,写示例用法:
;;
;; (with-temporary-buffer
;; (insert "content")
;; (buffer-string))
;;
;; 然后实现以匹配期望用法
使用 Declare 用于元数据:
;; 好 - 带 declare 的完整宏
(defmacro with-my-mode (buffer &rest body)
"在 BUFFER 中执行 BODY,带 my-mode 启用。"
(declare (indent 1)
(debug (sexp body)))
`(with-current-buffer ,buffer
(my-mode 1)
(unwind-protect
(progn ,@body)
(my-mode -1))))
偏好语法引用(反引号):
;; 好 - 反引号带解引用
(defmacro my-when (condition &rest body)
`(if ,condition
(progn ,@body)))
;; 坏 - 手动列表构造
(defmacro my-when (condition &rest body)
(list 'if condition
(cons 'progn body)))
6. 文档
文档字符串
函数文档字符串:
;; 好 - 完整文档字符串
(defun my-package-process-file (filename)
"处理在 FILENAME 的文件。
文件被读取、处理,结果被返回。
如果 FILENAME 不存在,发出错误信号。
返回处理后的内容作为字符串。"
(with-temp-buffer
(insert-file-contents filename)
(buffer-string)))
;; 好 - 文档所有参数
(defun my-package-create-entry (name email &optional age)
"创建带 NAME 和 EMAIL 的条目。
NAME 应是非空字符串。
EMAIL 应是有效电子邮件地址。
可选 AGE 应是正整数。
返回创建的条目作为 plist。"
...)
变量文档字符串:
;; 好
(defvar my-package-timeout 30
"网络操作的超时秒数。
此值由 my-package 中的所有网络函数使用。
设置为 nil 以禁用超时。")
;; 好 - 自定义变量带类型
(defcustom my-package-auto-save t
"是否自动保存文件。
当非 nil 时,文件在修改后自动保存。"
:type 'boolean
:group 'my-package)
首行摘要:
;; 好 - 首行是完整句子
(defun my-function (arg)
"处理 ARG 并返回结果。
关于函数行为的附加细节。
更多解释如果需要。"
...)
;; 坏 - 不完整首行
(defun my-function (arg)
"处理 ARG 和
返回结果。"
...)
注释
注释级别:
;;; 节标题
;;; 用于文件的主要节
;;; 注释:
;; 包描述和用法示例
;;; 代码:
;; 顶级注释
;; 用于函数前的解释
(defun my-function ()
;; 函数内注释
;; 用于解释代码块
(let ((x 10)) ; 边距注释用于内联解释
x))
注释最佳实践:
;; 好 - 解释为什么,而不是什么
(defun my-function (items)
;; 首先排序项目以优化查找性能
(let ((sorted-items (sort items #'string<)))
(process-items sorted-items)))
;; 坏 - 冗余注释
(defun my-function (items)
;; 排序项目
(sort items #'string<))
;; 好 - 自文档化代码代替注释
(defun process-active-buffers ()
(let ((active-buffers (get-active-buffers)))
(mapc #'process-buffer active-buffers)))
;; 坏 - 需要注释解释
(defun process ()
;; 获取活动缓冲区
(let ((bufs (get-bufs)))
;; 处理每个
(mapc #'proc bufs)))
注解关键词:
;; TODO: 添加远程文件支持
;; FIXME: 这与 Unicode 字符冲突
;; OPTIMIZE: 缓存结果以提高性能
;; HACK: package.el 中的变通方法
;; REVIEW: 这是最佳方法吗?
;; 好 - 带首字母和日期
;; TODO(user 2025-01-15): 实现异步版本
;; FIXME(user 2025-01-15): 处理空输入的边缘情况
7. 加载和自动加载
模块结构
正确文件页脚:
;;; my-package.el --- 简短描述 -*- lexical-binding: t; -*-
;; 版权所有 (C) 2025 您的名字
;; 作者:您的名字 <your.email@example.com>
;; 版本:1.0.0
;; 包-需要:((emacs "27.1"))
;; 关键词:便利, 工具
;; URL: https://github.com/user/my-package
;;; 注释:
;; 包的更长描述。
;; 用法示例。
;;; 代码:
(defun my-package-do-something ()
"做某事有用。")
(provide 'my-package)
;;; my-package.el 结束于此
Require 与 Load
使用 require 用于依赖:
;; 好 - 幂等、安全
(require 'subr-x)
(require 'cl-lib)
;; 坏 - 非幂等
(load "subr-x")
(load-library "cl-lib")
自动加载 Cookie
使用自动加载用于面向用户的命令:
;; 好 - 自动加载交互命令
;;;###autoload
(defun my-package-start ()
"启动 my-package。"
(interactive)
(my-package-mode 1))
;; 好 - 自动加载模式定义
;;;###autoload
(define-minor-mode my-package-mode
"切换 my-package 模式。"
:lighter " MyPkg"
(if my-package-mode
(my-package--enable)
(my-package--disable)))
;; 坏 - 自动加载内部函数
;;;###autoload
(defun my-package--internal-helper ()
"内部助手。")
8. 词法绑定
总是启用词法绑定:
;;; my-package.el --- 描述 -*- lexical-binding: t; -*-
;; 词法绑定更快且更直观
;; 总是用于新代码
词法绑定的好处:
;; 使用词法绑定,闭包工作正确
(defun make-counter ()
"创建计数器函数。"
(let ((count 0))
(lambda ()
(setq count (1+ count))
count)))
;; 用法:
;; (setq counter1 (make-counter))
;; (funcall counter1) => 1
;; (funcall counter1) => 2
9. 列表操作
使用 dolist 用于迭代:
;; 好 - 清晰且惯用
(dolist (item items)
(process-item item))
;; 坏 - 冗长
(mapc (lambda (item)
(process-item item))
items)
不使用 mapcar 用于副作用:
;; 坏 - mapcar 创建未使用列表
(mapcar #'process-item items)
;; 好 - dolist 用于副作用
(dolist (item items)
(process-item item))
;; 好 - seq-do 用于函数式风格
(seq-do #'process-item items)
10. 交互命令
正确交互规范:
;; 好 - 带提示
(defun my-package-open-file (filename)
"在 my-package 中打开 FILENAME。"
(interactive "F打开文件: ")
(find-file filename))
;; 好 - 多个参数
(defun my-package-replace (from to)
"在当前缓冲区中将 FROM 替换为 TO。"
(interactive
(list (read-string "替换: ")
(read-string "为: ")))
(save-excursion
(goto-char (point-min))
(while (search-forward from nil t)
(replace-match to))))
;; 好 - 使用前缀参数
(defun my-package-insert-n (n)
"插入 N 份某物。"
(interactive "p")
(dotimes (_ n)
(insert "文本")))
11. 错误处理
适当发出错误信号:
;; 好 - 描述性错误
(defun my-package-get-user (id)
"通过 ID 获取用户。"
(or (gethash id my-package-users)
(error "用户未找到:%s" id)))
;; 好 - user-error 用于用户错误
(defun my-package-save ()
"保存当前数据。"
(interactive)
(unless (my-package--has-changes-p)
(user-error "无更改保存")))
;; 好 - 带 condition-case
(defun my-package-load-file (filename)
"安全加载 FILENAME。"
(condition-case err
(with-temp-buffer
(insert-file-contents filename)
(read (current-buffer)))
(file-error
(message "无法读取文件:%s" (error-message-string err))
nil)))
最佳实践摘要
编写 Emacs Lisp 代码时:
- 总是启用词法绑定 在文件头
- 前缀所有公共符号 带包名
- 使用
defcustom用于用户可配置变量 - 添加自动加载 cookie 到交互命令
- 编写全面文档字符串 用于所有公共函数
- 偏好内置函数 而非重新发明功能
- 使用
declare在宏中用于缩进和调试 - 优雅处理错误 带信息性消息
- 遵循命名约定 一致地
- 保持函数专注 于单一职责
常见模式
包模板
;;; my-package.el --- 简短描述 -*- lexical-binding: t; -*-
;; 版权所有 (C) 2025 您的名字
;; 作者:您的名字 <email@example.com>
;; 版本:1.0.0
;; 包-需要:((emacs "27.1"))
;; 关键词:便利
;; URL: https://github.com/user/my-package
;;; 注释:
;; 更长描述和用法示例。
;;; 代码:
(require 'cl-lib)
(defgroup my-package nil
"我的包自定义组。"
:group 'convenience
:prefix "my-package-")
(defcustom my-package-option t
"启用某些选项。"
:type 'boolean
:group 'my-package)
;;;###autoload
(defun my-package-command ()
"主命令。"
(interactive)
(message "Hello from my-package!"))
(provide 'my-package)
;;; my-package.el 结束于此