《elisp 每日一练》—— define-derived-mode 管理 syntax-table

《elisp 每日一练》—— define-derived-mode 管理 syntax-table

Masimaro
2026-09-24 / 0 评论 / 0 阅读 / 正在检测是否收录...

在前面的文章中,我们了解了,实际在通过 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

评论 (0)

取消