ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TextMate 2 事件与过滤器(Events / Filters)机制完全指南:基于 eventSpecifier 与 callback.document 语义类的自动响应扩展

TextMate 2 事件与过滤器(Events / Filters)机制完全指南:基于 eventSpecifier 与 callback.document 语义类的自动响应扩展
  • 开发工具
  • 桌面应用
  • 代码编辑器

【免费下载链接】textmate

TextMate is a graphical text editor for macOS 10.12 or later

项目地址:https://gitcode.com/gh_mirrors/te/textmate
点击查看免费下载

导读

本文围绕 TextMate 2 的事件(Events)与过滤器(Filters)扩展机制展开,讲解如何通过为普通命令添加eventSpecifier键来捕获event.document.did_save保存事件,以及如何利用semanticClass(语义类)注册callback.document.*系列的导入/导出过滤器,让编辑器在打开、保存文件时自动对文档内容进行转换。读完本文,你将掌握 TextMate 2 事件命令与过滤器的完整配置语法、Avian 内置 Bundle 中的真实示例,以及底层file框架中过滤器的发现、选择与执行流程。

关联文档:events.md


一、概述:事件与过滤器的定位

在 TextMate 2 中,Bundle 命令除了通过菜单、快捷键、Tab 补全等方式被手动触发外,还可以被编辑器在特定时机自动调用。这套自动调用机制由两类构件组成:

机制触发时机典型用途
Events(事件)文档生命周期事件(如保存完成)保存后自动设置脚本可执行位、刷新预览
Filters(过滤器)文档内容的导入 / 导出流程中解压、加密、解码、转码等内容变换

两者的区别在于:事件的输出通常被丢弃(仅作为"动作钩子"),而过滤器的输出会被编辑器采用,直接替换文档内容或参与落盘内容。正如关联文档所说:"Filters are like events but their output is used"(过滤器与事件相似,但过滤器的输出会被使用)。


二、Events:捕获event.document.did_save

2.1 当前唯一的内置事件

关联文档明确指出,目前 TextMate 2 只触发一个事件:

event.document.did_save

即"文档保存完成"事件。从源码看,保存流程的终点位于 file/src/save.cc 的kStateNotifyCallback状态,保存完成后会回调did_save(...);上层 document/src/OakDocument.mm 将该回调转换为OakDocumentIOResultSuccess等结果并通知文档控制器。事件命令正是在这一时刻被查询并执行。

2.2 捕获事件的配置方式

捕获事件只需两步:

  1. 创建一个普通命令(tmCommand);
  2. 为命令添加eventSpecifier键,取值为要捕获的事件名。

配置示意(plist 片段):

<key>eventSpecifier</key> <string>event.document.did_save</string>

命令的其余键(command脚本、input、outputLocation等)与普通命令完全一致。

2.3 实战示例:Make Script Executable

关联文档给出的经典示例是 Avian Bundle 中的"Make Script Executable"命令。它被限定在source.rubyscope 内,因此当你新建一个 Ruby 文件并保存时,该文件会被自动加上可执行位。其配置要点为:

  • eventSpecifier:event.document.did_save
  • scope:source.ruby(也可根据语言扩展为source.python等)

也就是说,事件命令同样遵循 TextMate 的 scope 选择器规则:只有保存文件的 scope 与命令的 scope 匹配时,命令才会被触发。这是让同一事件在不同文件类型下产生不同行为的核心手段。

2.4 事件系统在版本演进中的扩展

虽然关联文档撰写时只有event.document.did_save一个事件,但从 about/Changes.md 可以确认事件机制后来得到了扩展:

  • 新增autoRefresh数组键,允许命令监听DocumentChanged、DocumentSaved、DocumentClosed等事件并自动重跑;
  • 当命令因上述事件被自动执行时,TextMate 会设置环境变量TM_REFRESH,其值即触发执行的事件名;
  • 新增callback.document.did-change-scm-status,用于在 commit/revert 之后触发 SCM Gutter 命令(见 Changes.md)。

这解释了事件命令的一个特殊行为:若命令带有 HTML 输出且只监听DocumentSaved,则 HTML 输出窗口会在主窗口关闭时一并关闭——因为该事件监控的是任意文档的保存。


三、Filters:内容导入 / 导出的自动转换

3.1 四种过滤器事件

与事件不同,过滤器有一套完整的"语义类"(semanticClass)命名空间。在 file/src/filter.cc 中定义了四个常量:

常量语义类取值触发时机
kBundleEventBinaryImportcallback.document.binary-import打开二进制文件时
kBundleEventBinaryExportcallback.document.binary-export保存(写出二进制)时
kBundleEventTextImportcallback.document.import打开文本文件时
kBundleEventTextExportcallback.document.export保存(写出文本)时

3.2 过滤器的发现策略:按语义类查询

过滤器是通过bundles::query按semanticClass字段查询命令项得到的(filter.cc):

