Block-ref 和 block-embed 是 roam research 中的关于块操作的两个概念。根据字面意思,block-ref 叫“块引用”,block-embed 叫“块嵌入”。这篇文本主要介绍如何在 emacs 中实现这两个功能。
如果你熟悉 roam research, 应该知道 block-ref 可以在其他块中插入一个块的引用,该块的引用会显示为“源块”的内容,因此 block-ref 本质是一个引用链接。而 block-embed 是在另一个块中直接插入“源块”的内容,因此 block-embed 本质上还是一段字符文本。
Block-ref 实现原理
在 emacs 中,为每一个 block 分配一个 uuid 属性 (overlay),并将块的 uuid, 块的内容以及其他属性存储在数据库中。=roam-block-ref-store= 命令存储该块的 uuid 引用,=roam-block-ref-insert= 命令插入该引用, 格式形如 “((89641396-6cff-4bd1-9d7d-08877ffb7177))”。根据引用中的 uuid 在数据库中查找该块对应的内容,然后将该引用的 display 属性设置为查询到的块的内容。
块引用是只读的,可以通过修改“源块”的内容,更新所有该块引用的显示。这个过程不难理解。修改源块的内容后,保存buffer时更新数据库。然后从数据库中获取修改后的内容,修改 display 属性值,实现 所有 block-ref 和 源块的内容同步。由于块引用同步的时机是保存buffer,因此建议设置 emacs 自动保存。
Block-embed 实现原理
每一个 block 除了存储了唯一的 uuid, 在数据库还存储了一个可以重复的 embed-id。我们将使用这个 embed-id 字段来实现 block-embed。
Embed-id 的值也用 uuid 表示,默认 uuid 等于 embed-id。使用 =roam-block-embed-store= 命令存储 embed block 的内容,=roam-block-embed-insert= 命令插入与源块相同的文本内容,同时将该块的 embed-id 设置为源块的 uuid。这样,每一个 embed block 都会有一个与源块的 uuid 值相等的 embed-id 字段。
前面提到,embed block 本质还是一块文本,因此可以修改。Block-embed 的强大之处就在于修改任意一个 embed block 或者 source block (源块),所有相关块的内容都会同步更新。
如何实现:根据当前修改的块的 uuid,在数据库中查找所有与该块的 embed-id 相同的块,获取到的这些块就是所有需要同步的。然后通过添加 =post-command-hook=, 实时更新所有块的内容。为什么这里需要实时更新,而不是像 block-ref 那样在保存时更新?因为每次保存会在数据库中更新所有块的内容,这意味着所有派生于同一个块的 embed block 的内容会被更新为最后一个缓存的 embed block,而这最后一个 embed block 很有可能不是当前修改的块,导致无法更新。实时更新可以保证所有 embed blocks 在保存时内容始终是同步的。
恢复 uuid 属性
可以预见,一个为所有块设置了 uuid 属性的文件在被 kill buffer 后,重新打开时,所有的 uuid 属性都会丢失,这会导致先前设置的所有 block-ref 和 block-embed 都将无法正常工作。因此我们需要在重新打开文件时恢复所有的 uuid 属性。使用 overlay 很容易实现。只需在每次保存时缓存所有 overlay 的范围及对应的 uuid,在打开文件时,根据缓存重新设置所有 uuid overlay 即可。
以上就是在 emacs 中实现 block-ref 和 block-embed 的主要原理。我将这些功能组织成了 emacs 插件: roam-block。详细的介绍及安装、用法请看 Github 文档。
效果演示
在使用之前先来看一看在 emacs 中的实现效果:
block-ref
block-embed
优化思路
Block-ref 需要保存 buffer 才能更新内容,可以实现像 block-embed 那样实时更新吗?可以。我已经有了大致的思路,借鉴 web 应用“前后端”的概念。每次修改 source block 内容后,先修改前端的显示,在保存时,再将后端和前端同步。
后记
从思路验证,到代码实现,花费了大约 1个月的时间。目前支持 org-mode, 后续会尝试支持 markdown。可以结合 emacs 中的 roam 插件使用,也可以单独使用。
分享整个 package 的实现思路的目的是:希望对其他 package 能够有所启发,后续我也会把这些功能集成到我的 gkroam 里。任何建议想法,欢迎留言!
最后放一下 github 仓库: