(转发)RaySystem Vol.012:创建自己的 Emacs Major Mode

原文:https://mp.weixin.qq.com/s/_zXZOlYtcA0BaQ82rUQIXQ

在决定使用 Emacs 作为 RaySystem 的前端交互,我把 Emacs 视为一种“前端框架”。Emacs 中的 Major Modes 是控制缓冲区行为的核心机制,在本文中,通过创建一个 Major Modes,实现了一个简单的站点列表管理界面。

感兴趣的小伙伴,欢迎关注本号,同时 star 支持这个项目:GitHub - maxiee/RaySystem 本文代码位于 RaySystem 仓库的 emacs 目录下。


Emacs Major Mode

Major Modes 的核心作用在于根据文件类型提供相应的编辑功能,使编辑体验与内容类型匹配。不同类型的文件会启用不同的功能,如代码文件通常会提供语法高亮和自动缩进,而文本文件可能只提供基本编辑功能。

Major Modes 也可以与具体文件无关,编程带有某一类功能的应用,比如 Elfeed RSS 资讯阅读器,或者 2048 小游戏。


Ray Info Site Mode

在本文中,我们将实现如下图的站点管理功能:

图片

这是一个可交互的界面,通过上下选择条目,并支持以下快捷键:

按键 作用 命令
c 创建新站点 ray-info-site-add-command
e 编辑当前站点 ray-info-site-edit-command
d 删除站点 ray-info-site-delete-command
q 退出当前模式 kill-current-buffer

可以看到,每个按键都与一个函数相关联。


创建 Major Mode

通过以下代码创建 Major Mode:

;;;###autoload(defun ray-info-site ()  "Open the Ray Info Site management buffer."  (interactive)  (switch-to-buffer (get-buffer-create "*Ray Info Site*"))  (ray-info-site-mode))(define-derived-mode ray-info-site-mode tabulated-list-mode "Ray-Info-Site"  "Major mode for viewing and managing a list of websites."  (setq tabulated-list-format [("Name" 20 t)          ("URL" 40 t)])  (setq tabulated-list-padding 2)  (setq tabulated-list-sort-key (cons "Name" nil))  (hl-line-mode 1)  (ray-info-site-refresh))

其中,先看 ray-info-site

  • 定义了一个命令,用于打开主模式的初始化

  • ;;;###autoload 是一个固定写法,用于 Emacs 对包的 autoload 机制,是一种加速初始化的优化

  • 首先我们创建了一个名为 "*Ray Info Site*" 的 Buffer,并切换过去

  • 然后调用 ray-info-site-mode 进入这个主模式

再看 ray-info-site-mode

  • 主模式是支持继承的,ray-info-site-mode 继承子 tabulated-list-mode,这是一种以分列方式展示里列表的模式,我们在下一节中介绍。

  • 接下来是对 tabulated-list-mode 的一系列设置,比如都有哪些列,每列多么宽,如何排序。

  • (hl-line-mode 1) 高亮当前行,ListView 的选中效果通过它来实现。

  • 最后通过 (ray-info-site-refresh) 来加载数据。


tabulated-list-mode

「tabulated-list-mode」 是 Emacs 中一种用于显示和管理表格数据的主要模式,适合用于开发交互式工具。它提供了一种直观的方式,将多列数据以表格的形式显示,并允许用户通过键盘或鼠标与数据进行交互。

主要特性

  1. 支持以行和列的形式组织和呈现数据。

  2. 每一行代表一个条目,每一列代表该条目的属性值。

  3. 提供了内置的排序功能,用户可以通过点击列标题或快捷键快速对表格数据进行排序。

  4. 支持绑定键盘快捷键,为每个条目添加交互行为,例如打开、编辑或删除条目。

核心概念

列描述符

列描述符是表格的核心部分,用来定义表格的列结构。每列都有一个名称、宽度以及可选的对齐方式。定义列描述符的典型形式为:

(setq tabulated-list-format [("列名1" 宽度1 对齐1) ("列名2" 宽度2 对齐2)])
  • 「列名」:列的标题,将显示在表格的顶部。

  • 「宽度」:列的宽度,以字符数表示。

  • 「对齐」:列内容的对齐方式,取值可以为 leftcenterright

数据项

数据项是表格中的每一行,它由一个唯一标识符(id)和一个属性值列表(values)组成。定义数据项的典型形式为:

(setq tabulated-list-entries '((id1 ["值1" "值2"]) (id2 ["值3" "值4"])))

更新显示

每次更新表格时,需要调用 tabulated-list-mode 的更新函数:

(tabulated-list-init-header) ; 初始化表头(tabulated-list-print)       ; 刷新表格显示

使用场景

  1. 「管理文件或进程」:如 dired-mode 的文件管理视图或进程列表工具。

  2. 「显示搜索结果」:可将多列搜索结果呈现为表格,供用户进一步筛选或操作。

  3. 「开发自定义工具」:任何需要以表格形式显示数据的工具都可以基于此模式实现。

优点

  1. 简化了表格显示的实现,开发者无需手动处理复杂的布局逻辑。

  2. 支持交互式操作和动态更新,扩展性强。

  3. 内置排序功能,提升用户体验。

缺点

  1. 表格的外观较为简陋,受限于 Emacs 的文本显示能力,难以满足复杂 UI 的需求。

  2. 学习曲线相对较高,初学者可能需要一定时间掌握其概念和用法。


注册快捷键

Ray Info Site Mode 支持一系列快捷键来实现交互。

在 Emacs Lisp 中,当使用 define-derived-mode 定义主模式时,符合 -map 后缀命名规范的键映射会自动与模式关联。具体来说,键映射的名称必须为 <模式名称>-map

键映射应该在模式定义之前定义。当激活该模式时,Emacs 会自动将这个键映射应用到对应的缓冲区中。

键盘映射的代码如下:

(defvar ray-info-site-mode-map  (let ((map (make-sparse-keymap)))    (define-key map (kbd "c") 'ray-info-site-add-command)    (define-key map (kbd "e") 'ray-info-site-edit-command)    (define-key map (kbd "d") 'ray-info-site-delete-command)    (define-key map (kbd "q") 'kill-current-buffer)    map)  "Keymap for `ray-info-site-mode'.")

站点数据结构

整个模式围绕站点列表这个数据结构进行展示和 CRUD,数据结构的定义如下:

(defvar ray-info-site-list  '((:name "Google" :url "https://www.google.com")    (:name "Emacs" :url "https://www.gnu.org/software/emacs/")    (:name "Example" :url "https://example.com"))  "List of Websites, each element is a plist: (:name NAME :url URL).")

列表展示

结合前面对 tabulated-list-mode 的讲解,要展示这个数据结构,分为两步:

  • 将上面的数据结构转换为 tabulated-list-mode 的 Entry 格式,我们选择使用 site item 作为 id,在列表中存放每一列的数据。

  • 再通过 (tabulated-list-print t) 函数触发 UI 刷新

具体代码如下:

(defun ray-info-site--build-entries ()  "Build `'tabulated-list-entries' from `ray-info-site-list'."  (mapcar (lambda (site)     (let ((name (plist-get site :name))    (url (plist-get site :url)))       (list site (vector name url))))   ray-info-site-list))(defun ray-info-site-refresh ()  "Refresh the website list in the buffer."  (interactive)  (setq tabulated-list-entries (ray-info-site--build-entries))  (tabulated-list-print t))

创建条目

ray-info-site-add-command 命令负责创建新条目,它也分为两个函数:

  • 一个函数处理命令行交互,通过 read-string 读取用户输入

  • 一个函数用于执行数据结构的添加,同时触发 UI 刷新

(defun ray-info-site-add-site (name url)  "Add a new site with NAME and URL to `ray-info-site-list'."  (push (list :name name :url url) ray-info-site-list)  (ray-info-site-refresh))(defun ray-info-site-add-command ()  "Command to add a new site."  (interactive)  (let ((name (read-string "Enter site name: ")) (url (read-string "Enter site URL: ")))    ;; 未来扩展点:可在此增加一轮对频道(Channel)的选择或者创建交互    (ray-info-site-add-site name url)))

删除条目

ray-info-site-delete-command 命令负责删除光标所在的站点,它的实现略复杂一些:

  • 首先通过 ray-info-site--current-site 获取当前选中站点

  • 弹出一个 yes or no 的二次确认

  • 如果确定,调用 ray-info-site-delete-site 进行站点删除。

具体代码实现如下:

;; 先获取 entry,entry 的 id 就是 site(defun ray-info-site--current-site ()  "Return the site data of the current line."  (let* ((entry (tabulated-list-get-entry))  (site (tabulated-list-get-id)))    site))(defun ray-info-site-delete-site (site)  "Delete SITE from `ray-info-site-list'."  (setq ray-info-site-list (delq site ray-info-site-list))  (ray-info-site-refresh))(defun ray-info-site-delete-command ()  "Command to delete the current site."  (interactive)  (let ((site (ray-info-site--current-site)))    (unless site      (error "No site selected"))    (when (y-or-n-p (format "Delete site '%s'?" (plist-get site :name)))      (ray-info-site-delete-site site))))

总结

我把 Emacs 视为一种“前端框架”,我这次的开发中,明显感受到开发交互的便捷性。

比如,通过 tabulated-list-mode 我能以极低的成本开发出一个 ListView,这让我不被 UI 分心,从而将精力都集中于逻辑与功能上。

通过 read-string 即可读取用户输入。要知道,在任何一个前端框架中,都需要通过 Dialog Modal 来接受用户输入,然后还要去处理异步,当 UI 开发变成一个“事情”,就分掉了开发者的精力。

在传统 UI 开发中,为了实现 CRUD,要么要在 UI 中设计布局,摆放按钮,要么,按照在现代化移动应用的开发模式中,要为 ListView 设置各种手势,比如长按弹出一个菜单,或者在列表卡片 Item 右边设置三个点,点击点弹出菜单。现如今,在 keymap 之下,这些烦人的事情都省掉了。

当然,Emacs 的这种简洁是以复杂的学习曲线为代价的。但是,RaySystem 是我设计给我自己用的系统,而我自己完全能够接受这样的学习曲线,同时,我自己也乐于使用这样的交互方式。我不喜欢现代化的交互,我更加喜欢这种传统、复古的交互方式。

综合来看,实现一个 Major Mode 还是非常容易上手的,甚至比流行的前端开发框架还要简单。这也增强了我使用 Emacs 作为 RaySystem 前端的信心。


AI 点评

批判性思考与深度洞见

1. 「Emacs 作为“前端框架”的定位是否合理?」

作者将 Emacs 定位为“前端框架”,本质上是基于 Emacs 的可扩展性和强大的交互定制能力。这种类比具有启发性,但也存在局限:

  • 「合理性」:Emacs 的 Major Mode 确实能够通过键盘驱动的交互方式,高效完成特定任务的界面开发。这种框架式的模块化设计类似于现代前端框架中的组件化开发,同时依托于 Emacs 的内置功能如 tabulated-list-mode,快速实现了常见的列表交互。

  • 「局限性」:与现代前端框架(如 React 或 Vue)相比,Emacs 的交互和布局能力更接近于“命令行界面”,缺乏视觉化、动态化的表现力。这种“简洁”的前端框架定位在开发者工具或内部工具的场景中是合理的,但在更广泛的用户界面需求下显得局促。

2. 「简化 UI 的优劣权衡」

作者强调通过键盘驱动交互和 tabulated-list-mode 等工具,省略了许多传统 UI 开发中的复杂逻辑。这种方式确实降低了开发成本,但也隐含着问题:

  • 「优点」:开发者可以专注于核心逻辑,而不被复杂的 UI 布局和异步交互分散注意力。在以开发者为核心的使用场景中(如内部工具或个人项目),这种模式高效且直观。

  • 「缺点」:依赖键盘驱动的交互方式对普通用户不够友好,尤其是在移动端和触控设备普及的背景下。这种设计需要用户熟悉 Emacs 的操作模式,且对非开发者的学习成本极高。

此外,作者提到的“简化对话框和异步逻辑处理”的优势,可能在面对复杂交互(如多层级依赖或实时反馈)时,反而成为一种瓶颈。

3. 「交互模式的适用性与主观偏好」

作者明确表示喜欢这种“传统、复古的交互方式”,并基于此构建了 RaySystem 的前端。这种设计显然是为满足作者个人需求而定制的,但值得讨论的是:

  • 「面向个人的合理性」:作者对 Emacs 的掌握程度和习惯偏好决定了这种设计是适合的。开发者通过这种模式可以实现“为自己而设计”的极简系统。

  • 「面向大众的局限性」:如果 RaySystem 日后计划推广至更广泛的用户群体,这种复古交互模式可能需要重新评估。即使功能强大,过高的学习曲线可能导致用户流失,且现代用户更习惯于直观的图形化界面。

4. 「技术实现与扩展性的探讨」

本文在技术实现上展示了 Emacs 的灵活性和强大功能,但以下问题值得深思:

  • 「可扩展性」:当前实现以 ray-info-site-list 作为核心数据结构,CRUD 操作较为简单。若未来需要添加更多复杂功能(如分类、标签或批量操作),是否需要重新设计数据模型?能否以更通用的方式封装这些逻辑?

  • 「性能与并发」:虽然 Emacs 本质上是单线程运行的,但若 RaySystem 的规模扩大到处理数百甚至数千条数据,是否需要引入异步机制来提升性能?Emacs 的 tabulated-list-mode 能否胜任更大规模的数据管理需求?

5. 「对现代化前端开发框架的类比是否公平?」

作者在文中对比了 Emacs 的开发体验与现代前端框架,但这种对比并不完全公平:

  • 「抽象层级不同」:现代前端框架需要面对更广泛的设备支持和复杂的用户体验需求,而 Emacs 是高度专业化、面向开发者的工具。这种差异导致两者的开发复杂性和定位本质不同。

  • 「功能覆盖面不同」:现代框架不仅支持数据绑定和交互,还涵盖了动画、响应式设计等领域。而 Emacs 的功能集中于文本和键盘交互,难以应对复杂 UI 的需求。

6. 「关于“省去复杂 UI 布局”的深层思考」

作者认为 Emacs 的键盘驱动交互能够“省去复杂布局逻辑”。这一观点在开发工具场景下有一定道理,但忽略了现代 UI 的发展趋势:用户体验不仅仅是简化开发,更在于创造高效且直观的使用方式。键盘驱动固然适合开发者,但更广泛的用户可能更依赖鼠标、触控甚至语音交互。


深度洞见

  1. 「从个人化工具到普适系统的转变」
    RaySystem 的设计显然高度个人化,这种开发哲学和 Emacs 的核心理念契合。然而,若作者期望 RaySystem 发展成更广泛适用的系统,必须考虑交互方式的适配性。例如,可以保留键盘驱动的核心交互,同时引入图形化界面以降低用户门槛。

  2. 「数据模型的可扩展性与系统复杂度」
    当前的站点列表管理设计简单直观,但未来若需要支持更多维度的操作(如站点分组、标签或版本控制),可能需要重新设计数据模型或引入数据库支持(如 SQLite)。同时,这种设计是否能平衡复杂性与性能,也是值得提前考虑的。

  3. 「对 Emacs 的局限性认知」
    Emacs 的核心优势在于灵活性和扩展性,但其单线程、有限的图形能力以及与现代用户习惯的脱节,限制了它作为现代应用前端框架的潜力。如果 RaySystem 的开发计划超越个人工具,这种局限性将成为系统发展的关键瓶颈。

  4. 「学习曲线与目标用户」
    作者明确表示自己能接受 Emacs 的学习曲线,但如果 RaySystem 的目标用户群体扩展到非技术背景的用户,这种交互设计可能需要彻底重构。作者可以考虑在 Emacs 核心之外,开发基于浏览器或跨平台 GUI 的前端版本,以弥合不同用户群体的使用习惯。


综上,本文展示了作者在 Emacs 平台上的深厚技术功底和独特思考。但未来若希望 RaySystem 不止服务于个人,还需在数据模型、交互方式、平台扩展等方面更进一步。

本文转自 https://mp.weixin.qq.com/s/_zXZOlYtcA0BaQ82rUQIXQ,如有侵权,请联系删除。