bundles::query(bundles::kFieldSemanticClass, event, pathAttributes, bundles::kItemTypeCommand)

其中pathAttributes是文件路径的 scope 属性。这一设计意味着:过滤器是 scope 敏感的,你可以让同一个导入事件在不同文件类型/路径属性下选择不同的处理命令。

一个关键差异是:

  • binary-import会额外读取文件前 256 字节内容,与命令的contentMatch正则进行匹配(binary_filters,见 filter.cc),命中则选中该过滤器;
  • 其余三种过滤器仅按语义类 + scope 查询(path_filters)。

因此二进制导入过滤器支持"内容指纹自动识别",例如检测 GZip、BZip2、PNG 或加密文件头。

3.3 过滤器的执行管线

过滤器命令由 filter::run 启动,其运行方式与普通命令一致(通过command::parser解析、command::runner执行),但使用了专用的event_delegate_t委托:

  • 输入固定为整个文档(input::entire_document+input_format::text),通过管道写入命令的 stdin;
  • 命令的 stdout 会被完整接住,并要求outputLocation为replaceDocument、格式为文本(filter.cc);
  • 过滤器输出最终回调set_content(...)替换文档内容。

也就是说,过滤器是一个"标准输入 → 标准输出"的转换器,无需关心 UI,非常适合复用gunzip、openssl、plutil等命令行工具。

3.4 过滤器在保存流程中的位置

从 save.cc 的状态机可以看到,保存文档时过滤器按如下顺序介入:

保存 → 文本导出过滤器(callback.document.export) → 换行符转换 → 字符集编码 → 二进制导出过滤器(callback.document.binary-export) → 写盘
  • 文本导出过滤器在内容进行换行与编码处理之前执行(kStateExecuteTextExportFilter),处理的是 UTF-8 文本;
  • 二进制导出过滤器在内容完成编码转换之后、写盘之前执行(kStateExecuteBinaryExportFilter);
  • 每个过滤器只执行一次(通过已执行 UUID 列表去重),源码中标注了"多个匹配时应弹出选择对话框"的 FIXME,即当前实现仅取首个匹配项(save.cc)。

对应地,open.cc 与 open.cc 表明打开文件时按相反顺序执行binary-import与text-import过滤器。


四、Avian Bundle 中的过滤器实例精解

关联文档提到 Avian Bundle 内置了"反编译 AppleScript、美化 tmCommand 文件、加密/解密内容"等过滤器。当前仓库中的 Avian.tmbundle 保留了其中一部分命令,可作为编写过滤器的完整范本。

4.1 Import GZip:基于魔数的自动解压

Import GZip.tmCommand 的关键配置:

<key>semanticClass</key> <string>callback.document.binary-import</string> <key>contentMatch</key> <string>\A\x{1F}\x{8B}\x{08}</string> <!-- gzip 魔数 1f 8b 08 --> <key>command</key> <string>#!/bin/bash exec /usr/bin/gunzip</string> <key>input</key><string>document</string> <key>outputLocation</key><string>replaceDocument</string>

它用contentMatch正则匹配文件头字节,命中后直接执行gunzip将内容解压回文档。这正是"打开 gzip 文件自动解压"的机制来源。

4.2 Import BZip2:同类范式

Import BZip2.tmCommand 与 GZip 完全同构,仅将魔数改为\ABZh[1-9]、命令改为exec /usr/bin/bunzip2。可见编写二进制导入过滤器只需修改 contentMatch 与转换命令即可复用同一模板。

4.3 Import PNG Image:导入时输出图像信息

Import PNG Image.tmCommand 展示了过滤器并不限于"无损变换":

<key>contentMatch</key> <string>\A\x{89}PNG\r\n\x{1A}\n</string> <key>command</key> <string>#!/bin/bash echo "PNG Image: ‘${TM_DISPLAYNAME}’" sips -g pixelWidth -g pixelHeight "$TM_FILEPATH"|egrep 'pixel(Width|Height)'</string>

它在匹配 PNG 文件头后,用sips读取图片尺寸并回写到文档。过滤器命令可以获得TM_DISPLAYNAME、TM_FILEPATH等完整的环境变量,因此可以基于文件路径而非仅内容做处理。

4.4 Encrypt on Save / Decrypt:完整的导出-导入闭环

Encrypt on Save.tmCommand 注册为callback.document.binary-export,在保存时用openssl enc -e -aes128加密内容;Decrypt.tmCommand 注册为callback.document.binary-import,以\ASalted__识别 OpenSSL 加盐加密文件,并在打开时解密。两者通过TextMate::UI.request_string询问密码,展示了过滤器与 UI 交互的写法:

  • scope:attr.rev-path.crypt(路径属性决定是否启用加密保存)
  • hideFromUser:1(不显示在 Bundle 命令菜单中)
  • input:document;outputLocation:replaceDocument

