《elisp 每日一练》——major mode 综合练习

《elisp 每日一练》——major mode 综合练习

Masimaro
2026-10-13 / 0 评论 / 1 阅读 / 正在检测是否收录...

截止到上一篇,我们完成了对major mode 相关概念的讨论,我们还是按照惯例先回顾一下前面的内容,接着来一个综合性的练习收尾

知识回顾

一个buffer 可以有多个 minor mode 但是有且只有一个major mode。我们可以通过 define-derived-mode 定义major mode。major mode 的定义具有继承的概念,一般是利用现在的major mode 扩展出新的。

我们常用的基础major mode 有:

  • fundamental-mode 最通用的major mode,Emacs新建buffer如果没有特殊指定,默认就是 fundamental-mode
  • text-mode 一般的文本buffer都可以从它继承,它含有基础的单词、段落分割、拼写检查
  • special-mode 它是只读交互buffer的基础, 绑定了 q 作为退出
  • prog-mode 编程语言的基础mode、默认启用语法高亮、代码缩进引擎机制,绑定代码注释逻辑,排斥拼写检查等文本特征

在定义我们的mode时,会管理 keymap、abbrev-table、syntax-table。它们都支持继承。继承方式各有不同:

  • keymap 通过 set-keymap-parent 设置父类。
  • abbrev-table 则使用 abbrev-table-put 为当前table 设置上 :parent 属性
  • syntax-table 通过 set-char-table-parent 设置父类

在设置完成之后,我们需要更新对应的buffer local 变量,更新的方式:

(use-local-map my-code-mode-map)
(set-syntax-table my-code-mode-syntax-table)
(setq local-abbrev-table my-code-mode-abbrev-table)

这些工作都由宏来解决,我们的工作就是定义与 major mode 同名的 map、syntax-table、abbrev-table 变量。宏会自动帮我们完成设置继承关系、更新buffer local 变量这些工作。

major mode 的一个重要工作是进行语法高亮,我们利用 font lock 和 syntax-table 配合进行也可以使用treesit 来完成这个工作。

如果使用传统的 font lock,我们需要定义一个由正则表达式到对应face的映射关系,并且在最后将这个关系赋值到 font-lock-defaults 上

如果使用 treesit, 我们采用查询语句的方式进行高亮。我们需要利用 treesit-font-lock-rules 将查询语句和对应face的映射关系进行编译,生成一个对应的list列表,后面我们需要将返回的列表设置到 buffer local 变量 treesit-font-lock-settings 上。并且需要设置 treesit-font-lock-feature-list 填入对应的feature 开启上面设置的某条高亮规则。最后需要调用 treesit-major-mode-setup 启动这套规则

最后我们可以通过 auto-mode-alist 设置文件关联,这样下次在打开对应后缀的文件之后会直接使用我们定义的mode

练习

这部分的练习,我们来实现一个简单的支持 markdown 高亮显示的 mode。它的功能如下:

  • 对一些元素进行高亮显示:标题按照不同字号加粗显示,支持一些特殊的字符效果,例如加粗、斜体、删除线
  • 我们添加一个 cb 快速创建table、<c 快速创建代码块
  • 定义一个函数并绑定快捷键为 M-<Enter> 实现列表/标题智能追加。

对于高亮,我们先定义基本的face

(defface my-md-title-1
  '((t :height 1.4 :weight bold :inherit font-lock-keyword-face))
  "一级标题 Face(大字号+加粗)")

(defface my-md-title-2
  '((t :height 1.2 :weight bold :inherit font-lock-keyword-face))
  "二级标题 Face (中字号+加粗)")

(defface my-md-title-3
  '((t :height 1.1 :weight bold :inherit font-lock-keyword-face))
  "三级标题 Face(中小字号 + 加粗)")

(defface my-md-title-4
  '((t :weight bold :inherit font-lock-keyword-face))
  "四级标题 Face(加粗)")

(defface my-md-bold
  '((t :weight bold :inherit font-lock-type-face))
  "粗体 Face")

(defface my-md-italic
  '((t :slant italic :inherit font-lock-variable-name-face))
  "斜体 Face")

(defface my-md-strike
  '((t :strike-through "gray50" :inherit font-lock-comment-face))
  "删除线 Face")

