在前面的文章中,我们了解了,实际在通过 define-derived-mode 定义自己的Major Mode 的时候,在处理 syntax-table 时主要通过继承的方式继承父类中内容,然后通过 modify-syntax-entry 修改不符合当前语境下的项目。或者使用 syntax-propertize-function 通过正则表达式处理一些复杂的情况。今天我们深入探讨 define-derived-mode 宏是如何管理 syntax-table 以及如何通过继承机制获得父类的 syntax-table
背景知识
syntax-table 关键字
与 abbrev-table 类似,define-derived-mode 支持通过 :syntax-table 自定义syntax-table。我们通过一个简单的示例看看宏展开的代码:
(define-derived-mode my/code-mode prog-mode "mycode"
"my code 测试mode"
:syntax-table (let ((my-code-syntax-table (make-syntax-table)))))我们展开之后发现,与 syntax-table 相关的代码只有
(set-syntax-table
(let ((my-code-syntax-table (make-syntax-table)))))也就是说它仅仅将我们通过 :syntax-table 关键字传进来的 内容设置为当前buffer中的 syntax-table。此时继承相关的内容由我们自己管理。
因此这里得出的结论也是与 abbrev-table 类似:应该在外部定义一个与宏里面 syntax-table 命名相同的 syntax-table 变量,我们在外部只关心具体的定义,而其余细节交给宏来处理。
set-char-table-parent
我们观察在继承机制下,对于 syntax-table 的处理发现,继承机制实际上是通过 set-char-table-parent 函数来完成。
(set-char-table-parent CHAR-TABLE PARENT)这个代码是底层通过 c 实现的,我们可以简单的看看在Emacs中它是如何实现的。在我找到的一些源码资料中显示,这个函数的C语言实现如下:
DEFUN ("set-char-table-parent", Fset_char_table_parent, Sset_char_table_parent,
2, 2, 0,
doc: /* Set the parent char-table of CHAR-TABLE to PARENT.
Return PARENT. PARENT must be either nil or another char-table. */)
(Lisp_Object char_table, Lisp_Object parent)
{
Lisp_Object temp;
CHECK_CHAR_TABLE (char_table);
if (!NILP (parent))
{
CHECK_CHAR_TABLE (parent);
for (temp = parent; !NILP (temp); temp = XCHAR_TABLE (temp)->parent)
if (EQ (temp, char_table))
error ("Attempt to make a chartable be its own parent");
}
set_char_table_parent (char_table, parent);
return parent;
}
static void
set_char_table_parent (Lisp_Object table, Lisp_Object val)
{
XCHAR_TABLE (table)->parent = val;
}从这里我们可以看到,函数内部主要的工作实际上是将表中的 parent 指针指向真实的父类。所以说它的继承结构实际上构成了一个链表。
按照我们写程序的一般规律来讲,如果是一个链表结构,可以合理的推测它的查找流程应该是这样的:
- 先在当前buffer 中记录的
syntax-table表中查找 - 如果未找到,则根据
parent指针找到它的父表,在父表中查找 - 根据记录的
parent指针一级级的往后找,直到找到或者parent指针是空
我们可以在Emacs的源码中找到查表相关的代码:
Lisp_Object
char_table_ref (Lisp_Object table, int c)
{
struct Lisp_Char_Table *tbl = XCHAR_TABLE (table);
Lisp_Object val;
if (ASCII_CHAR_P (c))
{
val = tbl->ascii;
if (SUB_CHAR_TABLE_P (val))
val = XSUB_CHAR_TABLE (val)->contents[c];
}
else
{
val = tbl->contents[CHARTAB_IDX (c, 0, 0)];
if (SUB_CHAR_TABLE_P (val))
val = sub_char_table_ref (val, c, UNIPROP_TABLE_P (table));
}
if (NILP (val))
{
val = tbl->defalt;
if (NILP (val) && CHAR_TABLE_P (tbl->parent))
val = char_table_ref (tbl->parent, c);
}
return val;
}别的先不用太纠结,我们看到它分了三种情况:
- 如果是普通的 ASCII 字符,直接查找表中的 contents 数据
- 如果是非 ASCII 字符,调用 sub_char_table_ref 查找
- 最后如果当前表中未找到,它会通过
parent指针找到父表,再递归调用一次
上面的代码中,我们看不到 syntax-table 的相关名称,转而退化为了 char-table。实际上 char-table 是Emacs专门为了快速查找 key 类型为 unicode 字符,而value 类型为对应数据这种映射关系而设计的数据结构。syntax-table 只是它里面的一种。如果深入了解Emacs之后会发现很多类似的结构。
总结
最后我们来总结一下关于 syntax-table 的相关内容。
- 默认情况下,
define-derived-mode会根据传入的parent参数来执行继承操作。实现继承的主要方式就是调用set-char-table-parent将多个表组成一个链表,实际查找时按链表顺序进行查找。 - 我们可以使用
:syntax-table关键字指定syntax-table,但是这个时候需要我们自己处理关于表的一切,define-derived-mode只负责设置当前buffer 的syntax-table为我们指定的内容。
评论 (0)