ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

xi-editor 注解(Annotations)机制解析:RFC 设计、协议实现与插件开发实战

xi-editor 注解(Annotations)机制解析:RFC 设计、协议实现与插件开发实战 xi-editor 注解Annotations机制解析RFC 设计、协议实现与插件开发实战【免费下载链接】xi-editorA modern editor with a backend written in Rust.项目地址: https://gitcode.com/gh_mirrors/xie/xi-editor导读本文围绕 xi-editor 的 RFC rfcs/2018-11-23-annotations.md 展开系统讲解 Annotations 这一核心概念的动机、数据结构、三端core / frontend / plugin协作协议以及它在仓库中的真实落地实现。通过阅读本文你将掌握为什么 selections 与 find 高亮要从样式系统迁移到注解体系AnnotationSlice、AnnotationStore与update_annotations协议如何协同工作以及如何基于plugin-lib用 Rust 编写一个输出诊断信息的插件。文中所有结论均有仓库源码与官方文档支撑可作为继续阅读 docs/docs/frontend-protocol.md 与相关源码的入口。背景一个统一表达文档区域附加信息的抽象从样式Styles到注解Annotations的演进在 xi-editor 早期设计中用户的选择selections和查找高亮find highlights是通过样式styles系统以特殊用例的方式表达的。但 RFC 明确指出这是一种长期存在的hack样式描述的是文档的外观。对同一份文档、同一套主题而言样式在理论上不应变化选择与查找高亮是瞬态transient状态与某一个视图view绑定会随用户操作频繁变化。二者本质不同。如果继续把瞬态状态塞进样式系统每次更新选择或高亮都需要使行缓存失效并重发整段文本代价高昂。RFC 提出应新增一个update级别的字段只发送新的高亮或选择状态而无需重发文本行。该设计也取代了旧版更新协议中名为update的 op 的职责RFC 原文注记该 op 可随本次变更移除。Annotations 的基本思想RFC 给出的定义非常简洁Annotations 是某种类型的数据 一组文档区域这一通用模式的泛化。一个注解annotation拥有自己的type由type决定其数据payload的结构前端决定是否处理、以及如何展示某种类型的注解。典型的目标场景包括编译器警告 / lint / 诊断信息diagnostics多人协同编辑时显示其他用户的游标可点击的外部链接版本控制状态added / modified / removed断点此外它还将承载用户的 selections 与 find highlights。RFC 声明文本样式 API 不会被实质改变但会被轻微简化——这正是仓库实际演进的路线在 frontend-protocol.md 中官方协议文档明确写道旧的保留样式 ID0 用于 selections、1 用于 find 结果仅出于向后兼容支持有限时间selections and find matches are now represented asannotations。动机四个层面的设计考量RFC 用四个小节阐述了设计动机理解它们有助于把握后续 API 的每一个细节。将视图状态与样式 span 分离如前所述选择与查找高亮属于与特定视图绑定的瞬态状态。更新它们不应使文档行缓存失效并重发文本行只需发送新的高亮/选择数据即可。这是引入独立注解通道最直接的理由。更大的前端设计自由度xi 希望被用作多种设备与形态因子的编辑器底座并允许前端设计师自由实验新的交互。注解为前端提供的是带作用域的语义信息而不声明如何展示。例如selection 应该使用主题中定义的选择颜色这类只是建议suggestion前端完全可以忽略并自行发挥。支撑新的特性诊断信息、版本控制状态等特性需要插件与前端之间的协调。以诊断为例一个简单的实现就是一组 span其 payload 为诊断级别 消息例如warning, unused import: \std::marker::PhantomData未来可以轻松扩展 payload加入一键修复quick-fix编辑或文档链接。版本控制状态则可以让 payload 标明范围是 added / modified / removed。无障碍Accessibility支持语义信息与文档内容并列存放而非混在一起使得未来支持屏幕阅读器等辅助设备更加容易。协议设计Wire 格式与数据结构update 对象中的 annotations 字段RFC 提出在更新协议中新增annotations字段它是一组 AnnotationSet 的列表表示当前视图作用域内的瞬态状态。用伪 JSON 表示// in the update object: { ops: [Op]?, annotations: [AnnotationSet]?, // other fields } // An AnnotationSet: { type: String, ranges: [[Position, Position]], payloads: [Any]?, metadata: Any? // maybe? }其中ranges中的位置很可能表示为可视行号 utf8 或 utf-16 偏移的组合payloads与ranges一一对应。metadata用于携带整组注解的元信息RFC 对是否纳入本 API 持开放态度详见后文开放问题。仓库中的最终 Wire 格式RFC 落地后官方协议文档 frontend-protocol.md 给出了update消息与AnnotationSlice的正式定义update rev?: number ops: Op[] view-id: string pristine: bool annotations: AnnotationSlice[] interface AnnotationSlice { type: find | selection | ... ranges: [[number, number, number, number]] // start_line, start_col, end_line, end_col payloads: [{}] // can be any json object or value n: number // number of ranges }与 RFC 草案相比正式定义有一处重要差异range 由[start, end]两元素变为[start_line, start_col, end_line, end_col]四元素。这一格式被 core 端 annotations.rs 中的AnnotationRange结构体以自定义 serde 序列化实现测试test_annotation_range_serialization断言了[1,3,4,1]这一紧凑的 4 元素数组格式annotations.rs。核心实现Core 端的 Annotation 类型体系RFC 草拟的核心端 Rust 结构在仓库中得到了忠实实现文件rust/core-lib/src/annotations.rs。注解类型与区间pub enum AnnotationType { Selection, Find, Other(String), }内置selection与find两种类型插件自定义类型通过Other(String)承载序列化到 wire 时分别输出字符串selection、find或自定义字符串见 annotations.rs。Annotations 与 AnnotationSlice/// A set of annotations of a given type. pub struct Annotations { pub items: SpansValue, pub annotation_type: AnnotationType, } /// A region of an Annotation. pub struct AnnotationSlice { annotation_type: AnnotationType, /// Annotation occurrences, guaranteed non-descending start order. ranges: VecAnnotationRange, /// If present, one payload per range. payloads: OptionVecValue, }Annotations是 core 内部表示以xi_rope的SpansValue存储某一类型的所有区间及其 payloadpayload 为任意 JSON 值AnnotationSlice是放到线上on the wire的类型即前文协议中的AnnotationSlice其ranges保证起始位置非降序non-descending start order便于前端顺序处理。AnnotationSlice::to_json()输出{type: ..., ranges: ..., payloads: ..., n: ...}四字段annotations.rs其中n为 ranges 数量与协议文档一致。转换 traitToAnnotationRFC 草案中的ToAnnotationtrait 在仓库中定义于 annotations.rs/// A trait for types (like Selection) that have a distinct representation /// in core but are presented to the frontend as annotations. pub trait ToAnnotation { /// Returns annotations that overlap the provided interval. fn get_annotations(self, interval: Interval, view: View, text: Rope) - AnnotationSlice; }该 trait 的意义在于Selection与Find在 core 内部有自己专用的表示多选区结构、查找匹配集合但在呈现给前端时统一转换为注解切片。两个实现分别位于selection.rsimpl ToAnnotation for Selection将可见区间内的选区 region 逐一转为AnnotationRangepayload 为Nonefind.rsimpl ToAnnotation for Find将查找出现occurrences转为 ranges并为每个 range 附带{id: find_id}作为 payload——这样前端可以区分来自多个 find 实例的高亮。按视图存储插件注解AnnotationStore插件提供的注解按视图view存储。RFC 草案中的AnnotationStore在 annotations.rs 实现为HashMapPluginId, VecAnnotations即每个插件各自保存一组按类型组织的注解集合pub struct AnnotationStore { store: HashMapPluginId, VecAnnotations, }核心操作与 RFC 草案一一对应方法语义源码位置update(source, interval, item)应用插件对某一类型注解的更新同类型则合并进对应集合annotations.rsiter_range(view, text, interval)迭代所有类型中与给定区间相交的注解转换为AnnotationSliceannotations.rsinvalidate(interval)使区间内的注解失效编辑发生时调用annotations.rsclear(plugin)移除某插件提供的全部注解annotations.rs实现细节值得注意iter_range中特意使用.filter()而非Spans::subseq()因为subseq()会过滤掉长度为 0 的 span而零宽注解如 selection 的插入点必须保留annotations.rs 注释明确说明。区间转换为AnnotationRange时通过view.offset_to_line_col(text, ...)把字节偏移换算为行/列坐标。AnnotationStore带有单元测试覆盖 update / clear 行为annotations.rs例如test_annotation_store_update验证不同插件来源的注解被独立存储。可见范围裁剪与 RFC 草案几乎一致的发送逻辑RFC 草案给出的准备更新伪代码在 view.rs 的send_update_for_plan中几乎原样落地// every time current visible range changes, annotations are sent to frontend let start_off self.offset_of_line(text, self.first_line); let end_off self.offset_of_line(text, self.first_line self.height 2); let visible_range Interval::new(start_off, end_off); let selection_annotations self.selection.get_annotations(visible_range, self, text).to_json(); let find_annotations self.find.iter().map(|f| f.get_annotations(visible_range, self, text).to_json()); let plugin_annotations self.annotations.iter_range(self, text, visible_range).map(|a| a.to_json()); let annotations iter::once(selection_annotations) .chain(find_annotations) .chain(plugin_annotations) .collect::Vec_();三类注解——selection、find、插件注解——被合并进annotations数组随client.update_view一并下发view.rs。可见范围取first_line到first_line height 2比 RFC 草案的1略宽用于滚动余量。每次可见范围变化都会重发当前可见区域的完整注解集合——这正是注解对前端无状态stateless原则的体现。前端视角无状态、类型驱动的展示模型RFC 为前端定义了三条核心规则仓库协议文档与实现均予确认1. 注解是无状态的对前端而言每次 update 都会携带与当前可见区域相交的完整注解集合前端无需自行缓存或合并。RFC 提到一种可能的优化把无状态降级为按类型无状态——收到某类型的新注解后该类型旧注解即失效。这也是future work清单中更高效失效more efficient invalidation的讨论基础。2. 每种类型都有定义的 payload 结构payload 的结构由注解类型决定。RFC 列举了三种示例类型selectionpayload 为光标在选区中的位置用[0, 1, 2]分别表示 start、end/upstream、end/downstream。仓库实现中 selection 注解的 payload 当前为Noneselection.rs该设计留待前端按需扩展linkpayload 为一个 URL 字符串diagnosticpayload 为包含 levelwarning / error / note、短消息、长详情字符串的对象。xi-editor 项目会定义一组内置类型及其 schema但插件可以自由定义新类型前端只要支持即可。3. 前端按类型决定如何展示注解类型及其文档只作提示最终展示方式完全由前端决定。RFC 以诊断警告为例GUI 前端可能选择把警告内联绘制在文本之上而 TUI 前端可能只在装订线gutter区域显示一个指示符。RFC 用三张截图Visual Studio Code、Neovim、Xcode 的错误展示说明同一语义在不同前端中的多样化呈现对应图片位于 rfcs/assets/vscode_err.png、rfcs/assets/nvim_err.png、rfcs/assets/xcode_err.png。对前端实现者而言协议文档 frontend-protocol.md 直接引用了本 RFC 作为 API 的详细描述来源二者应配合阅读。插件 APIupdate_annotations 协议与调用链协议方法RFC 提出插件通过单一协议方法增删改注解{ method: update_annotations, params: { view_id: view-id-3, rev: 4137193, type: diagnostics, range: [0, 1337], items: [ /* some annotations */], } }每个插件只能修改自己的注解每种注解类型分开存储。这与AnnotationStore的HashMapPluginId, VecAnnotations结构完全吻合——先按插件隔离再按类型隔离。仓库中的最终参数形态落地后的实际协议参数见 frontend-protocol.md略有调整区间由range拆分为startlen类型名改为annotation_typeupdate_annotations {start: 0, len: 20, spans: [{ start: 0, end: 4, data: null }], annotation_type: find, rev: 3 }update_annotations的语义是从偏移start起、长度为len的区间内更新既有注解并新增注解。插件端调用Rust plugin-lib在官方插件库 rust/plugin-lib/src/view.rs 中View::update_annotations封装了该 RPCpub fn update_annotations( self, start: usize, len: usize, annotation_spans: [DataSpan], annotation_type: AnnotationType, ) { let params json!({ plugin_id: self.plugin_id, view_id: self.view_id, start: start, len: len, rev: self.rev, spans: annotation_spans, annotation_type: annotation_type, }); self.peer.send_rpc_notification(update_annotations, params); }注意这里自动携带了plugin_id、view_id与当前修订号rev。DataSpan由{start, end, data}构成data为任意 JSON 值即 payload。Core 端接收与修订转换core 端收到该通知后经 event_context.rs 的PluginNotification::UpdateAnnotations分支转交 editor.rs 的Editor::update_annotations处理。这里有一段关键的健壮性逻辑用SpansBuilder::new(len)在[start, startlen)区间内构建SpansValue若插件给出的rev不是当前 head 修订则通过engine.try_delta_rev_head(rev)取得增量并用Transformer把start、end_offset及 span 本身变换transform到当前文本坐标——这正是 docs/docs/plugin.md 所描述的用户编辑作用于文本、插件编辑作用于富文本注解二者通过操作变换operational transforms收敛机制在注解通道上的具体应用最后调用view.update_annotations(plugin, iv, ...)view.rs写入该视图的AnnotationStore。编辑期间注解的调整RFC 明确指出编辑发生时core 会调整或移除注解但插件有责任响应用户编辑、删除或更新其既有注解。仓库中AnnotationStore::invalidate在编辑发生时会清除区间内的注解参见 annotations.rs 及Annotations::invalidate对Spans::delete_after的调用确保前端不会长期显示与当前文本不符的过期注解。示例基于 cargo check 的诊断插件RFC 以包装cargo check的 lint 插件为例完整展示了注解的生命周期插件在文件保存后于后台运行cargo check --message-formatjson捕获输出并抽取为注解集// AnnotationSet { type: diagnostic, ranges: [[420, 456]], payloads: [ { level: warning, // warning / error / info / ? message: unused import: std::marker::PhantomData, code: unused_import } ], metadata: null }注解集发送给 corecore 转发给前端按需展示。在仓库的实际 API 中第二步可写为一次update_annotations调用借助 plugin-lib 的View::update_annotationsspans数组中每个DataSpan的data即上述 payload 对象core 端将注解与用户编辑通过修订变换协调最终由send_update_for_plan把可见范围内的诊断随update消息推送给前端。若用户随后编辑了被诊断覆盖的文本core 会 invalidate 相应区间插件应重新运行检查并更新注解。RFC 还展望了 payload 的演进空间未来可为诊断附加一键修复编辑或文档链接版本控制插件则可用 payload 区分 added / modified / removed。开放问题与后续演进RFC 末尾以Questions / considerations形式记录了设计过程中悬而未决的议题其中多数已在仓库实现中给出了方向性答案metadata 处理是否在注解 API 内支持整组注解的元信息如 find 结果总数。RFC 倾向于独立的 metadata API本 API 内metadata字段标注为maybe。仓库中AnnotationSlice未包含 metadata 字段find 相关的总数/状态信息实际通过独立的find_status通知下发见 view.rs零宽zero-width注解版本控制中表示已删除区间、以及 selection 的插入点都需要零宽区间必须显式支持。仓库在iter_range中刻意保留零宽 span 已确认这一点类型检查与校验core 希望对已知类型的注解做校验避免盲目向前端转发畸形数据按需获取其他区域的注解当前只发送可见区域的注解未来可能需要 API 拉取文档其他区域的注解用到再建更高效的失效策略若用户在文档中频繁移动/编辑导致大量未变化注解被重复发送可能需要更显式的失效机制。RFC 同时指出当前完全无状态、整体重发的简单性很有吸引力编辑时注解的更新策略文本插入到 selection 内部时选区会扩展包含新文本插入到 style span 内部时会把 span 一分为二。注解内部发生编辑时该如何处理RFC 认为正确行为高度依赖注解类型可能需要按类型定义更新策略update strategy候选方案包括销毁被修改的注解或像 selection 一样扩展插件间通信远期的探索方向——注解是否也能作为插件之间传递元数据的机制多视图共享注解某些类型的注解可能在多个视图之间共享但当时 core 尚不支持多视图记为 future work。当前AnnotationStore按 view 存储的设计每个View持有一个 store见 view.rs与之对应。总结Annotations 是 xi-editor 中为文档区域附加带类型、带 payload 的瞬态语义信息的统一机制其价值在于将 selections / find 高亮等视图状态与文本样式解耦为前端提供语义化、无状态、可按类型自主决策展示方式的数据通道并成为诊断、链接、版本控制状态等插件特性的公共底座。RFC 提出的概念与协议在仓库中完整落地core 端以 annotations.rs 为核心实现selection / find 通过ToAnnotationtrait 接入插件通过update_annotationsRPCplugin-lib/src/view.rs推送注解最终由send_update_for_plan随可见范围裁剪后下发前端。建议按以下路径继续深入仓库RFC 原文rfcs/2018-11-23-annotations.md前端协议中的AnnotationSlice定义docs/docs/frontend-protocol.mdcore 端核心实现rust/core-lib/src/annotations.rsselection / find 的注解转换rust/core-lib/src/selection.rs、rust/core-lib/src/find.rs注解发送逻辑rust/core-lib/src/view.rs修订变换与插件调用链rust/core-lib/src/editor.rs、rust/core-lib/src/event_context.rs【免费下载链接】xi-editorA modern editor with a backend written in Rust.项目地址: https://gitcode.com/gh_mirrors/xie/xi-editor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

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