增量更新与发布
挂载的树与它的缓冲区是同一份内容的两个表示:树是事实来源,缓冲区是输出。在一次修改与缓冲区之间横着一次 flush——一条流水线,重算变化部分的样式、在边界内重排它、把结果化成行替换,并且只写这些行。本章描述这条流水线:节点修改接口、批处理与 flush、它们背后的脏位、一次 flush 的八个步骤、重排边界、绘制与它的缓存、缓冲区如何被写入、观察者收到的报告、缓冲区被外部编辑时会发生什么,以及整套机制维持的不变式。Runtime描述 ETAF 如何在这套机制之上批处理与发布;渲染模型描述它出发时的保留树。
每次修改都标记变化的节点并为它们的挂载排队;批处理之外,每次调用返回之前 flush 一次,所以单次编辑立即可见。
Function: ebox-node-put (node key value)
Section titled “Function: ebox-node-put (node key value)”把 NODE 的作者属性 KEY 设为 VALUE。
参数。 KEY 是作者属性关键字;VALUE 是该属性接受的任意值。
语义。 属性列表被复制,键被替换或追加,结果交给 ebox-node-set-props。它用于改单个属性;ebox-node-set-props 替换整个列表,重新渲染时通常调用后者。
返回。 NODE。
错误。 属性拒绝的值走与 ebox-node-set-props 相同的校验。
参见。 ebox-node-set-props、ebox-node-restyle。
Function: ebox-node-set-props (node props)
Section titled “Function: ebox-node-set-props (node props)”用 PROPS 替换 NODE 的作者属性。
参数。 PROPS 是作者属性的 plist,nil 表示清空。
语义。 列表相等时是空操作。否则属性被重新安装;节点的滚动偏移保留,改动被分类:只涉及样式属性的改动只重绘该节点,涉及结构或交互属性的改动会重排它。分类之所以重要,是因为大多数视觉更新都属于第一类。
返回。 NODE。
错误。 属性列表或取值畸形时报 ebox-error,并指出属性名。
参见。 ebox-node-put、ebox-node-restyle。
Function: ebox-node-set-text (node string)
Section titled “Function: ebox-node-set-text (node string)”把文本节点 NODE 的内容设为 STRING。
参数。 NODE 是文本节点;STRING 是新内容,可以为空。
语义。 文本相等(连属性一起比较)时是空操作,所以设置节点已经显示的值不花代价。否则载荷被替换,节点被标为重排,因为不同的字符串可能断成不同的行。原生输入会话正在编辑该节点时,值归会话所有;从外部设置它会替换会话显示的内容。
返回。 NODE。
错误。 STRING 不是字符串、或 NODE 不是文本节点时报错。
参见。 ebox-node-put、ebox-node-set-image。
Function: ebox-node-set-image (node image)
Section titled “Function: ebox-node-set-image (node image)”把图像节点 NODE 的 prepared 图像设为 IMAGE。
参数。 IMAGE 是 ebox-image-prepare 返回的值。
语义。 prepared 栅格被替换,节点被标为重排,因为不同的图像有不同的行数与宽度。准备留在这次调用之外:值在准备时就已经解码与转换,所以替换图像没有 I/O 代价。
返回。 NODE。
错误。 无;畸形的值会在节点布局时暴露。
参见。 ebox-image-prepare、ebox-node-set-text。
Function: ebox-node-set-states (node states)
Section titled “Function: ebox-node-set-states (node states)”把 NODE 的作者选择器 STATES 设为符号或字符串的列表。
语义。 状态供给状态选择器,例如写给 &:on 的规则。值被归一为名字,符号与字符串可以互换;集合不变时是空操作,变化时只把节点标为重算样式,绝不标重排。
返回。 NODE。
错误。 无。
参见。 ebox-node-restyle、ebox-node-set-props。
Function: ebox-node-restyle (node)
Section titled “Function: ebox-node-restyle (node)”在派生选择器状态变化后重算 NODE 的样式。
语义。 选择器可以匹配的某些状态是派生的而不是声明的——hover 与 focus 是内置例子——改变派生状态来源的调用方请求重算,而不是去标某个属性。节点被标为重算样式;下次 flush 重算它的样式,若几何没有变化就只重绘。
返回。 NODE。
错误。 无。
参见。 ebox-node-set-states。
Function: ebox-node-insert (parent child &optional index)
Section titled “Function: ebox-node-insert (parent child &optional index)”把 detached 的 CHILD 插入 PARENT 的 INDEX 处(默认最后),返回 CHILD。
参数。 CHILD 是节点或字符串,字符串会成为文本节点。INDEX 是子节点位置,nil 表示追加。
语义。 子节点必须是 detached 的。父级已经布局、且能吸收这次插入时,改动记录为某个位置上的结构操作,flush 因此移动后续行而不是重排整个父级。
返回。 CHILD。
错误。 已有父级的子节点、文本父级或未知 tag 报 ebox-error。
参见。 ebox-node-remove、ebox-node-move。
Function: ebox-node-remove (node)
Section titled “Function: ebox-node-remove (node)”从父级摘下 NODE 并返回它。
语义。 节点的子树忘掉自己的布局事实,同一个节点之后可以再被插入;已布局的流内节点记录带位置与高度的结构移除,flush 因此能增量地合上缺口。子树中由外部宿主持有的浮层会让那些行作废。
返回。 NODE。
错误。 没有父级的节点原样返回;非节点报 wrong-type-argument。
参见。 ebox-node-insert、ebox-node-replace-children。
Function: ebox-node-move (node index)
Section titled “Function: ebox-node-move (node index)”把 NODE 移到兄弟之间的 INDEX 位置。
语义。 节点已在该位置时是空操作。已布局的流内节点记录一次移除与一次插入,容器因此靠移动行来更新,而不是重建。根节点不能移动。
返回。 NODE。
错误。 移动根或非节点时报错。
参见。 ebox-node-insert、ebox-node-remove。
Function: ebox-node-replace-children (parent children)
Section titled “Function: ebox-node-replace-children (parent children)”用 CHILDREN 替换 PARENT 的子节点,复用已存在的节点。
参数。 CHILDREN 是节点与字符串的列表;字符串成为文本节点。
语义。 已经在父级子节点里的节点保持身份与布局状态,新节点被标为全新,被移除的节点忘掉布局。这是列表和解背后的原语:身份让一行在重新渲染后保住自己的状态。父级整体重排。
返回。 PARENT。
错误。 重复的子节点、属于其它父级的子节点、或文本父级报 ebox-error。
参见。 ebox-node-insert、ebox-node-remove。
批处理与 flush
Section titled “批处理与 flush”Macro: ebox-with-batch (&rest body)
Section titled “Macro: ebox-with-batch (&rest body)”把 BODY 作为一个更新批次运行;结束时 flush 排队的挂载。
语义。 批次里的修改只标记与排队;最外层批次结束时 flush 一次,非局部退出也一样,所以出错时被标记的一切仍会发布。批次可以嵌套,只有最外层会 flush。
返回。 BODY 的值。
参见。 ebox-call-with-batch、ebox-flush。
Function: ebox-call-with-batch (function)
Section titled “Function: ebox-call-with-batch (function)”把 FUNCTION 作为一个更新批次调用并返回它的值。
语义。 ebox-with-batch 的函数形式,适用于函数体无法被宏包住的回调。
返回。 FUNCTION 的值。
参见。 ebox-with-batch。
Function: ebox-flush ()
Section titled “Function: ebox-flush ()”让每个排队挂载的缓冲区保持最新。
语义。 每个排队的挂载 flush 一次;既没有 damage、根也没有脏位的挂载被跳过、不写任何东西,所以 flush 计数只计真正发布了内容的 flush。flush 不可重入:在 flush 内部触发的 flush 被忽略,期间产生的修改排队到下一次。批处理与钩子之外,调用方通常不需要这个函数;Ebox 会自动推进。
返回。 Nil。
错误。 无。
参见。 ebox-after-flush-functions、ebox-mount-flush-count。
标记一个节点会设置若干位,说明它需要哪类工作。样式位表示计算样式必须重算;布局位表示几何可能变化;绘制位表示只有像素变化。标记同时走到根的路径,清掉每一级祖先的固有尺寸缓存、把下一级子节点记入脏子节点列表,于是样式与布局阶段只沿通向变化的路径下探。已经知道是脏的祖先会终止这段行走。结构操作——一次插入,一次带位置与高度的移除——记录在父级上而不是记成标志,因为布局需要位置来移动行,而不是重新合成它们。
无论标记了多少节点,挂载只入队一次;队列由最外层批次或下一个推进点清空。
flush 流水线
Section titled “flush 流水线”一次 flush 对排队的挂载运行一条流水线。先取挂载的视口与自上次 flush 以来累积的 damage,判断是否有工作。样式阶段重算脏路径的计算样式;继承键与环境都未变的节点只下探到它的脏子节点,而布局相关取值保持不变的改动把节点标为重绘而不是重排。随后布局在结构边界内重排脏子树,能移动行的地方就利用记录下来的操作。锚定浮层会重新放置,因为它们的目标可能已经移动。
布局期间收集的 damage 之后被裁剪——整体变化吸收它内部更小的范围——并且每个 damage 持有者合成好的行从缓存里丢弃。剩下的被转成有序行替换 (START OLD NEW),其中 START 是应用前面替换之后的行号。变化无法表达为有序替换时——两个范围以不相容的方式重叠——flush 退化为整块重写缓冲区。否则它只写被替换的行,递增 flush 计数,保存报告,调用观察者,把缓冲区背景同步到根的颜色,再运行 after-flush 钩子。
布局设法只改变化能到达的部分。竖向 stack 通过移动后面的条目吸收子节点的高度变化,只让该子节点的行与一段有界的尾部受损;横向容器、flex 与 grid 容器、浮层宿主,以及子节点参与方式发生变化的任何父级,都会完整重排它们的子节点,因为一个条目的尺寸可能移动其它所有条目。获得或失去绝对定位的节点会在宿主之间迁移。未动的子树复用布局记录、固有尺寸、折好的段落与合成好的行,所以长列表底部的变化不会碰到顶部。
边界正是增量得以增量的原因:流水线只访问脏路径,而每个容器决定它是能吸收变化,还是必须重新摆放子节点。
绘制、缓存与 display 孪生
Section titled “绘制、缓存与 display 孪生”合成为节点的某一行生成一个扁平字符串列表——外边距、边框、内边距、内容与拼入的浮层——并把该列表缓存在节点上,行数有上限;段落改为缓存自己折好的行。字符串共享且不可变,所以没变过的行按同一性复用,它们的像素宽度也被记忆。丢弃缓存只沿 damage 持有者向上经过真正合成过它的节点这条链发生。
一个图形细节是:Emacs 对两个相邻、且携带同一个 eq display 值的字符只绘制一次。这会合并同一个 emoji 或同一条图像切片造成的连续段,所以合成对重复值在两个相等但不同的 display 对象之间交替;缓冲区保留的正是合成行用的那个对象。属性比较使用感知值的比较:face 按值、display 值按同一性,于是没变的行比较相等,又不会下探进树里。
写 buffer
Section titled “写 buffer”写入在三个层面增量。行通过每 128 行一个的稀疏 marker 定位,因此找到编辑的第一行只需一次二分加一段很短的行走,编辑之后的 marker 被平移而不是重建。编辑用一批行替换另一批行时,两端字符串共享的行原位重写,行内的 marker 因此留在原地。行内只替换公共前缀与后缀之间的字符,并且只设置不同的属性段,所以保留大部分内容的改动不动其余的文本属性,也不引发对应的重绘工作。
位置按行与列保持:point、各窗口的 point 与 window start 落在被替换行内时被跟踪并随文本移动,未被触及行内的位置自然随文本移动。换行符会拿到它前面单元格的 :extend face,盒子的背景因此延续到窗口右缘;同一个换行符还桥接多行元素的 hover 反馈。写入过程抑制只读检查、undo 记录与修改钩子,结束后恢复缓冲区的修改标志。
报告、观察者与钩子
Section titled “报告、观察者与钩子”Hook: ebox-after-flush-functions
Section titled “Hook: ebox-after-flush-functions”每次 flush 写入挂载缓冲区之后,带该缓冲区运行的异常钩子。
语义。 每次真正发布内容的 flush 后运行一次,在观察者与背景同步之后,只带缓冲区一个参数。需要看到新的已提交缓冲区、但又不应算作 flush 一部分的工作放在这里;在这里做滚动或测量,看到的是已经稳定的几何。
返回。 钩子返回值被忽略。
错误。 其中一个函数报错不会阻止其它函数。
参见。 ebox-flush、ebox-viewport-change-functions。
Function: ebox-buffer-set-observer (buffer-or-name observer)
Section titled “Function: ebox-buffer-set-observer (buffer-or-name observer)”每次 flush 之后以缓冲区和报告 plist 调用 OBSERVER。
参数。 OBSERVER 是接受两个参数的函数,nil 表示移除观察。
语义。 报告描述这次 flush 写了什么::provider(总是 ebox)、:strategy(lines 或 full)、:edits(范围数)、:lines(这些范围总共写了多少行)、:ranges(按应用顺序的 (START OLD NEW) 替换)、:flush(挂载的 flush 计数器)与 :duration-ms。观察者收到的是拷贝,且不得修改树;它用于度量与插桩。
返回。 Nil。
错误。 没有挂载的缓冲区报 user-error。
参见。 ebox-buffer-update-report、ebox-mount-flush-count。
Function: ebox-buffer-update-report (&optional buffer-or-name)
Section titled “Function: ebox-buffer-update-report (&optional buffer-or-name)”返回 BUFFER-OR-NAME 最近一次 flush 报告的拷贝,没有则 nil。
语义。 挂载保存着最近一次报告,应用可以在事后查询,而不必观察每次 flush。报告描述的就是观察者本会收到的那次 flush;该次 flush 带观察者计时时含 :duration-ms,不带观察者时只保存原始行范围,读者由此现场构造出的报告不含耗时。每次调用返回全新的拷贝。
返回。 报告 plist;首次 flush 之前为 nil。
错误。 没有挂载的缓冲区报 user-error。
参见。 ebox-buffer-set-observer。
Function: etaf-runtime-update-report (&optional runtime)
Section titled “Function: etaf-runtime-update-report (&optional runtime)”返回已挂载 RUNTIME 最近一次缓冲区更新报告,或 nil。
语义。 plist 就是引擎为最近一次提交生成的 flush 报告,与 ebox-buffer-update-report 为 Runtime 缓冲区返回的报告相同:策略、改动范围数、行数、范围与 flush 计数。应用读它来观察一次保留更新重写了变化的行、而不是整个缓冲区。
返回。 报告 plist;首次 flush 之前为 nil。
错误。 未挂载的 Runtime 报 etaf-runtime-error。
参见。 ebox-buffer-update-report、etaf-runtime-revision。
Error: etaf-observer-error
Section titled “Error: etaf-observer-error”Invalid ETAF observer operation。当观察者上下文状态以非法值构造或使用时抛出,例如不是函数的 sink、不是整数的身份。用 condition-case 捕获这个条件;失败的调用不返回值。它的父条件是 error,数据是描述违规的符号列表。
| 数据 | 原因 |
|---|---|
(Observer sink must be a function) |
报告 sink 不可调用 |
(Operation identity must be an integer) |
操作身份不是整数 |
(Runtime identity must be an integer) |
Runtime 身份不是整数 |
(Buffer name must be a string or nil) |
缓冲区名既不是字符串也不是 nil |
(Diagnostic sink must be a function) |
诊断 sink 不可调用 |
(Invalid observer context) |
对不是观察者上下文的东西操作 |
(Observed operation must be a function) |
把非函数当作被观测操作运行 |
通过挂载的 :observer 选项安装的观察者收到的是被观测操作快照,而不是裸的 Ebox 报告:格式版本、操作与 Runtime 身份、每次操作递增的序号、缓冲区名、各 provider 报告(包括上面的 Ebox flush 报告)与状态。观察者的错误与 quit 会被隔离:它们被报告给诊断 sink,永远不会到达被观测的代码,插桩因此不会破坏渲染。
外部编辑后的恢复
Section titled “外部编辑后的恢复”挂载的缓冲区不是只读的;Ebox 只在自己写入时抑制 undo 与修改钩子。来自外部的编辑——程序插入文本,用户在关掉模式的缓冲区里打字——让缓冲区与树不一致。Ebox 不会当场发现并修复它,因为树一侧没有这次编辑的记录可以应用。
发生的事是确定的:下一次写入被编辑行的 flush 会用合成内容替换该行,外部编辑恰好在树驱动变化的地方消失;flush 没有触及的行保留它们包含的内容。当缓冲区的 modified tick 表明别人写过时,按行记住的字符串被丢弃,于是下一次写入与真实缓冲区比较,而不是与过期的片段比较。整块重写——首次 flush 或 full damage——会恢复每一行。必须接受编辑的应用应当在树一侧建模编辑,原生输入会话正是这样做的。
这套机制由一小组不变式定义。保留树是唯一事实来源,缓冲区是输出。一次 flush 之后,damage 覆盖的每一行都等于树的行,缓冲区的行数等于根的高度。变化只通过 damage 到达缓冲区,所以 damage 必须覆盖发生变化的一切,且不覆盖别的。未被触及的行不被重写,未被触及的子树不被重排。一个批次无论包含多少修改都只 flush 一次。flush 不可重入。观察者只观察,绝不改变被发布的内容。
正是这些不变式让流水线可以跳过工作:任何未被报告为 damage 的行都被假定仍与树一致,这也解释了为什么外部编辑会一直存活到某次更新到达它的行。
修改错误在调用时报告:畸形的属性、错误种类的节点、已有父级的子节点、不能移动的根。它们让树保持原样,所以失败的修改绝不会发布半个更新;批处理加上 Ebox 自身的容纳意味着批次中抛出的修改仍会 flush 它之前标记的一切。
有两种情况看起来像缺陷、其实不是。报告没有 edits 的 flush 什么也没写,因为没有相关变化;flush 计数器不前进。与树不同的行是被外部编辑过、更新尚未到达的行;下一次覆盖它的更新会把树的内容放回去。两种解释都不成立时——缓冲区与树在一行上不一致,但没人编辑过——更新报告及其精确的 range 就是要看的地方。