;; 针对这些face,模仿官方的 font-lock-face 变量定义
(defvar my-md-title-1 'my-md-title-1)
(defvar my-md-title-2 'my-md-title-2)
(defvar my-md-title-3 'my-md-title-3)
(defvar my-md-title-4 'my-md-title-4)
(defvar my-md-bold 'my-md-bold)
(defvar my-md-italic 'my-md-italic)
(defvar my-md-strike 'my-md-strike)

然后我们针对markdown中这些标识通过正则表达式定义匹配规则

(defvar my-md-font-lock-keywords
  `(
    ;; 一级标题
    ("^#\\s-+\\(.*\\)$" 0 my-md-title-1)
    ;;二级标题
    ("^##\\s-+\\(.*\\)$" 0 my-md-title-2)
    ;;三级标题
    ("^###\\s-+\\(.*\\)$" 0 my-md-title-3)
    ;;四级及更高标题
    ("^####+\\s-+\\(.*\\)$" 0 my-md-title-4)
    ;;粗体
    ("\\*\\*\\([^*]+\\)\\*\\*" 1 my-md-bold)
    ;; 斜线
    ("\\*\\([^*]+\\)\\*" 1 my-md-italic)
    ;;删除线
    ("~~\\([^~]+\\)~~" 1 my-md-strike))
  "My-Markdown 的 Font Lock 正则样式规则")

然后我们定义一个简单 syntax-table,我们只识别字符串这一种词法块

(defvar my-markdown-mode-syntax-table
  (let ((st (make-syntax-table)))
    (modify-syntax-entry ?\" "\"" st)
    st)
  "My Markdown Mode 语法表")

接着我们定义一个 abbrev-table 添加两条规则

(defvar my-markdown-mode-abbrev-table
  "My Markdown Mode 缩写表")

(define-abbrev-table 'my-markdown-mode-abbrev-table
  '(
    ;;输入cel 自动展开代码块模板 (create elisp)
    ("cel" "```elisp\n\n```")
    ;;输出 tbl 创建一个表格的表头
    ("tbl" "| Header 1 | Header 2|\n|:-----|:-----|\n| Item1 | Item2|")))

对于自动追加对应的单元,我们通过正则表达式实现这么一个函数:

(defun my-markdown-insert-sibling-line()
  "在当前行的下方插入同类型、同级别的行"
  (interactive)
  (let* ((line (buffer-substring-no-properties
                (line-beginning-position) (line-end-position)))
         (new-prefix
          (cond
           ;;无序列表, 以 - 、* 、+ 开头的行,前面可能带有缩进
           ((string-match "^\\([ \t]*\\)\\([-*+]\\)\\([ \t]+\\)" line)
            (concat (match-string 1 line)
                (match-string 2 line)
                (match-string 3 line)))
           ;;有序列表 1. 或者 1) 等情形
           ((string-match "^\\([ \t]*\\)\\([0-9]+\\)\\([.)]\\)\\([ \t]+\\)" line)
            (concat (match-string 1 line)
                   (number-to-string
                      (1+ (string-to-number (match-string 2 line))))
                 (match-string 3 line)
                 (match-string 4 line)))
           ;; 标题
           ((string-match "^\\(#+\\)[ \t]+" line)
            (concat (match-string 1 line) " "))
           (t ""))))
    (end-of-line)
    (newline)
    (insert new-prefix)))

我们实现了一个在这个函数,我们首先通过 buffer-substring-no-properties 获取当前行的文本,但是我们只需要文本而不关心它的属性。然后利用正则表达式进行匹配,分别尝试匹配各级标题、有序列表和无序列表。string-match 会将匹配结果存储,后续我们通过 match-string 按分组获取并最终合成一个对应的前缀。最后我们将光标移动到当前行的末尾并插入一行,然后在新行插入之前组织的前缀。

最后我们对这个功能进行快捷键绑定

(defvar my-markdown-mode-map
  (let ((map (make-sparse-keymap)))
    (keymap-set map "M-RET" #'my-markdown-insert-sibling-line)
    map))

有了这些我们就可以组织成一个简单的 major mode

(define-derived-mode my-markdown-mode text-mode "My Markdown"
  "简单的自定义 markdown major mode"
  (setq-default font-lock-defaults '(my-md-font-lock-keywords nil nil))
  (abbrev-mode t)
  )

根据前面我们分析的结果,只要 abbrev-table、syntax-table、keymap 符合默认的命名规则,define-derived-mode 会帮助我们处理这些,我们唯一需要额外设置的就是 font-lock 了。最后要启用缩写机制别忘了开启 abbrev-mode。

我们可以用最后学到的 autoload 机制和文件绑定的方式,进行绑定。但是我这里就不再演示了。读者可以尝试使用它们使mode变得看起来更专业一些。

总结

到此为止,我们关于 minor mode 和 major mode 都结束了。但是我们在分析 define-derived-mode 和 define-minjor-mode 的时候忽略了hook。这部分我们将在接下来的hook专题中讨论。

0

评论 (0)

取消