- 开发工具
- 桌面应用
- 代码编辑器
【免费下载链接】textmate
TextMate is a graphical text editor for macOS 10.12 or later
导读
本文围绕 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 捕获事件的配置方式
捕获事件只需两步:
- 创建一个普通命令(
tmCommand); - 为命令添加
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_savescope: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 中定义了四个常量:
| 常量 | 语义类取值 | 触发时机 |
|---|---|---|
kBundleEventBinaryImport | callback.document.binary-import | 打开二进制文件时 |
kBundleEventBinaryExport | callback.document.binary-export | 保存(写出二进制)时 |
kBundleEventTextImport | callback.document.import | 打开文本文件时 |
kBundleEventTextExport | callback.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事件的完整链路:
- 用户在 TextMate 中新建
source.ruby文件并保存; - OakDocument.mm 调用
file::save(...); - save.cc 状态机依次执行文本导出过滤器、换行/编码处理、二进制导出过滤器,最后写盘;
- 写盘成功后回调
did_save(...)(save.cc),向上通知OakDocumentDidSaveNotification(OakDocument.mm); - 保存完成事件被派发,匹配
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强制):
- 输入单元必须是
document,格式为text; - 输出位置必须是
replaceDocument,格式为text; - 命令应保持"读 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
相关推荐
aiogram 过滤器(Filters)完全指南:事件路由、内置过滤器与自定义过滤器实战
aiogram 过滤器(Filters)完全指南:事件路由、内置过滤器与自定义过滤器实战 过滤器(Filters)是 aiogram 路由器(Router)将
后端即时通讯API设计CasperJS 事件与过滤器(Events & Filters)完全指南:从 EventEmitter 架构到 60+ 事件与 8 个过滤器的实战手册
CasperJS 事件与过滤器(Events & Filters)完全指南:从 EventEmitter 架构到 60+ 事件与 8 个过滤器的实战手册 Cas
测试网页爬虫Docker 事件系统完全指南:`docker events` 命令的时间过滤、过滤器与自定义格式实战
Docker 事件系统完全指南: docker events 命令的时间过滤、过滤器与自定义格式实战 docker events 是 Docker CLI 中用
容器运行时云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考