截止到上一篇,我们完成了对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-modetext-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)