这个闭环清晰说明:导出过滤器(保存时)与导入过滤器(打开时)可以成对设计,实现"文件以加密形态落盘、以明文形态编辑"的效果。

4.5 非过滤器的语义类:callback.file-browser.action-menu

同一 Bundle 中的 Compress Selected Items.tmCommand 与 Show Images.tmCommand 使用callback.file-browser.action-menu语义类,属于文件浏览器右键菜单动作(读取TM_SELECTED_FILES环境变量),说明semanticClass是一套通用的命令查询命名空间,事件/过滤器只是其中一部分。


五、从源码看完整触发链:一个 Ruby 文件的保存

结合文档与源码,梳理一个触发event.document.did_save事件的完整链路:

  1. 用户在 TextMate 中新建source.ruby文件并保存;
  2. OakDocument.mm 调用file::save(...);
  3. save.cc 状态机依次执行文本导出过滤器、换行/编码处理、二进制导出过滤器,最后写盘;
  4. 写盘成功后回调did_save(...)(save.cc),向上通知OakDocumentDidSaveNotification(OakDocument.mm);
  5. 保存完成事件被派发,匹配source.rubyscope 的 "Make Script Executable" 事件命令被触发执行,脚本获得可执行位。

事件与过滤器共用命令执行引擎,区别仅在"触发方式"与"输出是否被采用"。


六、编写自己的事件命令与过滤器:实践清单

事件命令模板

<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"> <dict> <key>name</key> <string>My Save Hook</string> <key>eventSpecifier</key> <string>event.document.did_save</string> <key>scope</key> <string>source.ruby</string> <key>command</key> <string>#!/bin/bash # 保存后要执行的动作,例如 chmod +x "$TM_FILEPATH"</string> <key>input</key> <string>document</string> <key>outputLocation</key> <string>discard</string> <key>uuid</key> <string>xxxx-xxxx-xxxx-xxxx</string> </dict> </plist>

注意:事件命令若无需输出,应将outputLocation设为discard,避免输出干扰文档。

过滤器模板(以文本导入为例)

<dict> <key>name</key> <string>My Import Filter</string> <key>semanticClass</key> <string>callback.document.import</string> <key>command</key> <string>#!/bin/bash cat /dev/stdin | my_converter</string> <key>input</key> <string>document</string> <key>inputFormat</key> <string>text</string> <key>outputLocation</key> <string>replaceDocument</string> <key>outputFormat</key> <string>text</string> </dict>

编写过滤器时请遵守三条约束(由源码event_delegate_t强制):

  1. 输入单元必须是document,格式为text;
  2. 输出位置必须是replaceDocument,格式为text;
  3. 命令应保持"读 stdin、写 stdout"的纯转换形态,如不满足约束会被拒绝(见 filter.cc)。

注意事项

  • 过滤器命令建议设置hideFromUser = 1,避免出现在用户可手动触发的命令菜单中;
  • 二进制导入过滤器靠contentMatch匹配文件头选择命令,多个候选时当前版本默认取最先匹配者;
  • 事件命令遵循 scope 选择器优先级规则,多个命令竞争同一事件时可利用 scope 精确度排序。

七、小结

TextMate 2 的事件与过滤器机制,本质上是将"编辑器生命周期"开放为可编程钩子:

  • 事件(eventSpecifier)让命令在保存等时刻自动运行,适合"保存后处理"类任务;
  • 过滤器(semanticClass)让命令以管道转换器形式参与文档的打开/保存管线,适合"透明格式转换"类任务;
  • 两者均以普通tmCommand为载体,仅通过少量 plist 键(eventSpecifier、semanticClass、contentMatch等)改变其触发时机与输出语义。

掌握这套机制后,你可以为任何文件类型定制"打开即解码、保存即加密/压缩"的自动流程,也可以把项目中的构建、格式化、权限修复等动作挂接到保存事件上,充分复用 TextMate 2 的命令执行引擎与 scope 体系。


延伸阅读:本仓库相关源码与示例均可在以下路径查看:

  • 过滤器核心实现:Frameworks/file/src/filter.cc
  • 保存状态机(过滤器介入顺序):Frameworks/file/src/save.cc
  • 打开时的导入过滤器调用:Frameworks/file/src/open.cc
  • 保存完成回调与通知:Frameworks/document/src/OakDocument.mm
  • Avian Bundle 过滤器实例:Applications/TextMate/support/Bundles/Avian.tmbundle/Commands
  • 事件机制演进记录:Applications/TextMate/about/Changes.md
  • 开发工具
  • 桌面应用
  • 代码编辑器

【免费下载链接】textmate

TextMate is a graphical text editor for macOS 10.12 or later

项目地址:https://gitcode.com/gh_mirrors/te/textmate
点击查看免费下载

相关推荐

上一篇:OpenAPI-GUI用户手册:从入门到精通的完整操作教程
下一篇:Armbian可视化仪表板:OpenSearch Dashboards的深度使用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进