refactor(ebox): publish roots through TP surfaces
Move ephemeral rendering, initial buffer mounts, declarative commits, and opaque handle updates onto retained TP surfaces while keeping Ebox layout planning and runtime indexes transactionally synchronized. Verified with: make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
This commit is contained in:
parent
fad54d7fb1
commit
cdf841232a
@ -21,7 +21,7 @@ Install TP first, then install Ebox through a package manager so its declared de
|
|||||||
:bgcolor "#F4F6FB")))
|
:bgcolor "#F4F6FB")))
|
||||||
```
|
```
|
||||||
|
|
||||||
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` returns propertized text; `ebox-render-to-buffer` publishes it into a buffer; and `ebox-commit` updates an existing runtime from a fresh root tree.
|
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` materializes propertized text through an ephemeral TP surface; `ebox-render-to-buffer` mounts a retained TP surface; and `ebox-commit` atomically updates that surface from a fresh root tree. Ebox copies declarative input before assigning runtime identity, so one source tree may be mounted in multiple buffers without transferring ownership.
|
||||||
|
|
||||||
## What belongs here
|
## What belongs here
|
||||||
|
|
||||||
@ -40,8 +40,8 @@ ETAF belongs in the sibling package. Install it separately when you need the uni
|
|||||||
| `ebox.el` | Public facade and package entry point. |
|
| `ebox.el` | Public facade and package entry point. |
|
||||||
| `ebox-style.el`, `ebox-tree.el`, `ebox-measure.el` | Style, tree, and measurement models. |
|
| `ebox-style.el`, `ebox-tree.el`, `ebox-measure.el` | Style, tree, and measurement models. |
|
||||||
| `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Formatting contexts and layout algorithms. |
|
| `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Formatting contexts and layout algorithms. |
|
||||||
| `ebox-surface.el` | Pure Ebox runtime/layout projection into generic TP surface plans. |
|
| `ebox-surface.el` | Ebox candidate projection plus TP surface mount/update and atomic Ebox runtime-state participation. |
|
||||||
| `ebox-buffer-backend.el`, `ebox-incremental.el` | Buffer publication and incremental updates. |
|
| `ebox-buffer-backend.el`, `ebox-incremental.el` | Layout dirty planning, marker indexes, and specialized local update paths. |
|
||||||
| `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and tree/runtime queries. |
|
| `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and tree/runtime queries. |
|
||||||
| `ebox-native-reflow.el`, `native/` | Optional native reflow boundary. |
|
| `ebox-native-reflow.el`, `native/` | Optional native reflow boundary. |
|
||||||
| `examples/`, `tests/` | Ebox-only examples and regression tests. |
|
| `examples/`, `tests/` | Ebox-only examples and regression tests. |
|
||||||
|
|||||||
@ -24,9 +24,9 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
|
|||||||
| `ebox-layout.el` | Box, row, column, stack, concatenation, spacer, wrapping, and common formatting context. |
|
| `ebox-layout.el` | Box, row, column, stack, concatenation, spacer, wrapping, and common formatting context. |
|
||||||
| `ebox-flex.el` | Flex normalization, lines, free-space distribution, and flex rendering. |
|
| `ebox-flex.el` | Flex normalization, lines, free-space distribution, and flex rendering. |
|
||||||
| `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. |
|
| `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. |
|
||||||
| `ebox-surface.el` | Pure candidate identity and projection from Ebox layout output to generic TP surface plans. |
|
| `ebox-surface.el` | Candidate identity, projection to TP surface plans, retained mount/update, and the rollback-capable Ebox runtime-state participant. |
|
||||||
| `ebox-buffer-backend.el` | Text properties, display spaces/borders, markers, extents, replacement, and buffer mutation. |
|
| `ebox-buffer-backend.el` | Text properties, display spaces/borders, markers, extents, and specialized local patch primitives. |
|
||||||
| `ebox-incremental.el` | Runtime state, snapshots, dirty planning, owner escalation, atomic publication, and reports. |
|
| `ebox-incremental.el` | Runtime state, snapshots, dirty planning, owner escalation, pure commit preparation, and reports. |
|
||||||
| `ebox-dsl.el` | Data-oriented `.ebox` forms and lowering to public nodes. |
|
| `ebox-dsl.el` | Data-oriented `.ebox` forms and lowering to public nodes. |
|
||||||
| `ebox-selector.el` | CSS-like queries over trees and runtime handles. |
|
| `ebox-selector.el` | CSS-like queries over trees and runtime handles. |
|
||||||
| `ebox-native-reflow.el` | Optional native module loading/build commands, ABI checks, bounded sessions, and Elisp fallback. |
|
| `ebox-native-reflow.el` | Optional native module loading/build commands, ABI checks, bounded sessions, and Elisp fallback. |
|
||||||
@ -40,15 +40,18 @@ The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/e
|
|||||||
The normal data flow is:
|
The normal data flow is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Source Tree
|
Caller-owned Source Tree
|
||||||
|
-> Surface-owned Runtime Copy
|
||||||
|
-> TP Candidate Objects
|
||||||
-> Element Tree
|
-> Element Tree
|
||||||
-> Computed Style
|
-> Computed Style
|
||||||
-> Box/Formatting Context
|
-> Box/Formatting Context
|
||||||
-> Measurement + Render Context
|
-> Measurement + Render Context
|
||||||
-> Layout Fragment/Snapshot
|
-> Layout Fragment/Snapshot
|
||||||
-> Pure TP Surface Plan
|
-> Ebox Dirty/Patch Plan
|
||||||
-> Dirty/Patch Plan
|
-> TP Surface Plan
|
||||||
-> Emacs Buffer Backend
|
-> TP Atomic Publication
|
||||||
|
-> Ebox Runtime-State Participant
|
||||||
```
|
```
|
||||||
|
|
||||||
| Model | Owner | Must not own |
|
| Model | Owner | Must not own |
|
||||||
@ -58,22 +61,26 @@ Source Tree
|
|||||||
| Measurement | `ebox-measure.el` | Application state or dirty policy. |
|
| Measurement | `ebox-measure.el` | Application state or dirty policy. |
|
||||||
| Formatting Context | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Buffer edits. |
|
| Formatting Context | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Buffer edits. |
|
||||||
| Fragment/Snapshot | `ebox-fragment.el`, `ebox-incremental.el` | Source parsing or identity allocation. |
|
| Fragment/Snapshot | `ebox-fragment.el`, `ebox-incremental.el` | Source parsing or identity allocation. |
|
||||||
| Surface Projection | `ebox-surface.el` | Buffer mutation or Ebox layout decisions. |
|
| Surface Projection and Full-Root Publication | `ebox-surface.el` plus public TP surface APIs | Ebox layout decisions or generic diff execution. |
|
||||||
| Dirty/Patch | `ebox-incremental.el` | Raw measurement or direct buffer edits. |
|
| Dirty/Patch Semantics | `ebox-incremental.el` | Raw measurement or TP buffer writes. |
|
||||||
| Buffer Backend | `ebox-buffer-backend.el` | Style semantics or application state. |
|
| Generic Surface Diff/Commit | TP | Ebox geometry, dirty policy, or application state. |
|
||||||
|
| Specialized Local Backend | `ebox-buffer-backend.el` | Full-root mount/commit ownership, style semantics, or application state. |
|
||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
- Public Ebox nodes are data; `ebox--*` names are private.
|
- Public Ebox nodes are data; `ebox--*` names are private.
|
||||||
- A one-element horizontal list such as `'(420)` denotes pixels; ordinary horizontal numbers denote character columns.
|
- A one-element horizontal list such as `'(420)` denotes pixels; ordinary horizontal numbers denote character columns.
|
||||||
- `ebox-render` does not publish to a buffer; `ebox-render-to-buffer` owns initial publication; `ebox-commit` owns declarative replacement.
|
- `ebox-render` uses an ephemeral TP surface and does not publish to a buffer; `ebox-render-to-buffer` mounts a retained TP surface; `ebox-commit` prepares Ebox semantics and updates that surface through TP.
|
||||||
|
- Declarative input remains caller-owned. Live mount/commit assigns identity only on a surface-owned copy, so the same source can back multiple buffers.
|
||||||
|
- Logical `:id` values resolve through `ebox-region-resolve` to opaque surface-scoped handles. Handles, not source-tree numeric ids, distinguish the same logical region mounted in different buffers.
|
||||||
- A failed candidate leaves the previous buffer, runtime identity, and report intact.
|
- A failed candidate leaves the previous buffer, runtime identity, and report intact.
|
||||||
- Keys are local to siblings; visible strings are never used as identity.
|
- Keys are local to siblings; visible strings are never used as identity.
|
||||||
- `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`.
|
- `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`.
|
||||||
- Buffer coordinates belong to the generation that produced them and must be refreshed after mutation.
|
- Buffer coordinates belong to the generation that produced them and must be refreshed after mutation.
|
||||||
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
|
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
|
||||||
- Loading Ebox never builds or installs the optional Rust module.
|
- Loading Ebox never builds or installs the optional Rust module.
|
||||||
- The Phase 7 projector assigns TP candidate identity before layout, emits plans containing no markers, positions, buffers, or TP runtime handles, and is byte-for-byte and property-for-property equivalent to `ebox-render`. Public live entry points still use the existing Ebox backend until the next cutover phase.
|
- Phase 8 is active: `ebox-render-to-buffer` and declarative `ebox-commit` use TP as their only live publication boundary, while Ebox runtime state joins the same rollback-capable transaction. Existing specialized numeric region, selector, viewport, and scroll update publishers remain until the local-update cutover; they must not become an alternate full-root path.
|
||||||
|
- Successful full-root reports preserve the Ebox semantic strategy and planned publication scope, then add `:publication-scope tp-surface`, TP operation counts, surface revision, and retained-object reconciliation counts.
|
||||||
|
|
||||||
## Grid contract
|
## Grid contract
|
||||||
|
|
||||||
|
|||||||
@ -24,9 +24,9 @@
|
|||||||
| `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 |
|
| `ebox-layout.el` | box、row、column、stack、concat、spacer、换行和通用 formatting context。 |
|
||||||
| `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 |
|
| `ebox-flex.el` | flex 归一化、line、剩余空间分配和 flex 渲染。 |
|
||||||
| `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 |
|
| `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 |
|
||||||
| `ebox-surface.el` | Ebox 候选 identity,以及从布局输出到通用 TP surface plan 的纯投影。 |
|
| `ebox-surface.el` | 候选 identity、向 TP surface plan 的投影、retained mount/update,以及支持 rollback 的 Ebox runtime-state participant。 |
|
||||||
| `ebox-buffer-backend.el` | text property、display space/border、marker、extent、替换和 buffer 变更。 |
|
| `ebox-buffer-backend.el` | text property、display space/border、marker、extent 与专用局部 patch primitive。 |
|
||||||
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、原子发布和报告。 |
|
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、纯 commit 准备和报告。 |
|
||||||
| `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 |
|
| `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 |
|
||||||
| `ebox-selector.el` | 对树和 runtime handle 的 CSS-like 查询。 |
|
| `ebox-selector.el` | 对树和 runtime handle 的 CSS-like 查询。 |
|
||||||
| `ebox-native-reflow.el` | 可选 native 模块加载/构建、ABI 校验、受限 session 和 Elisp fallback。 |
|
| `ebox-native-reflow.el` | 可选 native 模块加载/构建、ABI 校验、受限 session 和 Elisp fallback。 |
|
||||||
@ -40,15 +40,18 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
|||||||
正常数据流是:
|
正常数据流是:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Source Tree
|
调用者拥有的 Source Tree
|
||||||
|
-> Surface 拥有的 Runtime Copy
|
||||||
|
-> TP Candidate Objects
|
||||||
-> Element Tree
|
-> Element Tree
|
||||||
-> Computed Style
|
-> Computed Style
|
||||||
-> Box/Formatting Context
|
-> Box/Formatting Context
|
||||||
-> Measurement + Render Context
|
-> Measurement + Render Context
|
||||||
-> Layout Fragment/Snapshot
|
-> Layout Fragment/Snapshot
|
||||||
-> 纯 TP Surface Plan
|
-> Ebox Dirty/Patch Plan
|
||||||
-> Dirty/Patch Plan
|
-> TP Surface Plan
|
||||||
-> Emacs Buffer Backend
|
-> TP 原子发布
|
||||||
|
-> Ebox Runtime-State Participant
|
||||||
```
|
```
|
||||||
|
|
||||||
| 模型 | Owner | 不得拥有 |
|
| 模型 | Owner | 不得拥有 |
|
||||||
@ -58,22 +61,26 @@ Source Tree
|
|||||||
| Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 |
|
| Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 |
|
||||||
| Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 |
|
| Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 |
|
||||||
| Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 |
|
| Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 |
|
||||||
| Surface Projection | `ebox-surface.el` | Buffer 变更或 Ebox 布局决策。 |
|
| Surface 投影与整根发布 | `ebox-surface.el` 加公共 TP surface API | Ebox 布局决策或通用 diff 执行。 |
|
||||||
| Dirty/Patch | `ebox-incremental.el` | 原始测量或直接 buffer 编辑。 |
|
| Dirty/Patch 语义 | `ebox-incremental.el` | 原始测量或 TP buffer 写入。 |
|
||||||
| Buffer Backend | `ebox-buffer-backend.el` | 样式语义或应用状态。 |
|
| 通用 Surface Diff/Commit | TP | Ebox 几何、dirty 策略或应用状态。 |
|
||||||
|
| 专用局部 Backend | `ebox-buffer-backend.el` | 整根 mount/commit 所有权、样式语义或应用状态。 |
|
||||||
|
|
||||||
## 不变量
|
## 不变量
|
||||||
|
|
||||||
- 公共 Ebox 节点是数据;`ebox--*` 名称是私有实现。
|
- 公共 Ebox 节点是数据;`ebox--*` 名称是私有实现。
|
||||||
- `'(420)` 这样的单元素横向 list 表示像素;普通横向数字表示字符列。
|
- `'(420)` 这样的单元素横向 list 表示像素;普通横向数字表示字符列。
|
||||||
- `ebox-render` 不发布到 buffer;`ebox-render-to-buffer` 负责首次发布;`ebox-commit` 负责声明式替换。
|
- `ebox-render` 使用临时 TP surface,不发布到 buffer;`ebox-render-to-buffer` 挂载 retained TP surface;`ebox-commit` 准备 Ebox 语义后通过 TP 更新该 surface。
|
||||||
|
- 声明式输入始终由调用者拥有。live mount/commit 只在 surface-owned copy 上分配 identity,因此同一 source 可以挂载到多个 buffer。
|
||||||
|
- 逻辑 `:id` 通过 `ebox-region-resolve` 解析为不透明、surface-scoped 的 handle;同一逻辑 region 挂载到多个 buffer 时,由 handle 而不是 source-tree 数字 id 区分。
|
||||||
- 候选失败时必须保留之前的 buffer、runtime identity 和报告。
|
- 候选失败时必须保留之前的 buffer、runtime identity 和报告。
|
||||||
- Key 只在兄弟节点中有效;不能用可见字符串作为 identity。
|
- Key 只在兄弟节点中有效;不能用可见字符串作为 identity。
|
||||||
- `owner-rerender` 范围大于 `span-patch`,`span-patch` 大于 `paint-patch`。
|
- `owner-rerender` 范围大于 `span-patch`,`span-patch` 大于 `paint-patch`。
|
||||||
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
||||||
- Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。
|
- Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。
|
||||||
- 加载 Ebox 不会构建或安装可选 Rust 模块。
|
- 加载 Ebox 不会构建或安装可选 Rust 模块。
|
||||||
- Phase 7 projector 在布局前分配 TP candidate identity,输出不含 marker、position、buffer 或 TP runtime handle 的 plan,并与 `ebox-render` 保持逐字节、逐属性等价。公开 live 入口在下一阶段切换前仍使用现有 Ebox backend。
|
- 当前已进入 Phase 8:`ebox-render-to-buffer` 与声明式 `ebox-commit` 只通过 TP 这一条 live 发布边界,Ebox runtime state 在同一个可 rollback transaction 中参与提交。现有的数字 region、selector、viewport 与 scroll 专用局部 publisher 暂时保留到局部更新切换阶段,但不得成为第二条整根发布路径。
|
||||||
|
- 成功的整根报告会保留 Ebox 语义策略与 planned publication scope,再加入 `:publication-scope tp-surface`、TP operation 数、surface revision 和 retained-object reconciliation 统计。
|
||||||
|
|
||||||
## Grid 合同
|
## Grid 合同
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,10 @@ This document defines the low-level publication contract. It is independent of E
|
|||||||
1. The caller owns the fresh source tree.
|
1. The caller owns the fresh source tree.
|
||||||
2. `ebox-tree.el` owns identity, keys, traversal, and snapshots.
|
2. `ebox-tree.el` owns identity, keys, traversal, and snapshots.
|
||||||
3. `ebox-style.el`, `ebox-measure.el`, and the layout modules own normalization, measurement, and geometry.
|
3. `ebox-style.el`, `ebox-measure.el`, and the layout modules own normalization, measurement, and geometry.
|
||||||
4. `ebox-incremental.el` owns dirty classification, patch planning, candidate publication, and update reports.
|
4. `ebox-incremental.el` owns dirty classification, patch planning, pure declarative commit preparation, and Ebox report semantics.
|
||||||
5. `ebox-buffer-backend.el` is the only owner of text-property and buffer mutation.
|
5. `ebox-surface.el` projects retained candidates and joins Ebox runtime-state publication to the TP transaction.
|
||||||
|
6. TP owns generic retained-surface reconciliation, buffer diff execution, revision changes, and rollback for initial mount and declarative full-root commit.
|
||||||
|
7. `ebox-buffer-backend.el` owns Ebox marker/index primitives and the specialized local numeric region, viewport, and scroll publishers that have not yet crossed the local-update cutover.
|
||||||
|
|
||||||
No layer may infer application state from visible buffer text. No public caller may mutate a published tree in place.
|
No layer may infer application state from visible buffer text. No public caller may mutate a published tree in place.
|
||||||
|
|
||||||
@ -17,23 +19,25 @@ No layer may infer application state from visible buffer text. No public caller
|
|||||||
```text
|
```text
|
||||||
fresh root
|
fresh root
|
||||||
-> validate and normalize
|
-> validate and normalize
|
||||||
-> reconcile keys and identity
|
-> copy into surface-owned candidate state
|
||||||
|
-> reconcile TP objects, keys, and Ebox identity
|
||||||
-> measure/layout candidate
|
-> measure/layout candidate
|
||||||
-> classify dirty owners
|
-> classify dirty owners
|
||||||
-> render candidate spans
|
-> produce TP surface plan plus Ebox report base
|
||||||
-> publish atomically
|
-> TP prepares and atomically publishes buffer changes
|
||||||
-> refresh snapshots and report
|
-> Ebox transaction participant publishes matching runtime indexes/report
|
||||||
|
-> retire superseded markers and timers after success
|
||||||
```
|
```
|
||||||
|
|
||||||
The candidate is either fully published or discarded. A failed candidate preserves the previous buffer text, properties, runtime identity, scroll state, and last successful report.
|
The candidate is either fully published or discarded. A failed render, TP write, Ebox participant, or publication callback preserves the previous buffer text, properties, TP revision, Ebox runtime identity, scroll state, and last successful report. If the target buffer is killed during publication, teardown is authoritative and rollback never resurrects it.
|
||||||
|
|
||||||
## Patch order
|
## Patch order
|
||||||
|
|
||||||
The planner prefers `paint-patch`, then `span-patch`, then `owner-rerender`, and finally `root-rerender` when geometry or identity makes a smaller operation unsafe. A patch must not silently widen its scope; the report records the selected strategy, dirty keys, owners, and patch operations.
|
The planner prefers `paint-patch`, then `span-patch`, then `owner-rerender`, and finally `root-rerender` when geometry or identity makes a smaller operation unsafe. A patch must not silently widen its semantic scope. Declarative full-root reports preserve that Ebox strategy and planned scope while separately recording TP's actual surface operations and revision.
|
||||||
|
|
||||||
## Identity and coordinates
|
## Identity and coordinates
|
||||||
|
|
||||||
Keys are local to a sibling collection. Region and host-ref positions are generation-bound; after a buffer mutation, callers must obtain fresh positions from the public accessor. Buffer markers, display spans, and text properties are backend facts, not source-tree identity.
|
Keys are local to a sibling collection. Declarative source trees never own live TP objects, markers, or buffer coordinates. A live logical `:id` resolves through `ebox-region-resolve` to an opaque handle tied to one TP surface object; mounting the same source in two buffers therefore yields distinct handles. Region and host-ref positions are generation-bound; after a buffer mutation, callers must obtain fresh positions from the public accessor. Buffer markers, display spans, and text properties are publication facts, not source-tree identity.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,10 @@
|
|||||||
1. 调用者拥有新的 source tree。
|
1. 调用者拥有新的 source tree。
|
||||||
2. `ebox-tree.el` 拥有 identity、key、遍历和 snapshot。
|
2. `ebox-tree.el` 拥有 identity、key、遍历和 snapshot。
|
||||||
3. `ebox-style.el`、`ebox-measure.el` 与布局模块拥有归一化、测量和几何。
|
3. `ebox-style.el`、`ebox-measure.el` 与布局模块拥有归一化、测量和几何。
|
||||||
4. `ebox-incremental.el` 拥有 dirty 分类、patch 规划、候选发布和更新报告。
|
4. `ebox-incremental.el` 拥有 dirty 分类、patch 规划、纯声明式 commit 准备和 Ebox 报告语义。
|
||||||
5. `ebox-buffer-backend.el` 是 text property 与 buffer 变更的唯一 owner。
|
5. `ebox-surface.el` 投影 retained candidate,并让 Ebox runtime-state 发布加入 TP transaction。
|
||||||
|
6. TP 拥有首次 mount 与声明式整根 commit 的通用 retained-surface reconciliation、buffer diff 执行、revision 变更和 rollback。
|
||||||
|
7. `ebox-buffer-backend.el` 拥有 Ebox marker/index primitive,以及尚未完成局部更新切换的数字 region、viewport 与 scroll 专用 publisher。
|
||||||
|
|
||||||
任何层都不能从可见 buffer 文本推断应用状态;公共调用者不能原地修改已经发布的树。
|
任何层都不能从可见 buffer 文本推断应用状态;公共调用者不能原地修改已经发布的树。
|
||||||
|
|
||||||
@ -17,23 +19,25 @@
|
|||||||
```text
|
```text
|
||||||
新的根树
|
新的根树
|
||||||
-> 校验与归一化
|
-> 校验与归一化
|
||||||
-> reconciliation key 与 identity
|
-> 复制到 surface-owned candidate state
|
||||||
|
-> reconciliation TP object、key 与 Ebox identity
|
||||||
-> 测量/布局候选
|
-> 测量/布局候选
|
||||||
-> 分类 dirty owner
|
-> 分类 dirty owner
|
||||||
-> 渲染候选 span
|
-> 生成 TP surface plan 与 Ebox report base
|
||||||
-> 原子发布
|
-> TP 准备并原子发布 buffer 变更
|
||||||
-> 刷新 snapshot 与报告
|
-> Ebox transaction participant 发布匹配的 runtime index/report
|
||||||
|
-> 成功后退役旧 marker 与 timer
|
||||||
```
|
```
|
||||||
|
|
||||||
候选要么完整发布,要么丢弃。候选失败时必须保留之前的 buffer 文本、属性、runtime identity、滚动状态和最近一次成功报告。
|
候选要么完整发布,要么丢弃。render、TP 写入、Ebox participant 或发布回调失败时,必须保留之前的 buffer 文本、属性、TP revision、Ebox runtime identity、滚动状态和最近一次成功报告。如果目标 buffer 在发布期间被 kill,teardown 是最终状态,rollback 绝不能把它复活。
|
||||||
|
|
||||||
## Patch 顺序
|
## Patch 顺序
|
||||||
|
|
||||||
规划器优先使用 `paint-patch`,再使用 `span-patch`、`owner-rerender`,最后在几何或 identity 使小操作不安全时使用 `root-rerender`。Patch 不得静默扩大范围;报告要记录策略、dirty key、owner 和 patch 操作。
|
规划器优先使用 `paint-patch`,再使用 `span-patch`、`owner-rerender`,最后在几何或 identity 使小操作不安全时使用 `root-rerender`。Patch 不得静默扩大语义范围。声明式整根报告保留该 Ebox strategy 与 planned scope,同时单独记录 TP 的实际 surface operation 和 revision。
|
||||||
|
|
||||||
## Identity 与坐标
|
## Identity 与坐标
|
||||||
|
|
||||||
Key 只在同级兄弟集合内有效。Region 与 host-ref 位置绑定于 generation;buffer 变更后调用者必须通过公共 accessor 重新获得位置。Buffer marker、display span 和 text property 是 backend 事实,不是 source tree identity。
|
Key 只在同级兄弟集合内有效。声明式 source tree 不拥有 live TP object、marker 或 buffer 坐标。live 逻辑 `:id` 通过 `ebox-region-resolve` 解析为绑定某个 TP surface object 的不透明 handle;同一 source 挂载到两个 buffer 时会得到不同 handle。Region 与 host-ref 位置绑定于 generation;buffer 变更后调用者必须通过公共 accessor 重新获得位置。Buffer marker、display span 和 text property 是发布事实,不是 source tree identity。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@ -68,17 +68,17 @@ Grid tracks can be fixed, fractional, `auto`, `minmax`, or repeated. Explicit pl
|
|||||||
|
|
||||||
## 5. Render text or a buffer
|
## 5. Render text or a buffer
|
||||||
|
|
||||||
`ebox-render` is pure with respect to buffers and returns propertized text. `ebox-render-to-buffer` owns the initial buffer publication:
|
`ebox-render` is pure with respect to buffers and returns propertized text through an ephemeral TP surface. `ebox-render-to-buffer` copies the declarative source and mounts a retained TP surface for initial publication:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(let ((node (ebox-column
|
(let ((node (ebox-column
|
||||||
(ebox-create :content "Ready" :width '(240))
|
(ebox-create :id "status" :content "Ready" :width '(240))
|
||||||
(ebox-create :content "Rendered by Ebox"))))
|
(ebox-create :content "Rendered by Ebox"))))
|
||||||
(ebox-render node)
|
(ebox-render node)
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" node))
|
(ebox-render-to-buffer "*Ebox Demo*" node))
|
||||||
```
|
```
|
||||||
|
|
||||||
The returned text carries display, face, region, and identity properties needed by Ebox. Do not edit those properties by hand.
|
The returned text carries display, face, region, and identity properties needed by Ebox. Do not edit those properties by hand. The source node remains caller-owned and can be mounted in more than one buffer; each buffer receives independent runtime identity and state.
|
||||||
|
|
||||||
## 6. Update an existing buffer
|
## 6. Update an existing buffer
|
||||||
|
|
||||||
@ -91,17 +91,24 @@ Build a fresh root tree and commit it to the existing buffer:
|
|||||||
(ebox-create :content "Updated" :key 'title :width '(240))))
|
(ebox-create :content "Updated" :key 'title :width '(240))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Ebox compares stable keys and region identity, chooses the smallest safe patch, and records a report:
|
Ebox compares stable keys and region identity, prepares the semantic dirty/owner plan, and asks TP to atomically publish the new retained surface and Ebox runtime state. It records both the Ebox plan and TP execution summary:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-buffer-update-report "*Ebox Demo*")
|
(ebox-buffer-update-report "*Ebox Demo*")
|
||||||
```
|
```
|
||||||
|
|
||||||
If a candidate cannot be proven safe, Ebox escalates to an owner or root rerender. A failed commit must leave the previously published tree and buffer intact.
|
If a candidate cannot be proven safe, Ebox escalates to an owner or root rerender. A failed render, publication, runtime-state swap, or publication callback leaves the previous buffer, TP surface, Ebox runtime state, and last successful report intact.
|
||||||
|
|
||||||
## 7. Selectors and handles
|
## 7. Selectors and handles
|
||||||
|
|
||||||
Selectors query the rendered tree and return public handles. They do not edit the buffer or own application state. Prefer explicit keys and refs for application identity; use selectors for inspection and bounded updates.
|
Selectors query the rendered tree and return public match records. They do not own application state. Give an editable box a logical `:id`, resolve it in one live buffer, and pass the opaque surface-scoped handle to `ebox-region-update`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(let ((handle (ebox-region-resolve "*Ebox Demo*" "status")))
|
||||||
|
(ebox-region-update handle :content "Ready" :color "#166534"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The same logical id in two buffers resolves to two different handles, so updating one surface cannot accidentally mutate the other. A handle becomes stale when its retained object is removed or its buffer is killed. Prefer `ebox-region-resolve` handles for new multi-buffer code; numeric region ids remain an internal/legacy update form during the local-update cutover.
|
||||||
|
|
||||||
## 8. Standalone `.ebox` files
|
## 8. Standalone `.ebox` files
|
||||||
|
|
||||||
|
|||||||
@ -68,17 +68,17 @@ Grid 轨道支持固定值、分数、`auto`、`minmax` 和重复轨道。显式
|
|||||||
|
|
||||||
## 5. 渲染文本或 buffer
|
## 5. 渲染文本或 buffer
|
||||||
|
|
||||||
`ebox-render` 相对于 buffer 是纯函数,返回带属性文本;`ebox-render-to-buffer` 负责首次发布:
|
`ebox-render` 相对于 buffer 是纯函数,通过临时 TP surface 返回带属性文本;`ebox-render-to-buffer` 会复制声明式 source,并挂载 retained TP surface 完成首次发布:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(let ((node (ebox-column
|
(let ((node (ebox-column
|
||||||
(ebox-create :content "Ready" :width '(240))
|
(ebox-create :id "status" :content "Ready" :width '(240))
|
||||||
(ebox-create :content "Rendered by Ebox"))))
|
(ebox-create :content "Rendered by Ebox"))))
|
||||||
(ebox-render node)
|
(ebox-render node)
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" node))
|
(ebox-render-to-buffer "*Ebox Demo*" node))
|
||||||
```
|
```
|
||||||
|
|
||||||
返回文本带有 Ebox 所需的 display、face、region 和 identity 属性,不要手动修改这些属性。
|
返回文本带有 Ebox 所需的 display、face、region 和 identity 属性,不要手动修改这些属性。source 节点仍由调用者拥有,可以挂载到多个 buffer;每个 buffer 都获得独立的 runtime identity 与状态。
|
||||||
|
|
||||||
## 6. 更新已有 buffer
|
## 6. 更新已有 buffer
|
||||||
|
|
||||||
@ -91,17 +91,24 @@ Grid 轨道支持固定值、分数、`auto`、`minmax` 和重复轨道。显式
|
|||||||
(ebox-create :content "Updated" :key 'title :width '(240))))
|
(ebox-create :content "Updated" :key 'title :width '(240))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Ebox 会比较稳定 key 和 region identity,选择最小且安全的 patch,并记录报告:
|
Ebox 会比较稳定 key 和 region identity,准备 dirty/owner 语义计划,再由 TP 原子发布新的 retained surface 与 Ebox runtime 状态。报告同时保留 Ebox 计划与 TP 执行摘要:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-buffer-update-report "*Ebox Demo*")
|
(ebox-buffer-update-report "*Ebox Demo*")
|
||||||
```
|
```
|
||||||
|
|
||||||
无法证明安全时会提升到 owner 或 root rerender;候选提交失败必须保留之前已发布的树和 buffer。
|
无法证明安全时会提升到 owner 或 root rerender;render、发布、runtime 状态交换或发布回调失败时,之前的 buffer、TP surface、Ebox runtime 状态和最近一次成功报告都必须保持不变。
|
||||||
|
|
||||||
## 7. Selector 与 handle
|
## 7. Selector 与 handle
|
||||||
|
|
||||||
Selector 查询已渲染的树并返回公共 handle;它不编辑 buffer,也不拥有应用状态。应用 identity 优先使用显式 key 和 ref;selector 适合检查和有限更新。
|
Selector 查询已渲染的树并返回公共匹配记录,但不拥有应用状态。给可编辑 box 设置逻辑 `:id`,在某个 live buffer 中解析它,再把不透明、surface-scoped 的 handle 交给 `ebox-region-update`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(let ((handle (ebox-region-resolve "*Ebox Demo*" "status")))
|
||||||
|
(ebox-region-update handle :content "Ready" :color "#166534"))
|
||||||
|
```
|
||||||
|
|
||||||
|
两个 buffer 中相同的逻辑 id 会解析为两个不同 handle,因此更新一个 surface 不会误改另一个。retained object 被删除或 buffer 被 kill 后,原 handle 会变 stale。新的多 buffer 代码应优先使用 `ebox-region-resolve` handle;在局部更新执行器切换完成前,数字 region id 仍作为内部/旧式更新形式保留。
|
||||||
|
|
||||||
## 8. 独立 `.ebox` 文件
|
## 8. 独立 `.ebox` 文件
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,7 @@
|
|||||||
(declare-function ebox--smooth-scroll-stop
|
(declare-function ebox--smooth-scroll-stop
|
||||||
"ebox" (region-id))
|
"ebox" (region-id))
|
||||||
(declare-function ebox--install-buffer-scroll-content-span-template
|
(declare-function ebox--install-buffer-scroll-content-span-template
|
||||||
"ebox" (buffer template start))
|
"ebox" (buffer template start &optional defer-window-region-set))
|
||||||
(declare-function ebox--node-children
|
(declare-function ebox--node-children
|
||||||
"ebox" (node))
|
"ebox" (node))
|
||||||
(declare-function ebox--root-region-box
|
(declare-function ebox--root-region-box
|
||||||
@ -250,6 +250,10 @@ user code or signal after mutation.")
|
|||||||
'ebox-incremental-before-runtime-mutation-hook buffer kind)))
|
'ebox-incremental-before-runtime-mutation-hook buffer kind)))
|
||||||
|
|
||||||
(defvar ebox--buffer-render-state-table)
|
(defvar ebox--buffer-render-state-table)
|
||||||
|
(defvar ebox-surface--buffer-surface)
|
||||||
|
|
||||||
|
(declare-function tp-surface-client-state "tp-surface" (surface))
|
||||||
|
(declare-function tp-surface-live-p "tp-surface" (surface))
|
||||||
|
|
||||||
(defvar ebox-render-cache-max-entries 2048
|
(defvar ebox-render-cache-max-entries 2048
|
||||||
"Maximum persistent render cache entries retained per buffer.
|
"Maximum persistent render cache entries retained per buffer.
|
||||||
@ -392,7 +396,7 @@ Kept as a narrow predicate for callers that need box-only sizing semantics."
|
|||||||
(while plist
|
(while plist
|
||||||
(let ((key (pop plist))
|
(let ((key (pop plist))
|
||||||
(value (pop plist)))
|
(value (pop plist)))
|
||||||
(unless (eq key :render-cache)
|
(unless (memq key '(:render-cache :surface-object))
|
||||||
(push key signature)
|
(push key signature)
|
||||||
(push (ebox--render-cache-value-signature value) signature))))
|
(push (ebox--render-cache-value-signature value) signature))))
|
||||||
(nreverse signature)))
|
(nreverse signature)))
|
||||||
@ -968,7 +972,14 @@ Keys are region-id, values are plists with :scroll-offset, :content-lines, etc."
|
|||||||
(eq buffer
|
(eq buffer
|
||||||
(car ebox-incremental--buffer-render-state-override)))
|
(car ebox-incremental--buffer-render-state-override)))
|
||||||
(cdr ebox-incremental--buffer-render-state-override)
|
(cdr ebox-incremental--buffer-render-state-override)
|
||||||
(gethash buffer ebox--buffer-render-state-table)))
|
(let ((surface
|
||||||
|
(and (buffer-live-p buffer)
|
||||||
|
(boundp 'ebox-surface--buffer-surface)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
ebox-surface--buffer-surface))))
|
||||||
|
(if (and surface (tp-surface-live-p surface))
|
||||||
|
(tp-surface-client-state surface)
|
||||||
|
(gethash buffer ebox--buffer-render-state-table)))))
|
||||||
|
|
||||||
(defun ebox--buffer-update-report (buffer)
|
(defun ebox--buffer-update-report (buffer)
|
||||||
"Return the update report stored in BUFFER's render state."
|
"Return the update report stored in BUFFER's render state."
|
||||||
@ -4414,7 +4425,8 @@ prepared proof and publication cannot render, promote owners, or fall back."
|
|||||||
(plist-get root-metadata
|
(plist-get root-metadata
|
||||||
:scroll-content-span-template)
|
:scroll-content-span-template)
|
||||||
(caar (plist-get (car results)
|
(caar (plist-get (car results)
|
||||||
:new-buffer-spans)))))
|
:new-buffer-spans))
|
||||||
|
t)))
|
||||||
(incremental-scroll-refresh
|
(incremental-scroll-refresh
|
||||||
(and (eq refresh-scope :incremental-root)
|
(and (eq refresh-scope :incremental-root)
|
||||||
(plist-get (car results)
|
(plist-get (car results)
|
||||||
@ -6331,6 +6343,44 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements."
|
|||||||
root node-id replacement index))))
|
root node-id replacement index))))
|
||||||
root)))))
|
root)))))
|
||||||
|
|
||||||
|
(defun ebox-incremental-consume-candidate (buffer-or-name candidate)
|
||||||
|
"Consume CANDIDATE for BUFFER-OR-NAME and return its pure surface input.
|
||||||
|
The returned plist contains the prepared root, Ebox plan report, identity
|
||||||
|
policy, and runtime state overrides needed by the TP surface publisher.
|
||||||
|
CANDIDATE is sealed before target and staleness validation, so every attempted
|
||||||
|
commit consumes it even when later preparation fails."
|
||||||
|
(unless (ebox-candidate-p candidate)
|
||||||
|
(error "Ebox logical commit requires an Ebox candidate"))
|
||||||
|
(when (ebox-candidate--sealed-p candidate)
|
||||||
|
(error "Ebox candidate is already sealed"))
|
||||||
|
(setf (ebox-candidate--sealed-p candidate) t)
|
||||||
|
(let ((buffer (get-buffer buffer-or-name))
|
||||||
|
(old-state (ebox-candidate--base-state candidate)))
|
||||||
|
(unless (buffer-live-p buffer)
|
||||||
|
(error "Ebox candidate commit requires a live buffer: %S"
|
||||||
|
buffer-or-name))
|
||||||
|
(ebox-incremental--candidate-assert-current candidate buffer old-state)
|
||||||
|
(ebox-incremental--surface-commit-input
|
||||||
|
buffer old-state
|
||||||
|
(ebox-incremental--prepare-logical-candidate
|
||||||
|
buffer old-state candidate))))
|
||||||
|
|
||||||
|
(defun ebox-incremental-prepare-root-commit (buffer-or-name next-root)
|
||||||
|
"Prepare declarative NEXT-ROOT for TP publication in BUFFER-OR-NAME.
|
||||||
|
Return the same pure surface-input shape as
|
||||||
|
`ebox-incremental-consume-candidate'."
|
||||||
|
(let* ((buffer (get-buffer buffer-or-name))
|
||||||
|
(old-state (and buffer (ebox--buffer-render-state buffer))))
|
||||||
|
(unless (buffer-live-p buffer)
|
||||||
|
(error "Ebox declarative commit requires a live buffer: %S"
|
||||||
|
buffer-or-name))
|
||||||
|
(unless old-state
|
||||||
|
(error "Ebox buffer has no rendered runtime: %S" buffer))
|
||||||
|
(ebox-incremental--surface-commit-input
|
||||||
|
buffer old-state
|
||||||
|
(ebox-incremental--prepare-declarative-root
|
||||||
|
buffer old-state next-root))))
|
||||||
|
|
||||||
(defun ebox-incremental--hash-keys (table)
|
(defun ebox-incremental--hash-keys (table)
|
||||||
"Return the keys currently present in hash TABLE."
|
"Return the keys currently present in hash TABLE."
|
||||||
(let (keys)
|
(let (keys)
|
||||||
@ -8334,6 +8384,123 @@ All owner promotion and rendering finishes before this function returns."
|
|||||||
promoted)))))))
|
promoted)))))))
|
||||||
complete)))
|
complete)))
|
||||||
|
|
||||||
|
(defun ebox-incremental--prepared-plan-result (op)
|
||||||
|
"Return publication metadata predicted by prepared patch OP."
|
||||||
|
(let* ((prepared (plist-get op :prepared-publication))
|
||||||
|
(spans (or (plist-get prepared :spans)
|
||||||
|
(plist-get prepared :old-refresh-spans)))
|
||||||
|
(result
|
||||||
|
(list :op (plist-get op :op)
|
||||||
|
:owner-id (plist-get op :owner-id)
|
||||||
|
:owner-type (plist-get prepared :owner-type)
|
||||||
|
:span-count (length spans))))
|
||||||
|
(dolist (key '(:affected-region-ids :slot-preserving
|
||||||
|
:flex-line-rerender :publication-scope
|
||||||
|
:published-buffer-spans :full-root-required-p
|
||||||
|
:patch-origin :full-frame-bytes :replacement-bytes
|
||||||
|
:old-span-footprint-signature
|
||||||
|
:new-span-footprint-signature
|
||||||
|
:old-external-footprint-signature
|
||||||
|
:new-external-footprint-signature
|
||||||
|
:old-parent-slot-signature
|
||||||
|
:new-parent-slot-signature))
|
||||||
|
(when (plist-member prepared key)
|
||||||
|
(setq result (plist-put result key (plist-get prepared key)))))
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defun ebox-incremental--prepared-plan-report
|
||||||
|
(prepared patch-set candidate-state)
|
||||||
|
"Return Ebox's semantic report for PREPARED PATCH-SET and CANDIDATE-STATE."
|
||||||
|
(let* ((results (mapcar #'ebox-incremental--prepared-plan-result patch-set))
|
||||||
|
(strategy (and results (ebox--patch-results-strategy results)))
|
||||||
|
(single (and (= (length results) 1) (car results)))
|
||||||
|
(patch-report
|
||||||
|
(and results
|
||||||
|
(ebox--update-report
|
||||||
|
nil strategy
|
||||||
|
:dirty-count (length (plist-get prepared :dirty-set))
|
||||||
|
:patch-count (length results)
|
||||||
|
:patch-ops (mapcar (lambda (result)
|
||||||
|
(plist-get result :op))
|
||||||
|
results)
|
||||||
|
:owner-ids (mapcar (lambda (result)
|
||||||
|
(plist-get result :owner-id))
|
||||||
|
results)
|
||||||
|
:owner-type (plist-get single :owner-type)
|
||||||
|
:span-count (cl-loop for result in results
|
||||||
|
sum (plist-get result :span-count))
|
||||||
|
:slot-preserving
|
||||||
|
(cl-some (lambda (result)
|
||||||
|
(plist-get result :slot-preserving))
|
||||||
|
results)
|
||||||
|
:old-span-footprint-signature
|
||||||
|
(plist-get single :old-span-footprint-signature)
|
||||||
|
:new-span-footprint-signature
|
||||||
|
(plist-get single :new-span-footprint-signature)
|
||||||
|
:old-external-footprint-signature
|
||||||
|
(plist-get single :old-external-footprint-signature)
|
||||||
|
:new-external-footprint-signature
|
||||||
|
(plist-get single :new-external-footprint-signature)
|
||||||
|
:old-parent-slot-signature
|
||||||
|
(plist-get single :old-parent-slot-signature)
|
||||||
|
:new-parent-slot-signature
|
||||||
|
(plist-get single :new-parent-slot-signature)))))
|
||||||
|
(ebox-incremental--commit-report
|
||||||
|
prepared patch-report candidate-state)))
|
||||||
|
|
||||||
|
(defun ebox-incremental--prepare-surface-patch-report
|
||||||
|
(buffer old-state prepared candidate-state)
|
||||||
|
"Prove BUFFER's PREPARED candidate against OLD-STATE.
|
||||||
|
Return a non-publishing report for CANDIDATE-STATE."
|
||||||
|
(let* ((render-dirty-set
|
||||||
|
(cl-remove-if
|
||||||
|
(lambda (entry) (eq (plist-get entry :dirty-kind) 'metadata))
|
||||||
|
(plist-get prepared :dirty-set)))
|
||||||
|
(candidate-region-box-table
|
||||||
|
(plist-get candidate-state :region-box-table))
|
||||||
|
(candidate-scroll-state-table
|
||||||
|
(plist-get prepared :scroll-state-table))
|
||||||
|
(base-buffer-modified-tick
|
||||||
|
(with-current-buffer buffer (buffer-modified-tick)))
|
||||||
|
patch-set)
|
||||||
|
(let ((ebox--region-box-table candidate-region-box-table)
|
||||||
|
(ebox--scroll-global-state candidate-scroll-state-table)
|
||||||
|
(ebox--scroll-idle-prefetch-timers (make-hash-table :test 'equal))
|
||||||
|
(ebox--smooth-scroll-state-table (make-hash-table :test 'equal))
|
||||||
|
(ebox--render-cache-scroll-state-restorable-p nil)
|
||||||
|
(ebox-incremental--buffer-render-state-override
|
||||||
|
(cons buffer candidate-state))
|
||||||
|
(ebox-incremental--candidate-base-state (cons buffer old-state)))
|
||||||
|
(cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch)
|
||||||
|
(lambda (&rest _) nil)))
|
||||||
|
(setq patch-set
|
||||||
|
(ebox-incremental--prepare-declarative-patch-set
|
||||||
|
buffer render-dirty-set))))
|
||||||
|
(unless (= base-buffer-modified-tick
|
||||||
|
(with-current-buffer buffer (buffer-modified-tick)))
|
||||||
|
(error "Ebox buffer changed while declarative patches were prepared"))
|
||||||
|
(ebox-incremental--prepared-plan-report
|
||||||
|
prepared patch-set candidate-state)))
|
||||||
|
|
||||||
|
(defun ebox-incremental--surface-commit-input
|
||||||
|
(buffer old-state prepared)
|
||||||
|
"Return TP surface input for BUFFER from OLD-STATE and PREPARED runtime."
|
||||||
|
(let* ((candidate-state
|
||||||
|
(ebox-incremental--candidate-state
|
||||||
|
old-state (plist-get prepared :root)
|
||||||
|
(plist-get prepared :index) prepared))
|
||||||
|
(report
|
||||||
|
(ebox-incremental--prepare-surface-patch-report
|
||||||
|
buffer old-state prepared candidate-state)))
|
||||||
|
(list :root (plist-get prepared :root)
|
||||||
|
:report-base report
|
||||||
|
:preserve-identities-p t
|
||||||
|
:state-overrides
|
||||||
|
(list :logical-candidate-p
|
||||||
|
(plist-get prepared :logical-candidate-p)
|
||||||
|
:detached-identity-history
|
||||||
|
(plist-get prepared :detached-identity-history)))))
|
||||||
|
|
||||||
(defun ebox-incremental--finalize-declarative-scroll-publication
|
(defun ebox-incremental--finalize-declarative-scroll-publication
|
||||||
(scroll-keys)
|
(scroll-keys)
|
||||||
"Retire stale scroll timers and resume prefetch for committed SCROLL-KEYS."
|
"Retire stale scroll timers and resume prefetch for committed SCROLL-KEYS."
|
||||||
|
|||||||
@ -19,6 +19,9 @@
|
|||||||
(defvar ebox--render-runtime-revision nil
|
(defvar ebox--render-runtime-revision nil
|
||||||
"Current buffer runtime revision for render-local cache validation.")
|
"Current buffer runtime revision for render-local cache validation.")
|
||||||
|
|
||||||
|
(defvar ebox--region-id-counter)
|
||||||
|
(defvar ebox--runtime-node-id-counter)
|
||||||
|
|
||||||
(defvar ebox--intrinsic-layout-measurement nil
|
(defvar ebox--intrinsic-layout-measurement nil
|
||||||
"Non-nil while measuring intrinsic layout contributions.
|
"Non-nil while measuring intrinsic layout contributions.
|
||||||
Descendants still see the current containing block, but stack/column containers
|
Descendants still see the current containing block, but stack/column containers
|
||||||
@ -1394,15 +1397,17 @@ Internal implementation of `ebox-render' for box nodes."
|
|||||||
(setq result (ebox--apply-hidden-visibility result)))
|
(setq result (ebox--apply-hidden-visibility result)))
|
||||||
result)))
|
result)))
|
||||||
|
|
||||||
;;;###autoload
|
(defvar ebox--surface-materialization-active nil
|
||||||
(defun ebox-render (node)
|
"Non-nil while an Ebox TP producer is running raw layout.")
|
||||||
"Render a layout NODE to a multi-line string.
|
|
||||||
|
(defun ebox--render-layout (node)
|
||||||
|
"Render layout NODE directly to a multi-line propertized string.
|
||||||
NODE can be:
|
NODE can be:
|
||||||
- a box plist created by `ebox-create'
|
- a box plist created by `ebox-create'
|
||||||
- a concat node created by `ebox-concat'
|
- a concat node created by `ebox-concat'
|
||||||
- a stack node created by `ebox-stack'
|
- a stack node created by `ebox-stack'
|
||||||
- a flex node created by `ebox-flex'
|
- a flex node created by `ebox-flex'
|
||||||
This is the single public rendering entry point."
|
This function owns spatial layout only; it never creates a live TP surface."
|
||||||
(ebox--with-validated-display-cache
|
(ebox--with-validated-display-cache
|
||||||
(let ((ebox--box-content-render-cache
|
(let ((ebox--box-content-render-cache
|
||||||
(or ebox--box-content-render-cache
|
(or ebox--box-content-render-cache
|
||||||
@ -1416,9 +1421,30 @@ This is the single public rendering entry point."
|
|||||||
((eq type 'stack) (ebox--render-stack node))
|
((eq type 'stack) (ebox--render-stack node))
|
||||||
((eq type 'flex) (ebox--render-flex node))
|
((eq type 'flex) (ebox--render-flex node))
|
||||||
((eq type 'grid) (ebox--render-grid node))
|
((eq type 'grid) (ebox--render-grid node))
|
||||||
((eq type 'flex-item) (ebox-render (plist-get node :node)))
|
((eq type 'flex-item) (ebox--render-layout (plist-get node :node)))
|
||||||
(t (error "ebox-render: unknown node type %S" type)))))))
|
(t (error "ebox-render: unknown node type %S" type)))))))
|
||||||
|
|
||||||
|
(declare-function ebox-surface-producer
|
||||||
|
"ebox-surface"
|
||||||
|
(source &optional previous-state preserve-identities-p
|
||||||
|
state-overrides))
|
||||||
|
(declare-function tp-surface-materialize-string "tp-surface" (plan-or-producer))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun ebox-render (node)
|
||||||
|
"Render layout NODE to a multi-line string through a TP materialization.
|
||||||
|
NODE accepts every layout node supported by `ebox--render-layout'. The
|
||||||
|
materialized surface is ephemeral and creates no live buffer state."
|
||||||
|
(if (or ebox--surface-materialization-active
|
||||||
|
ebox--render-runtime-revision)
|
||||||
|
(ebox--render-layout node)
|
||||||
|
(require 'ebox-surface)
|
||||||
|
(let ((ebox--surface-materialization-active t)
|
||||||
|
(ebox--region-id-counter ebox--region-id-counter)
|
||||||
|
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||||
|
(tp-surface-materialize-string
|
||||||
|
(ebox-surface-producer node nil t)))))
|
||||||
|
|
||||||
(defun ebox--width-pad (string extra-pixels)
|
(defun ebox--width-pad (string extra-pixels)
|
||||||
"Append a display-space of EXTRA-PIXELS to the right of every line in STRING.
|
"Append a display-space of EXTRA-PIXELS to the right of every line in STRING.
|
||||||
Used by `ebox--render-stack' to equalise rows to the same total width."
|
Used by `ebox--render-stack' to equalise rows to the same total width."
|
||||||
@ -1993,6 +2019,7 @@ last cached leaf instead of walking the stack from the beginning."
|
|||||||
max-pixel-width-cache)
|
max-pixel-width-cache)
|
||||||
(ebox--render-recached-source-node-cache
|
(ebox--render-recached-source-node-cache
|
||||||
recached-source-node-cache)
|
recached-source-node-cache)
|
||||||
|
(ebox--surface-materialization-active t)
|
||||||
(ebox--scroll-window-prewarm-state prewarm-state)
|
(ebox--scroll-window-prewarm-state prewarm-state)
|
||||||
(ebox--scroll-window-render-disabled t)
|
(ebox--scroll-window-render-disabled t)
|
||||||
(ebox--scroll-window-render-result nil))
|
(ebox--scroll-window-render-result nil))
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
(defvar ebox-viewport-width nil)
|
(defvar ebox-viewport-width nil)
|
||||||
(defvar ebox-viewport-height nil)
|
(defvar ebox-viewport-height nil)
|
||||||
(defvar ebox--render-runtime-revision nil)
|
(defvar ebox--render-runtime-revision nil)
|
||||||
|
(defvar ebox--surface-materialization-active nil)
|
||||||
(defvar ebox--render-cache-table nil)
|
(defvar ebox--render-cache-table nil)
|
||||||
(defvar ebox--render-cache-signature-cache nil)
|
(defvar ebox--render-cache-signature-cache nil)
|
||||||
(defvar ebox--viewport-dependent-node-ids-cache nil)
|
(defvar ebox--viewport-dependent-node-ids-cache nil)
|
||||||
@ -36,6 +37,7 @@
|
|||||||
(plist-get ,render-state :viewport-height))
|
(plist-get ,render-state :viewport-height))
|
||||||
(ebox--render-runtime-revision
|
(ebox--render-runtime-revision
|
||||||
(plist-get ,render-state :runtime-revision))
|
(plist-get ,render-state :runtime-revision))
|
||||||
|
(ebox--surface-materialization-active t)
|
||||||
(ebox--render-cache-table
|
(ebox--render-cache-table
|
||||||
(plist-get ,render-state :render-cache))
|
(plist-get ,render-state :render-cache))
|
||||||
(ebox--render-cache-signature-cache
|
(ebox--render-cache-signature-cache
|
||||||
|
|||||||
@ -10,10 +10,12 @@
|
|||||||
(require 'subr-x)
|
(require 'subr-x)
|
||||||
(require 'ebox-buffer-backend)
|
(require 'ebox-buffer-backend)
|
||||||
(require 'ebox-tree)
|
(require 'ebox-tree)
|
||||||
|
(require 'tp-surface)
|
||||||
|
|
||||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
(declare-function ebox--buffer-root-node "ebox-incremental" (buffer))
|
(declare-function ebox--buffer-root-node "ebox-incremental" (buffer))
|
||||||
|
(declare-function ebox--buffer-render-state "ebox-incremental" (buffer))
|
||||||
(declare-function ebox--buffer-selector-id-table "ebox-incremental" (buffer))
|
(declare-function ebox--buffer-selector-id-table "ebox-incremental" (buffer))
|
||||||
(declare-function ebox--buffer-selector-class-table
|
(declare-function ebox--buffer-selector-class-table
|
||||||
"ebox-incremental" (buffer))
|
"ebox-incremental" (buffer))
|
||||||
@ -24,6 +26,71 @@
|
|||||||
(declare-function ebox-region-update "ebox" (region-id &rest props))
|
(declare-function ebox-region-update "ebox" (region-id &rest props))
|
||||||
(defvar ebox--region-update-buffer-hint)
|
(defvar ebox--region-update-buffer-hint)
|
||||||
|
|
||||||
|
(cl-defstruct
|
||||||
|
(ebox-region-handle
|
||||||
|
(:constructor ebox-selector--make-region-handle)
|
||||||
|
(:conc-name ebox-selector--region-handle-))
|
||||||
|
"Opaque surface-scoped identity for one editable Ebox region."
|
||||||
|
buffer surface object logical-id)
|
||||||
|
|
||||||
|
(defun ebox-selector--live-region-handle
|
||||||
|
(buffer object &optional logical-id)
|
||||||
|
"Return a live region handle for OBJECT in BUFFER, or nil."
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(surface (plist-get state :surface))
|
||||||
|
(region-id
|
||||||
|
(and (tp-object-live-p object)
|
||||||
|
(gethash object
|
||||||
|
(plist-get state :surface-object-region-table)))))
|
||||||
|
(when (and (tp-surface-live-p surface) region-id)
|
||||||
|
(ebox-selector--make-region-handle
|
||||||
|
:buffer buffer :surface surface :object object :logical-id logical-id))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun ebox-region-resolve (buffer-or-name logical-id)
|
||||||
|
"Resolve LOGICAL-ID in BUFFER-OR-NAME to an opaque editable region handle."
|
||||||
|
(let ((buffer (get-buffer buffer-or-name)))
|
||||||
|
(unless (buffer-live-p buffer)
|
||||||
|
(user-error "Ebox region target is not a live buffer: %S"
|
||||||
|
buffer-or-name))
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(normalized (ebox-selector--metadata-string logical-id))
|
||||||
|
(entries
|
||||||
|
(and state
|
||||||
|
(gethash normalized
|
||||||
|
(plist-get state :logical-id-region-table))))
|
||||||
|
(handles
|
||||||
|
(delq nil
|
||||||
|
(mapcar
|
||||||
|
(lambda (entry)
|
||||||
|
(ebox-selector--live-region-handle
|
||||||
|
buffer (car entry) normalized))
|
||||||
|
entries))))
|
||||||
|
(pcase (length handles)
|
||||||
|
(0 (user-error "Ebox logical region does not exist: %S" logical-id))
|
||||||
|
(1 (car handles))
|
||||||
|
(_ (user-error "Ebox logical region is ambiguous: %S" logical-id))))))
|
||||||
|
|
||||||
|
(defun ebox-selector--region-target (handle)
|
||||||
|
"Return HANDLE's current internal `(BUFFER . REGION-ID)' target."
|
||||||
|
(unless (ebox-region-handle-p handle)
|
||||||
|
(signal 'wrong-type-argument (list 'ebox-region-handle-p handle)))
|
||||||
|
(let* ((buffer (ebox-selector--region-handle-buffer handle))
|
||||||
|
(surface (ebox-selector--region-handle-surface handle))
|
||||||
|
(object (ebox-selector--region-handle-object handle))
|
||||||
|
(state (and (buffer-live-p buffer)
|
||||||
|
(ebox--buffer-render-state buffer)))
|
||||||
|
(region-id
|
||||||
|
(and state
|
||||||
|
(eq surface (plist-get state :surface))
|
||||||
|
(tp-surface-live-p surface)
|
||||||
|
(tp-object-live-p object)
|
||||||
|
(gethash object
|
||||||
|
(plist-get state :surface-object-region-table)))))
|
||||||
|
(unless region-id
|
||||||
|
(user-error "Ebox region handle is stale"))
|
||||||
|
(cons buffer region-id)))
|
||||||
|
|
||||||
(defun ebox-selector--identifier-char-p (char)
|
(defun ebox-selector--identifier-char-p (char)
|
||||||
"Return non-nil when CHAR is accepted in a selector identifier."
|
"Return non-nil when CHAR is accepted in a selector identifier."
|
||||||
(or (and (>= char ?a) (<= char ?z))
|
(or (and (>= char ?a) (<= char ?z))
|
||||||
@ -331,7 +398,19 @@ selectors match the user's logical layout children."
|
|||||||
|
|
||||||
(defun ebox-selector--handle-with-buffer (handle buffer)
|
(defun ebox-selector--handle-with-buffer (handle buffer)
|
||||||
"Return selector HANDLE annotated with BUFFER ownership."
|
"Return selector HANDLE annotated with BUFFER ownership."
|
||||||
(append handle (list :buffer buffer)))
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(region-id (plist-get handle :region-id))
|
||||||
|
(object
|
||||||
|
(and region-id
|
||||||
|
(gethash region-id
|
||||||
|
(plist-get state :region-surface-object-table))))
|
||||||
|
(region-handle
|
||||||
|
(and object
|
||||||
|
(ebox-selector--live-region-handle
|
||||||
|
buffer object
|
||||||
|
(ebox-tree-node-id (plist-get handle :node))))))
|
||||||
|
(append handle
|
||||||
|
(list :buffer buffer :region-handle region-handle))))
|
||||||
|
|
||||||
(defun ebox-selector--single-id-simple (sequence)
|
(defun ebox-selector--single-id-simple (sequence)
|
||||||
"Return the simple selector when SEQUENCE is a single #id candidate."
|
"Return the simple selector when SEQUENCE is a single #id candidate."
|
||||||
@ -470,9 +549,8 @@ tree-query handle fields."
|
|||||||
|
|
||||||
(defun ebox-selector--update-region (handle props)
|
(defun ebox-selector--update-region (handle props)
|
||||||
"Apply PROPS to HANDLE's region through `ebox-region-update'."
|
"Apply PROPS to HANDLE's region through `ebox-region-update'."
|
||||||
(let* ((region-id (plist-get handle :region-id))
|
(let ((ebox--region-update-buffer-hint (plist-get handle :buffer)))
|
||||||
(ebox--region-update-buffer-hint (plist-get handle :buffer)))
|
(apply #'ebox-region-update (plist-get handle :region-id) props)))
|
||||||
(apply #'ebox-region-update region-id props)))
|
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-selector-update-buffer (buffer selector &rest props)
|
(defun ebox-selector-update-buffer (buffer selector &rest props)
|
||||||
@ -489,7 +567,7 @@ skips, and update reports."
|
|||||||
(unless matches
|
(unless matches
|
||||||
(user-error "ebox-selector: no matches for %S" selector))
|
(user-error "ebox-selector: no matches for %S" selector))
|
||||||
(dolist (match matches)
|
(dolist (match matches)
|
||||||
(if (plist-get match :region-id)
|
(if (plist-get match :region-handle)
|
||||||
(push match editable)
|
(push match editable)
|
||||||
(push (ebox-selector--skip-handle match 'no-region) skipped)))
|
(push (ebox-selector--skip-handle match 'no-region) skipped)))
|
||||||
(setq editable (nreverse editable)
|
(setq editable (nreverse editable)
|
||||||
|
|||||||
442
ebox-surface.el
442
ebox-surface.el
@ -1,11 +1,12 @@
|
|||||||
;;; ebox-surface.el --- Pure retained surface projection -*- lexical-binding: t; -*-
|
;;; ebox-surface.el --- Retained projection and publication -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
;;; Commentary:
|
;;; Commentary:
|
||||||
|
|
||||||
;; Owns the pure boundary from an Ebox candidate runtime and rendered layout to
|
;; Owns the boundary from an Ebox candidate runtime to a generic TP surface
|
||||||
;; a generic TP surface plan. It never publishes or scans a live buffer.
|
;; plan, and the atomic publication participant that mirrors the committed TP
|
||||||
|
;; generation into Ebox's buffer-local runtime indexes.
|
||||||
|
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
@ -20,10 +21,272 @@
|
|||||||
(defvar ebox--scroll-global-state)
|
(defvar ebox--scroll-global-state)
|
||||||
(defvar ebox--scroll-idle-prefetch-timers)
|
(defvar ebox--scroll-idle-prefetch-timers)
|
||||||
(defvar ebox--smooth-scroll-state-table)
|
(defvar ebox--smooth-scroll-state-table)
|
||||||
|
(defvar ebox--box-extents)
|
||||||
|
(defvar ebox--box-extent-template)
|
||||||
|
(defvar ebox--buffer-render-state-table)
|
||||||
|
(defvar ebox--viewport-dependent-node-ids-cache)
|
||||||
|
(defvar ebox--viewport-dependent-subtree-cache)
|
||||||
|
(defvar ebox--viewport-height-dependent-subtree-cache)
|
||||||
|
(defvar ebox--render-runtime-revision)
|
||||||
|
(defvar ebox--render-cache-table)
|
||||||
|
(defvar ebox--render-cache-signature-cache)
|
||||||
|
(defvar ebox--flex-content-min-width-table)
|
||||||
|
|
||||||
(declare-function ebox-render "ebox-layout" (node))
|
(declare-function ebox--render-layout "ebox-layout" (node))
|
||||||
|
(declare-function ebox-buffer--build-region-role-span-template
|
||||||
|
"ebox-buffer-backend" (string))
|
||||||
|
(declare-function ebox-buffer-materialize-region-role-spans
|
||||||
|
"ebox-buffer-backend" (buffer))
|
||||||
|
(declare-function ebox--build-box-extent-template "ebox" (rendered))
|
||||||
|
(declare-function ebox--build-scroll-content-span-template "ebox" (string))
|
||||||
|
(declare-function ebox--set-box-extents "ebox" (region-id start end))
|
||||||
|
(declare-function ebox--clear-buffer-extents
|
||||||
|
"ebox" (&optional buffer preserve-template))
|
||||||
|
(declare-function ebox--clear-box-extents-for-region-ids
|
||||||
|
"ebox" (region-ids))
|
||||||
|
(declare-function ebox--install-box-extent-template
|
||||||
|
"ebox" (buffer template start))
|
||||||
|
(declare-function ebox--install-buffer-scroll-content-span-template
|
||||||
|
"ebox" (buffer template start &optional defer-window-region-set))
|
||||||
|
(declare-function ebox-incremental--hash-snapshot
|
||||||
|
"ebox-incremental" (table keys))
|
||||||
|
(declare-function ebox-incremental--restore-hash-snapshot
|
||||||
|
"ebox-incremental" (table entries))
|
||||||
|
(declare-function ebox-incremental--replace-hash-entries
|
||||||
|
"ebox-incremental" (target keys source))
|
||||||
|
(declare-function ebox-incremental--detach-scroll-state-table-markers
|
||||||
|
"ebox-incremental" (table))
|
||||||
|
(declare-function ebox--runtime-region-id-conflict
|
||||||
|
"ebox-incremental" (region-id-set target-buffer))
|
||||||
|
(declare-function ebox--viewport-dependent-node-id-axes
|
||||||
|
"ebox-incremental" (node))
|
||||||
|
(declare-function ebox-incremental--detach-region-role-span-table
|
||||||
|
"ebox-incremental" (table))
|
||||||
|
(declare-function ebox-incremental--finalize-declarative-scroll-publication
|
||||||
|
"ebox-incremental" (scroll-keys))
|
||||||
(declare-function ebox--scroll-schedule-idle-prefetch
|
(declare-function ebox--scroll-schedule-idle-prefetch
|
||||||
"ebox" (region-id &optional delay))
|
"ebox" (region-id &optional delay))
|
||||||
|
(declare-function ebox--scroll-clear-state "ebox" (region-id))
|
||||||
|
(declare-function ebox--smooth-scroll-stop "ebox" (region-id))
|
||||||
|
|
||||||
|
(defvar-local ebox-surface--buffer-surface nil
|
||||||
|
"Live TP content surface mounted for the current Ebox buffer.")
|
||||||
|
|
||||||
|
(defun ebox-surface--live-buffer-surface (buffer)
|
||||||
|
"Return BUFFER's live Ebox TP surface, or nil."
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(let ((surface
|
||||||
|
(with-current-buffer buffer ebox-surface--buffer-surface)))
|
||||||
|
(and (tp-surface-live-p surface) surface))))
|
||||||
|
|
||||||
|
(defun ebox-surface--buffer-extent-snapshot (buffer)
|
||||||
|
"Return numeric live box extents owned by BUFFER."
|
||||||
|
(let (snapshot)
|
||||||
|
(maphash
|
||||||
|
(lambda (region-id extents)
|
||||||
|
(when (eq (marker-buffer (car extents)) buffer)
|
||||||
|
(push (list region-id
|
||||||
|
(marker-position (car extents))
|
||||||
|
(marker-position (cdr extents)))
|
||||||
|
snapshot)))
|
||||||
|
ebox--box-extents)
|
||||||
|
snapshot))
|
||||||
|
|
||||||
|
(defun ebox-surface--restore-buffer-extents (buffer snapshot template)
|
||||||
|
"Restore BUFFER box extents from SNAPSHOT and numeric TEMPLATE."
|
||||||
|
(ebox--clear-buffer-extents buffer)
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(setq-local ebox--box-extent-template template)
|
||||||
|
(dolist (entry snapshot)
|
||||||
|
(ebox--set-box-extents (nth 0 entry) (nth 1 entry) (nth 2 entry))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--runtime-keys (state key)
|
||||||
|
"Return hash keys stored under KEY in runtime STATE."
|
||||||
|
(when-let ((table (plist-get state key)))
|
||||||
|
(ebox-surface--hash-keys table)))
|
||||||
|
|
||||||
|
(defun ebox-surface--commit-report (surface state report-base)
|
||||||
|
"Return Ebox's compact report for SURFACE, STATE, and REPORT-BASE."
|
||||||
|
(let* ((tp-report (tp-surface-report surface))
|
||||||
|
(text-operations (or (plist-get tp-report :text-operations) 0))
|
||||||
|
(property-operations
|
||||||
|
(or (plist-get tp-report :property-operations) 0))
|
||||||
|
(patch-count (+ text-operations property-operations))
|
||||||
|
(report (copy-sequence report-base))
|
||||||
|
(planned-publication-scope (plist-get report :publication-scope)))
|
||||||
|
(dolist (entry
|
||||||
|
`((:runtime-published . t)
|
||||||
|
(:runtime-revision . ,(plist-get state :runtime-revision))
|
||||||
|
(:surface-revision . ,(tp-surface-revision surface))
|
||||||
|
(:publication-scope . tp-surface)
|
||||||
|
(:planned-publication-scope . ,planned-publication-scope)
|
||||||
|
(:tp-render-scope . surface-plan)
|
||||||
|
(:tp-operation-count . ,patch-count)
|
||||||
|
(:tp-transaction-id . ,(plist-get tp-report :transaction-id))
|
||||||
|
(:tp-text-operations . ,text-operations)
|
||||||
|
(:tp-property-operations . ,property-operations)
|
||||||
|
(:reconciled-objects
|
||||||
|
. ,(plist-get tp-report :reconciled-objects))
|
||||||
|
(:created-objects . ,(plist-get tp-report :created-objects))
|
||||||
|
(:removed-objects . ,(plist-get tp-report :removed-objects))
|
||||||
|
(:moved-objects . ,(plist-get tp-report :moved-objects))))
|
||||||
|
(setq report (plist-put report (car entry) (cdr entry))))
|
||||||
|
(unless (plist-member report :strategy)
|
||||||
|
(setq report
|
||||||
|
(plist-put report :strategy
|
||||||
|
(if (zerop patch-count) 'no-op 'surface-commit))))
|
||||||
|
(unless (plist-member report :constraint-source)
|
||||||
|
(setq report (plist-put report :constraint-source 'declarative)))
|
||||||
|
(unless (plist-member report :constraint-root-node-id)
|
||||||
|
(setq report
|
||||||
|
(plist-put report :constraint-root-node-id
|
||||||
|
(plist-get (plist-get state :root-node) :node-id))))
|
||||||
|
(unless (plist-member report :dirty-count)
|
||||||
|
(setq report
|
||||||
|
(plist-put report :dirty-count
|
||||||
|
(if (zerop patch-count) 0 1))))
|
||||||
|
(unless (plist-member report :patch-count)
|
||||||
|
(setq report (plist-put report :patch-count patch-count)))
|
||||||
|
(unless (plist-member report :patch-ops)
|
||||||
|
(setq report
|
||||||
|
(plist-put report :patch-ops
|
||||||
|
(and (> patch-count 0) '(tp-surface)))))
|
||||||
|
report))
|
||||||
|
|
||||||
|
(defun ebox-surface--publish-runtime-state
|
||||||
|
(buffer surface old-state report-base after-publication)
|
||||||
|
"Register BUFFER runtime publication for SURFACE after OLD-STATE.
|
||||||
|
REPORT-BASE requests an Ebox commit report. AFTER-PUBLICATION, when non-nil,
|
||||||
|
runs after TP and Ebox point at the same candidate generation."
|
||||||
|
(let (new-state region-snapshot scroll-snapshot extent-snapshot
|
||||||
|
old-template old-surface old-mirror region-keys scroll-keys)
|
||||||
|
(tp-transaction-participate
|
||||||
|
(list 'ebox/runtime buffer)
|
||||||
|
(lambda ()
|
||||||
|
(setq new-state (tp-surface-client-state surface)
|
||||||
|
old-surface
|
||||||
|
(with-current-buffer buffer ebox-surface--buffer-surface)
|
||||||
|
old-mirror (gethash buffer ebox--buffer-render-state-table)
|
||||||
|
old-template (with-current-buffer buffer ebox--box-extent-template)
|
||||||
|
extent-snapshot (ebox-surface--buffer-extent-snapshot buffer)
|
||||||
|
region-keys
|
||||||
|
(delete-dups
|
||||||
|
(append (ebox-surface--runtime-keys old-state :region-id-set)
|
||||||
|
(ebox-surface--runtime-keys new-state :region-id-set)))
|
||||||
|
scroll-keys
|
||||||
|
(delete-dups
|
||||||
|
(append (copy-sequence (plist-get old-state :scroll-region-ids))
|
||||||
|
(copy-sequence (plist-get new-state :scroll-region-ids)))))
|
||||||
|
(setq region-snapshot
|
||||||
|
(ebox-incremental--hash-snapshot
|
||||||
|
ebox--region-box-table region-keys)
|
||||||
|
scroll-snapshot
|
||||||
|
(ebox-incremental--hash-snapshot
|
||||||
|
ebox--scroll-global-state scroll-keys))
|
||||||
|
(when new-state
|
||||||
|
(plist-put new-state :surface surface)
|
||||||
|
(plist-put new-state :runtime-revision
|
||||||
|
(1- (tp-surface-revision surface)))
|
||||||
|
(when report-base
|
||||||
|
(plist-put new-state :last-update-report
|
||||||
|
(ebox-surface--commit-report
|
||||||
|
surface new-state report-base))))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(setq-local ebox-surface--buffer-surface surface))
|
||||||
|
(if new-state
|
||||||
|
(puthash buffer new-state ebox--buffer-render-state-table)
|
||||||
|
(remhash buffer ebox--buffer-render-state-table))
|
||||||
|
(ebox-incremental--replace-hash-entries
|
||||||
|
ebox--region-box-table region-keys
|
||||||
|
(plist-get new-state :region-box-table))
|
||||||
|
(ebox-incremental--replace-hash-entries
|
||||||
|
ebox--scroll-global-state scroll-keys
|
||||||
|
(plist-get new-state :scroll-state-table))
|
||||||
|
(ebox--clear-buffer-extents buffer)
|
||||||
|
(when new-state
|
||||||
|
(ebox--install-box-extent-template
|
||||||
|
buffer (plist-get new-state :box-extent-template) 1)
|
||||||
|
(ebox--install-buffer-scroll-content-span-template
|
||||||
|
buffer (plist-get new-state :scroll-content-span-template) 1)
|
||||||
|
(ebox-buffer-materialize-region-role-spans buffer))
|
||||||
|
(when after-publication
|
||||||
|
(funcall after-publication
|
||||||
|
(plist-get new-state :last-update-report)))
|
||||||
|
(unless (buffer-live-p buffer)
|
||||||
|
(error "Ebox declarative target died during publication")))
|
||||||
|
(lambda ()
|
||||||
|
(when-let ((table (plist-get new-state :scroll-state-table)))
|
||||||
|
(ebox-incremental--detach-scroll-state-table-markers table))
|
||||||
|
(if (buffer-live-p buffer)
|
||||||
|
(progn
|
||||||
|
(when region-snapshot
|
||||||
|
(ebox-incremental--restore-hash-snapshot
|
||||||
|
ebox--region-box-table region-snapshot))
|
||||||
|
(when scroll-snapshot
|
||||||
|
(ebox-incremental--restore-hash-snapshot
|
||||||
|
ebox--scroll-global-state scroll-snapshot))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(setq-local ebox-surface--buffer-surface old-surface))
|
||||||
|
(ebox-surface--restore-buffer-extents
|
||||||
|
buffer extent-snapshot old-template)
|
||||||
|
(if old-mirror
|
||||||
|
(puthash buffer old-mirror ebox--buffer-render-state-table)
|
||||||
|
(remhash buffer ebox--buffer-render-state-table)))
|
||||||
|
(remhash buffer ebox--buffer-render-state-table)
|
||||||
|
(dolist (region-id region-keys)
|
||||||
|
(remhash region-id ebox--region-box-table))
|
||||||
|
(dolist (region-id scroll-keys)
|
||||||
|
(ebox--scroll-clear-state region-id)
|
||||||
|
(ebox--smooth-scroll-stop region-id))
|
||||||
|
(ebox--clear-box-extents-for-region-ids region-keys))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--retire-state (old-state new-state)
|
||||||
|
"Retire marker and timer resources in OLD-STATE after NEW-STATE commits."
|
||||||
|
(when old-state
|
||||||
|
(let ((old-role-table (plist-get old-state :region-role-span-table))
|
||||||
|
(new-role-table (plist-get new-state :region-role-span-table)))
|
||||||
|
(unless (eq old-role-table new-role-table)
|
||||||
|
(ebox-incremental--detach-region-role-span-table old-role-table)))
|
||||||
|
(let ((old-scroll-table (plist-get old-state :scroll-state-table))
|
||||||
|
(new-scroll-table (plist-get new-state :scroll-state-table)))
|
||||||
|
(unless (eq old-scroll-table new-scroll-table)
|
||||||
|
(ebox-incremental--detach-scroll-state-table-markers old-scroll-table)))))
|
||||||
|
|
||||||
|
(defun ebox-surface-mount-buffer
|
||||||
|
(buffer source &optional report-base after-publication
|
||||||
|
preserve-identities-p state-overrides)
|
||||||
|
"Mount or atomically update BUFFER from Ebox SOURCE and return its surface.
|
||||||
|
REPORT-BASE requests a committed Ebox report. AFTER-PUBLICATION runs inside
|
||||||
|
the rollback-capable transaction. PRESERVE-IDENTITIES-P retains identities
|
||||||
|
already present in a logical candidate. STATE-OVERRIDES augments its runtime."
|
||||||
|
(let* ((surface (ebox-surface--live-buffer-surface buffer))
|
||||||
|
(old-state (and surface (tp-surface-client-state surface)))
|
||||||
|
(preserve-identities-p
|
||||||
|
(or preserve-identities-p
|
||||||
|
(and (null old-state)
|
||||||
|
(ebox-surface--source-identities-available-p
|
||||||
|
source buffer))))
|
||||||
|
(producer
|
||||||
|
(ebox-surface-producer
|
||||||
|
source old-state preserve-identities-p state-overrides)))
|
||||||
|
(tp-with-transaction
|
||||||
|
(if surface
|
||||||
|
(tp-surface-update surface producer)
|
||||||
|
(setq surface
|
||||||
|
(tp-surface-mount
|
||||||
|
buffer producer
|
||||||
|
'(:capability content :inhibit-read-only t))))
|
||||||
|
(ebox-surface--publish-runtime-state
|
||||||
|
buffer surface old-state report-base after-publication))
|
||||||
|
(let* ((new-state (tp-surface-client-state surface))
|
||||||
|
(scroll-keys
|
||||||
|
(delete-dups
|
||||||
|
(append (copy-sequence (plist-get old-state :scroll-region-ids))
|
||||||
|
(copy-sequence (plist-get new-state :scroll-region-ids))))))
|
||||||
|
(ebox-surface--retire-state old-state new-state)
|
||||||
|
(ebox-incremental--finalize-declarative-scroll-publication scroll-keys))
|
||||||
|
surface))
|
||||||
|
|
||||||
(defconst ebox-surface--root-key 'ebox/surface
|
(defconst ebox-surface--root-key 'ebox/surface
|
||||||
"Stable TP key for an Ebox surface projection root.")
|
"Stable TP key for an Ebox surface projection root.")
|
||||||
@ -34,18 +297,55 @@
|
|||||||
(defconst ebox-surface--fragments-key 'ebox/fragments
|
(defconst ebox-surface--fragments-key 'ebox/fragments
|
||||||
"Stable TP key for linear rendered fragments.")
|
"Stable TP key for linear rendered fragments.")
|
||||||
|
|
||||||
(defun ebox-surface--candidate-root (source previous-state)
|
(defun ebox-surface--clear-runtime-attachments (root)
|
||||||
"Return an isolated runtime copy of SOURCE reconciled with PREVIOUS-STATE."
|
"Clear TP handles and render-cache attachments below ROOT."
|
||||||
|
(cl-labels
|
||||||
|
((visit (node)
|
||||||
|
(when (and (listp node) (not (stringp node)))
|
||||||
|
(when (plist-member node :surface-object)
|
||||||
|
(plist-put node :surface-object nil))
|
||||||
|
(when (plist-member node :render-cache)
|
||||||
|
(plist-put node :render-cache nil))
|
||||||
|
(dolist (child (ebox-tree--children-raw node))
|
||||||
|
(visit child)))))
|
||||||
|
(visit root))
|
||||||
|
root)
|
||||||
|
|
||||||
|
(defun ebox-surface--candidate-root
|
||||||
|
(source previous-state preserve-identities-p)
|
||||||
|
"Return an isolated runtime copy of SOURCE for PREVIOUS-STATE.
|
||||||
|
When PRESERVE-IDENTITIES-P is non-nil, retain existing Ebox node and region
|
||||||
|
identities while always discarding TP handles and render-cache attachments."
|
||||||
(unless (and (listp source) (not (stringp source)))
|
(unless (and (listp source) (not (stringp source)))
|
||||||
(error "Ebox surface source must be an Ebox node"))
|
(error "Ebox surface source must be an Ebox node"))
|
||||||
(ebox-tree-validate-declarative-root source)
|
(ebox-tree-validate-declarative-root source)
|
||||||
(let ((candidate
|
(let ((candidate (ebox-tree-copy-node-structure source)))
|
||||||
(ebox-tree-clear-runtime-identities
|
(if preserve-identities-p
|
||||||
(ebox-tree-copy-node-structure source))))
|
(ebox-surface--clear-runtime-attachments candidate)
|
||||||
|
(ebox-tree-clear-runtime-identities candidate))
|
||||||
(when-let ((previous-root (plist-get previous-state :root-node)))
|
(when-let ((previous-root (plist-get previous-state :root-node)))
|
||||||
(ebox-tree-reconcile-runtime previous-root candidate))
|
(ebox-tree-reconcile-runtime previous-root candidate))
|
||||||
candidate))
|
candidate))
|
||||||
|
|
||||||
|
(defun ebox-surface--source-region-id-set (source)
|
||||||
|
"Return region ids already present in Ebox SOURCE without mutating it."
|
||||||
|
(let ((ids (make-hash-table :test 'equal)))
|
||||||
|
(cl-labels
|
||||||
|
((visit (node)
|
||||||
|
(when (and (listp node) (not (stringp node)))
|
||||||
|
(when-let ((region-id (plist-get node :region-id)))
|
||||||
|
(puthash region-id t ids))
|
||||||
|
(dolist (child (ebox-tree--children-raw node))
|
||||||
|
(visit child)))))
|
||||||
|
(visit source))
|
||||||
|
ids))
|
||||||
|
|
||||||
|
(defun ebox-surface--source-identities-available-p (source buffer)
|
||||||
|
"Return non-nil when SOURCE's migration ids are available for BUFFER."
|
||||||
|
(or (stringp source)
|
||||||
|
(not (ebox--runtime-region-id-conflict
|
||||||
|
(ebox-surface--source-region-id-set source) buffer))))
|
||||||
|
|
||||||
(defun ebox-surface--node-key (node)
|
(defun ebox-surface--node-key (node)
|
||||||
"Return NODE's namespaced sibling key, or nil for positional identity."
|
"Return NODE's namespaced sibling key, or nil for positional identity."
|
||||||
(when-let ((key (plist-get node :key)))
|
(when-let ((key (plist-get node :key)))
|
||||||
@ -92,6 +392,40 @@
|
|||||||
(plist-get state :region-node-table))
|
(plist-get state :region-node-table))
|
||||||
table))
|
table))
|
||||||
|
|
||||||
|
(defun ebox-surface--object-region-table (region-objects)
|
||||||
|
"Return an object-to-region table from REGION-OBJECTS."
|
||||||
|
(let ((table (make-hash-table :test 'eq)))
|
||||||
|
(maphash (lambda (region-id object)
|
||||||
|
(puthash object region-id table))
|
||||||
|
region-objects)
|
||||||
|
table))
|
||||||
|
|
||||||
|
(defun ebox-surface--node-editable-region-id (node)
|
||||||
|
"Return NODE's editable Ebox region id, or nil."
|
||||||
|
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||||
|
('box (plist-get node :region-id))
|
||||||
|
((or 'flex 'grid)
|
||||||
|
(plist-get (plist-get node :box) :region-id))
|
||||||
|
('flex-item
|
||||||
|
(ebox-surface--node-editable-region-id (plist-get node :node)))
|
||||||
|
(_ nil)))
|
||||||
|
|
||||||
|
(defun ebox-surface--logical-id-region-table (state region-objects)
|
||||||
|
"Return root-scoped logical-id entries for STATE and REGION-OBJECTS."
|
||||||
|
(let ((table (make-hash-table :test 'equal)))
|
||||||
|
(maphash
|
||||||
|
(lambda (logical-id entries)
|
||||||
|
(dolist (entry entries)
|
||||||
|
(when-let* ((region-id
|
||||||
|
(ebox-surface--node-editable-region-id (car entry)))
|
||||||
|
(object (gethash region-id region-objects)))
|
||||||
|
(push (cons object region-id) (gethash logical-id table)))))
|
||||||
|
(plist-get state :selector-id-table))
|
||||||
|
(maphash (lambda (logical-id matches)
|
||||||
|
(puthash logical-id (nreverse matches) table))
|
||||||
|
table)
|
||||||
|
table))
|
||||||
|
|
||||||
(defun ebox-surface--hash-keys (table)
|
(defun ebox-surface--hash-keys (table)
|
||||||
"Return TABLE keys in unspecified order."
|
"Return TABLE keys in unspecified order."
|
||||||
(let (keys)
|
(let (keys)
|
||||||
@ -106,18 +440,46 @@
|
|||||||
(let ((ebox--region-box-table (plist-get state :region-box-table))
|
(let ((ebox--region-box-table (plist-get state :region-box-table))
|
||||||
(ebox--scroll-global-state scroll-table)
|
(ebox--scroll-global-state scroll-table)
|
||||||
(ebox--scroll-idle-prefetch-timers timer-table)
|
(ebox--scroll-idle-prefetch-timers timer-table)
|
||||||
(ebox--smooth-scroll-state-table smooth-table))
|
(ebox--smooth-scroll-state-table smooth-table)
|
||||||
|
(ebox--render-runtime-revision
|
||||||
|
(plist-get state :runtime-revision))
|
||||||
|
(ebox--render-cache-table (plist-get state :render-cache))
|
||||||
|
(ebox--render-cache-signature-cache
|
||||||
|
(plist-get state :render-signature-cache))
|
||||||
|
(ebox--viewport-dependent-node-ids-cache
|
||||||
|
(make-hash-table :test 'eq))
|
||||||
|
(ebox--viewport-dependent-subtree-cache
|
||||||
|
(make-hash-table :test 'eq))
|
||||||
|
(ebox--viewport-height-dependent-subtree-cache
|
||||||
|
(plist-get state :viewport-height-dependent-subtree-cache))
|
||||||
|
(ebox--flex-content-min-width-table
|
||||||
|
(plist-get state :flex-content-min-widths)))
|
||||||
(cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch)
|
(cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch)
|
||||||
(lambda (&rest _) nil)))
|
(lambda (&rest _) nil)))
|
||||||
(prog1 (ebox-render (plist-get state :root-node))
|
(prog1 (let ((ebox--surface-materialization-active t))
|
||||||
|
(ebox--render-layout (plist-get state :root-node)))
|
||||||
(plist-put state :scroll-state-table scroll-table)
|
(plist-put state :scroll-state-table scroll-table)
|
||||||
(plist-put state :scroll-region-ids
|
(plist-put state :scroll-region-ids
|
||||||
(ebox-surface--hash-keys scroll-table)))))))
|
(ebox-surface--hash-keys scroll-table)))))))
|
||||||
|
|
||||||
(defun ebox-surface--finish-runtime-state (state)
|
(defun ebox-surface--finish-runtime-state (state)
|
||||||
"Install post-layout runtime indexes into candidate STATE."
|
"Install post-layout runtime indexes into candidate STATE."
|
||||||
(ebox--render-state-install-index
|
(setq state
|
||||||
state (ebox--runtime-index (plist-get state :root-node) t)))
|
(ebox--render-state-install-index
|
||||||
|
state (ebox--runtime-index (plist-get state :root-node) t)))
|
||||||
|
(let* ((root (plist-get state :root-node))
|
||||||
|
(ebox--viewport-dependent-node-ids-cache
|
||||||
|
(make-hash-table :test 'eq))
|
||||||
|
(ebox--viewport-height-dependent-subtree-cache
|
||||||
|
(plist-get state :viewport-height-dependent-subtree-cache))
|
||||||
|
(axes (ebox--viewport-dependent-node-id-axes root)))
|
||||||
|
(plist-put state :viewport-dependent-node-id-axes axes)
|
||||||
|
(plist-put state :viewport-dependent-node-ids
|
||||||
|
(and axes
|
||||||
|
(delete-dups
|
||||||
|
(copy-sequence (append (car axes) (cdr axes))))))
|
||||||
|
(plist-put state :viewport-dependent-node-ids-ready t))
|
||||||
|
state)
|
||||||
|
|
||||||
(defun ebox-surface--role-ids-at (rendered position)
|
(defun ebox-surface--role-ids-at (rendered position)
|
||||||
"Return namespaced Ebox role/id pairs at POSITION in RENDERED."
|
"Return namespaced Ebox role/id pairs at POSITION in RENDERED."
|
||||||
@ -196,10 +558,31 @@
|
|||||||
:children fragments :capability 'content))
|
:children fragments :capability 'content))
|
||||||
:capability 'content)))
|
:capability 'content)))
|
||||||
|
|
||||||
(defun ebox-surface--project (context source previous-state)
|
(defun ebox-surface--apply-state-overrides (state overrides)
|
||||||
"Project SOURCE in CONTEXT using optional PREVIOUS-STATE."
|
"Apply plist OVERRIDES to candidate runtime STATE and return STATE."
|
||||||
(let* ((root (ebox-surface--candidate-root source previous-state))
|
(while overrides
|
||||||
(state (ebox--new-buffer-render-state root))
|
(setq state (plist-put state (pop overrides) (pop overrides))))
|
||||||
|
state)
|
||||||
|
|
||||||
|
(defun ebox-surface--project
|
||||||
|
(context source previous-state preserve-identities-p state-overrides)
|
||||||
|
"Project SOURCE in CONTEXT using PREVIOUS-STATE and identity policy.
|
||||||
|
PRESERVE-IDENTITIES-P retains existing Ebox node and region identities."
|
||||||
|
(if (stringp source)
|
||||||
|
(let ((object
|
||||||
|
(tp-object-ensure context nil ebox-surface--root-key
|
||||||
|
'ebox/string)))
|
||||||
|
(ignore object)
|
||||||
|
(tp-surface-result-create
|
||||||
|
(tp-surface-plan-create
|
||||||
|
:key ebox-surface--root-key :kind 'ebox/string
|
||||||
|
:text source :capability 'content)))
|
||||||
|
(let* ((root (ebox-surface--candidate-root
|
||||||
|
source previous-state preserve-identities-p))
|
||||||
|
(state
|
||||||
|
(ebox-surface--apply-state-overrides
|
||||||
|
(ebox--new-buffer-render-state root)
|
||||||
|
(copy-sequence state-overrides)))
|
||||||
(surface-root
|
(surface-root
|
||||||
(tp-object-ensure context nil ebox-surface--root-key 'ebox/surface))
|
(tp-object-ensure context nil ebox-surface--root-key 'ebox/surface))
|
||||||
(node-root
|
(node-root
|
||||||
@ -211,20 +594,37 @@
|
|||||||
(plist-put state :region-box-table (make-hash-table :test 'equal))
|
(plist-put state :region-box-table (make-hash-table :test 'equal))
|
||||||
(let ((rendered (ebox-surface--render-candidate state)))
|
(let ((rendered (ebox-surface--render-candidate state)))
|
||||||
(setq state (ebox-surface--finish-runtime-state state))
|
(setq state (ebox-surface--finish-runtime-state state))
|
||||||
|
(plist-put state :region-role-span-table
|
||||||
|
(ebox-buffer--build-region-role-span-template rendered))
|
||||||
|
(plist-put state :box-extent-template
|
||||||
|
(ebox--build-box-extent-template rendered))
|
||||||
|
(plist-put state :scroll-content-span-template
|
||||||
|
(let ((ebox--scroll-global-state
|
||||||
|
(plist-get state :scroll-state-table)))
|
||||||
|
(ebox--build-scroll-content-span-template rendered)))
|
||||||
(let* ((node-objects (ebox-surface--node-object-table objects-by-node))
|
(let* ((node-objects (ebox-surface--node-object-table objects-by-node))
|
||||||
(region-objects
|
(region-objects
|
||||||
(ebox-surface--region-object-table state node-objects)))
|
(ebox-surface--region-object-table state node-objects)))
|
||||||
(plist-put state :surface-node-object-table node-objects)
|
(plist-put state :surface-node-object-table node-objects)
|
||||||
(plist-put state :region-surface-object-table region-objects)
|
(plist-put state :region-surface-object-table region-objects)
|
||||||
|
(plist-put state :surface-object-region-table
|
||||||
|
(ebox-surface--object-region-table region-objects))
|
||||||
|
(plist-put state :logical-id-region-table
|
||||||
|
(ebox-surface--logical-id-region-table
|
||||||
|
state region-objects))
|
||||||
(tp-surface-result-create
|
(tp-surface-result-create
|
||||||
(ebox-surface--surface-plan
|
(ebox-surface--surface-plan
|
||||||
context surface-root rendered region-objects)
|
context surface-root rendered region-objects)
|
||||||
state)))))
|
state))))))
|
||||||
|
|
||||||
(defun ebox-surface--producer (source &optional previous-state)
|
(defun ebox-surface-producer
|
||||||
"Return a TP producer for Ebox SOURCE and optional PREVIOUS-STATE."
|
(source &optional previous-state preserve-identities-p state-overrides)
|
||||||
|
"Return a TP producer for Ebox SOURCE and optional PREVIOUS-STATE.
|
||||||
|
PRESERVE-IDENTITIES-P retains existing Ebox node and region identities.
|
||||||
|
STATE-OVERRIDES augments the candidate Ebox runtime state."
|
||||||
(lambda (context)
|
(lambda (context)
|
||||||
(ebox-surface--project context source previous-state)))
|
(ebox-surface--project
|
||||||
|
context source previous-state preserve-identities-p state-overrides)))
|
||||||
|
|
||||||
(provide 'ebox-surface)
|
(provide 'ebox-surface)
|
||||||
|
|
||||||
|
|||||||
139
ebox.el
139
ebox.el
@ -2636,8 +2636,10 @@ ROOT-START, and avoids live extent scans for prepared root publications."
|
|||||||
table))
|
table))
|
||||||
|
|
||||||
(defun ebox--install-buffer-scroll-content-span-template
|
(defun ebox--install-buffer-scroll-content-span-template
|
||||||
(buffer template start)
|
(buffer template start &optional defer-window-region-set)
|
||||||
"Install scroll marker TEMPLATE in BUFFER relative to START."
|
"Install scroll marker TEMPLATE in BUFFER relative to START.
|
||||||
|
When DEFER-WINDOW-REGION-SET is non-nil, postpone computing the visible
|
||||||
|
region set until the first scroll operation."
|
||||||
(when (and (buffer-live-p buffer) (hash-table-p template))
|
(when (and (buffer-live-p buffer) (hash-table-p template))
|
||||||
(let ((region-set (ebox--buffer-region-id-set buffer))
|
(let ((region-set (ebox--buffer-region-id-set buffer))
|
||||||
(seen (make-hash-table :test 'equal))
|
(seen (make-hash-table :test 'equal))
|
||||||
@ -2654,7 +2656,7 @@ ROOT-START, and avoids live extent scans for prepared root publications."
|
|||||||
(cons (+ start (cadr entry))
|
(cons (+ start (cadr entry))
|
||||||
(+ start (cddr entry)))))
|
(+ start (cddr entry)))))
|
||||||
entries)
|
entries)
|
||||||
t)))
|
defer-window-region-set)))
|
||||||
template)
|
template)
|
||||||
(maphash
|
(maphash
|
||||||
(lambda (region-id state)
|
(lambda (region-id state)
|
||||||
@ -6873,7 +6875,8 @@ Defaults to the current buffer."
|
|||||||
|
|
||||||
(defun ebox--cleanup-current-buffer ()
|
(defun ebox--cleanup-current-buffer ()
|
||||||
"Remove Ebox runtime state owned by the current buffer."
|
"Remove Ebox runtime state owned by the current buffer."
|
||||||
(ebox--clear-buffer-runtime-state (current-buffer)))
|
(ebox--clear-buffer-runtime-state (current-buffer))
|
||||||
|
(setq-local ebox-surface--buffer-surface nil))
|
||||||
|
|
||||||
(defun ebox--foreign-region-in-range-p (start end region-id)
|
(defun ebox--foreign-region-in-range-p (start end region-id)
|
||||||
"Return non-nil if START..END contains another ebox region."
|
"Return non-nil if START..END contains another ebox region."
|
||||||
@ -7192,10 +7195,71 @@ are settled in the same update transaction."
|
|||||||
(memq key '(:width :min-width :max-width)))
|
(memq key '(:width :min-width :max-width)))
|
||||||
changed-keys)))
|
changed-keys)))
|
||||||
|
|
||||||
|
(defun ebox--surface-region-update (handle props)
|
||||||
|
"Apply PROPS to surface-scoped region HANDLE through one TP commit."
|
||||||
|
(pcase-let* ((`(,buffer . ,region-id)
|
||||||
|
(ebox-selector--region-target handle))
|
||||||
|
(state (ebox--buffer-render-state buffer))
|
||||||
|
(box (gethash region-id
|
||||||
|
(plist-get state :region-box-table)))
|
||||||
|
(expanded (ebox--expand-plist props))
|
||||||
|
(analysis (ebox--region-update-analysis box expanded)))
|
||||||
|
(dolist (key (plist-get analysis :unknown-keys))
|
||||||
|
(message "ebox-region-update: unknown key %S (ignored)" key))
|
||||||
|
(if (not (plist-get analysis :changed-p))
|
||||||
|
(let ((report
|
||||||
|
(ebox--update-report
|
||||||
|
nil 'no-op :region-handle handle :constraint-source 'region
|
||||||
|
:runtime-published nil :runtime-revision
|
||||||
|
(plist-get state :runtime-revision)
|
||||||
|
:surface-revision
|
||||||
|
(tp-surface-revision (plist-get state :surface))
|
||||||
|
:dirty-count 0 :patch-count 0 :patch-ops nil)))
|
||||||
|
(plist-put state :last-update-report report)
|
||||||
|
report)
|
||||||
|
(let* ((candidate-root
|
||||||
|
(ebox-tree-copy-node-structure
|
||||||
|
(plist-get state :root-node)))
|
||||||
|
(candidate-box
|
||||||
|
(ebox--root-region-box candidate-root region-id))
|
||||||
|
changed-keys)
|
||||||
|
(unless candidate-box
|
||||||
|
(user-error "Ebox region handle no longer resolves to a box"))
|
||||||
|
(cl-loop for (key value) on expanded by #'cddr
|
||||||
|
for target-key = (ebox--region-update-normalize-property key)
|
||||||
|
when (or (eq target-key :content)
|
||||||
|
(memq target-key ebox--region-update-longhand-props))
|
||||||
|
unless (equal (ebox-get candidate-box target-key) value)
|
||||||
|
do (ebox-put candidate-box target-key value)
|
||||||
|
and do (push target-key changed-keys))
|
||||||
|
(setq changed-keys (nreverse changed-keys))
|
||||||
|
(let ((ebox-viewport-width (plist-get state :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get state :viewport-height)))
|
||||||
|
(let ((surface
|
||||||
|
(ebox-surface-mount-buffer
|
||||||
|
buffer candidate-root
|
||||||
|
(list :region-handle handle
|
||||||
|
:constraint-source 'region
|
||||||
|
:dirty-count 1
|
||||||
|
:dirty-keys changed-keys
|
||||||
|
:dirty-kinds
|
||||||
|
(list (ebox--region-update-dirty-kind changed-keys)))
|
||||||
|
nil t
|
||||||
|
(list :viewport-width (plist-get state :viewport-width)
|
||||||
|
:viewport-height (plist-get state :viewport-height)
|
||||||
|
:detached-identity-history
|
||||||
|
(plist-get state :detached-identity-history)))))
|
||||||
|
(plist-get (tp-surface-client-state surface)
|
||||||
|
:last-update-report)))))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-region-update (region-id &rest props)
|
(defun ebox-region-update (region-id &rest props)
|
||||||
"Update one or more properties of the box identified by REGION-ID.
|
"Update one or more properties of the box identified by REGION-ID.
|
||||||
|
|
||||||
|
REGION-ID may also be an opaque handle returned by `ebox-region-resolve'.
|
||||||
|
Handles are surface-scoped and are the preferred form when one declarative
|
||||||
|
source is mounted in more than one buffer.
|
||||||
|
|
||||||
PROPS accepts the same shorthand and longhand keywords as `ebox-create'.
|
PROPS accepts the same shorthand and longhand keywords as `ebox-create'.
|
||||||
Properties are expanded through `ebox-property-rules', written to the box
|
Properties are expanded through `ebox-property-rules', written to the box
|
||||||
plist, then rendered once. Unsupported pseudo properties such as
|
plist, then rendered once. Unsupported pseudo properties such as
|
||||||
@ -7206,10 +7270,14 @@ updates return a `no-op' report.
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
(ebox-region-update id :content \"Hello\")
|
(ebox-region-update id :content \"Hello\")
|
||||||
|
(ebox-region-update (ebox-region-resolve buffer \"status\")
|
||||||
|
:content \"Ready\")
|
||||||
(ebox-region-update id :padding (list 1 2) :border-color \"red\")
|
(ebox-region-update id :padding (list 1 2) :border-color \"red\")
|
||||||
(ebox-region-update id :content \"Hi\" :width 30 :color \"white\")"
|
(ebox-region-update id :content \"Hi\" :width 30 :color \"white\")"
|
||||||
(ebox--with-render-gc
|
(ebox--with-render-gc
|
||||||
(ebox--with-validated-display-cache
|
(if (ebox-region-handle-p region-id)
|
||||||
|
(ebox--surface-region-update region-id props)
|
||||||
|
(ebox--with-validated-display-cache
|
||||||
(catch 'ebox-region-update-result
|
(catch 'ebox-region-update-result
|
||||||
(let* ((inhibit-read-only t)
|
(let* ((inhibit-read-only t)
|
||||||
(buffer (ebox--region-update-buffer region-id))
|
(buffer (ebox--region-update-buffer region-id))
|
||||||
@ -7513,7 +7581,7 @@ Examples:
|
|||||||
(setq transaction-completed t))
|
(setq transaction-completed t))
|
||||||
(unless transaction-completed
|
(unless transaction-completed
|
||||||
(restore-model)))
|
(restore-model)))
|
||||||
result-report)))))))
|
result-report))))))))
|
||||||
|
|
||||||
(defvar ebox-scroll-map nil
|
(defvar ebox-scroll-map nil
|
||||||
"Keymap for scroll interaction within ebox.")
|
"Keymap for scroll interaction within ebox.")
|
||||||
@ -7582,34 +7650,13 @@ NODE can be any box/concat/stack node created by `ebox-create',
|
|||||||
`ebox-concat', or `ebox-stack', or a pre-rendered string."
|
`ebox-concat', or `ebox-stack', or a pre-rendered string."
|
||||||
(declare (indent 1))
|
(declare (indent 1))
|
||||||
(unless (stringp node)
|
(unless (stringp node)
|
||||||
;; Validate opaque host identities before clearing an existing target.
|
;; Validate opaque host identities before preparing a target generation.
|
||||||
(ebox-tree-validate-host-refs node))
|
(ebox-tree-validate-host-refs node))
|
||||||
(let* ((runtime-root
|
(let ((buffer (get-buffer-create buffer-or-name)))
|
||||||
(if (stringp node)
|
|
||||||
node
|
|
||||||
(ebox--prepare-buffer-runtime-root
|
|
||||||
node buffer-or-name)))
|
|
||||||
(buffer (get-buffer-create buffer-or-name)))
|
|
||||||
(ebox--with-render-gc
|
(ebox--with-render-gc
|
||||||
|
(ebox-surface-mount-buffer buffer node)
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(let ((inhibit-read-only t))
|
(goto-char (point-min))
|
||||||
(ebox--clear-buffer-runtime-state buffer)
|
|
||||||
(erase-buffer)
|
|
||||||
(progn
|
|
||||||
(if (stringp runtime-root)
|
|
||||||
(insert runtime-root)
|
|
||||||
(ebox--set-buffer-render-state buffer runtime-root)
|
|
||||||
(condition-case err
|
|
||||||
(insert
|
|
||||||
(ebox--with-buffer-render-context buffer
|
|
||||||
(ebox-render runtime-root)))
|
|
||||||
(error
|
|
||||||
(ebox--clear-buffer-render-state buffer)
|
|
||||||
(signal (car err) (cdr err)))))
|
|
||||||
(unless (stringp runtime-root)
|
|
||||||
(ebox--refresh-buffer-scroll-content-markers buffer)
|
|
||||||
(ebox-buffer-refresh-region-role-spans buffer)))
|
|
||||||
(goto-char (point-min)))
|
|
||||||
(add-hook 'kill-buffer-hook #'ebox--cleanup-current-buffer nil t)
|
(add-hook 'kill-buffer-hook #'ebox--cleanup-current-buffer nil t)
|
||||||
(ebox-buffer-mode 1)
|
(ebox-buffer-mode 1)
|
||||||
(read-only-mode 1)
|
(read-only-mode 1)
|
||||||
@ -7741,16 +7788,26 @@ it must not run application code. An error rolls the Ebox publication back.
|
|||||||
Return the successful publication report stored by `ebox-buffer-update-report'."
|
Return the successful publication report stored by `ebox-buffer-update-report'."
|
||||||
(unless (or (null after-publication) (functionp after-publication))
|
(unless (or (null after-publication) (functionp after-publication))
|
||||||
(signal 'wrong-type-argument (list 'functionp after-publication)))
|
(signal 'wrong-type-argument (list 'functionp after-publication)))
|
||||||
(let ((ebox-incremental--after-declarative-publication
|
(let ((buffer (get-buffer buffer-or-name)))
|
||||||
(or after-publication
|
(unless (buffer-live-p buffer)
|
||||||
ebox-incremental--after-declarative-publication)))
|
(error "Ebox declarative commit requires an existing live buffer: %S"
|
||||||
(if (ebox-candidate-p next-root)
|
buffer-or-name))
|
||||||
(ebox-incremental-commit-candidate buffer-or-name next-root)
|
(let* ((commit-input
|
||||||
(let ((buffer (get-buffer buffer-or-name)))
|
(if (ebox-candidate-p next-root)
|
||||||
(unless (buffer-live-p buffer)
|
(ebox-incremental-consume-candidate buffer next-root)
|
||||||
(error "Ebox declarative commit requires an existing live buffer: %S"
|
(ebox-incremental-prepare-root-commit buffer next-root)))
|
||||||
buffer-or-name))
|
(source (plist-get commit-input :root))
|
||||||
(ebox-incremental-commit buffer next-root)))))
|
(callback
|
||||||
|
(or after-publication
|
||||||
|
ebox-incremental--after-declarative-publication))
|
||||||
|
(surface
|
||||||
|
(ebox-surface-mount-buffer
|
||||||
|
buffer source
|
||||||
|
(plist-get commit-input :report-base)
|
||||||
|
callback
|
||||||
|
(plist-get commit-input :preserve-identities-p)
|
||||||
|
(plist-get commit-input :state-overrides))))
|
||||||
|
(plist-get (tp-surface-client-state surface) :last-update-report))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-rerender-buffer-with-context
|
(defun ebox-rerender-buffer-with-context
|
||||||
|
|||||||
@ -82,6 +82,18 @@
|
|||||||
(and (not ambiguous) candidate)))))
|
(and (not ambiguous) candidate)))))
|
||||||
(ebox-buffer-update-report target)))
|
(ebox-buffer-update-report target)))
|
||||||
|
|
||||||
|
(defun ebox-test--runtime-root (buffer)
|
||||||
|
"Return BUFFER's surface-owned runtime root."
|
||||||
|
(plist-get (ebox--buffer-render-state buffer) :root-node))
|
||||||
|
|
||||||
|
(defun ebox-test--selector-node (buffer selector)
|
||||||
|
"Return the first runtime node matching SELECTOR in BUFFER."
|
||||||
|
(plist-get (car (ebox-selector-query-buffer buffer selector)) :node))
|
||||||
|
|
||||||
|
(defun ebox-test--selector-node-id (buffer selector)
|
||||||
|
"Return the first runtime node id matching SELECTOR in BUFFER."
|
||||||
|
(plist-get (car (ebox-selector-query-buffer buffer selector)) :node-id))
|
||||||
|
|
||||||
(ert-deftest ebox-scroll-window-lookahead-accepts-root-reflow-override ()
|
(ert-deftest ebox-scroll-window-lookahead-accepts-root-reflow-override ()
|
||||||
"Root reflow should be able to defer all hidden lookahead work."
|
"Root reflow should be able to defer all hidden lookahead work."
|
||||||
(let ((ebox-scroll-lazy-prefix-lookahead-lines 8)
|
(let ((ebox-scroll-lazy-prefix-lookahead-lines 8)
|
||||||
@ -371,7 +383,7 @@
|
|||||||
(should-not (gethash (plist-get right :region-id) template))))
|
(should-not (gethash (plist-get right :region-id) template))))
|
||||||
|
|
||||||
(ert-deftest ebox-live-mount-uses-runtime-copy-and-leaves-source-stable ()
|
(ert-deftest ebox-live-mount-uses-runtime-copy-and-leaves-source-stable ()
|
||||||
"First live mount should copy SOURCE while preserving stable source ids."
|
"Live publication should update a surface copy without mutating SOURCE."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((source (ebox-build
|
(let* ((source (ebox-build
|
||||||
'(box :id "root" :content "before\nhidden" :width (120)
|
'(box :id "root" :content "before\nhidden" :width (120)
|
||||||
@ -381,114 +393,67 @@
|
|||||||
(progn
|
(progn
|
||||||
(should-not (plist-member source :region-id))
|
(should-not (plist-member source :region-id))
|
||||||
(ebox-render-to-buffer buffer source)
|
(ebox-render-to-buffer buffer source)
|
||||||
(let* ((runtime-root (ebox--buffer-root-node buffer))
|
(let* ((runtime-root (ebox-test--runtime-root buffer))
|
||||||
(source-ids (ebox-region-ids source))
|
(handle (ebox-region-resolve buffer "root")))
|
||||||
(region-id (car source-ids))
|
|
||||||
(runtime-box
|
|
||||||
(gethash region-id
|
|
||||||
(ebox--buffer-region-box-table buffer))))
|
|
||||||
(should source-ids)
|
|
||||||
(should (equal (ebox-region-ids source) source-ids))
|
|
||||||
(should-not (eq runtime-root source))
|
(should-not (eq runtime-root source))
|
||||||
(should (eq runtime-root runtime-box))
|
(ebox-region-update handle
|
||||||
(should (eq (gethash region-id ebox--region-box-table)
|
:content "after\nhidden"
|
||||||
runtime-box))
|
:color "#222222"
|
||||||
(let ((ebox--region-update-buffer-hint buffer))
|
:width '(140))
|
||||||
(ebox-region-update region-id
|
(setq runtime-root (ebox-test--runtime-root buffer))
|
||||||
:content "after\nhidden"
|
(should-not (plist-member source :region-id))
|
||||||
:color "#222222"
|
(should-not (plist-member source :node-id))
|
||||||
:width '(140)
|
|
||||||
:scroll-offset 1))
|
|
||||||
(should (equal (ebox-region-ids source) source-ids))
|
|
||||||
(should (equal (ebox-get source :content) "before\nhidden"))
|
(should (equal (ebox-get source :content) "before\nhidden"))
|
||||||
(should (equal (ebox-get source :color) "#111111"))
|
(should (equal (ebox-get source :color) "#111111"))
|
||||||
(should (equal (ebox-get source :width) 120))
|
(should (equal (ebox-get source :width) 120))
|
||||||
(should (equal (ebox-get source :scroll-offset) 0))
|
(should (equal (ebox-get source :scroll-offset) 0))
|
||||||
(should (equal (ebox-get runtime-box :content) "after\nhidden"))
|
(should (equal (ebox-get runtime-root :content) "after\nhidden"))
|
||||||
(should (equal (ebox-get runtime-box :color) "#222222"))
|
(should (equal (ebox-get runtime-root :color) "#222222"))
|
||||||
(should (equal (ebox-get runtime-box :width) 140))
|
(should (equal (ebox-get runtime-root :width) 140))
|
||||||
(should (equal (ebox-get runtime-box :scroll-offset) 1))
|
|
||||||
(should (= (plist-get (ebox--scroll-get-state region-id)
|
|
||||||
:scroll-offset)
|
|
||||||
1))
|
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (string-match-p "hidden"
|
(should (string-match-p "after"
|
||||||
(buffer-string))))))
|
(buffer-string))))))
|
||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-live-mount-rejects-second-owner-before-target-cleanup ()
|
(ert-deftest ebox-live-mount-allows-one-source-on-independent-surfaces ()
|
||||||
"Second live mount of retained ids should fail without touching target state."
|
"One immutable source should mount and update independently in two buffers."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((source (ebox-build
|
(let* ((source (ebox-build
|
||||||
'(box :id "root" :content "owned\nhidden" :width (120)
|
'(box :id "root" :content "owned\nhidden" :width (120)
|
||||||
:height 1 :overflow scroll)))
|
:height 1 :overflow scroll)))
|
||||||
(_source-ids (ebox-region-ids source))
|
first-buffer second-buffer)
|
||||||
(retained-copy (copy-tree source))
|
|
||||||
(missing-target-name (generate-new-buffer-name " *ebox-live-missing*"))
|
|
||||||
owner-buffer target-buffer
|
|
||||||
owner-state owner-root owner-region-id owner-scroll-state
|
|
||||||
owner-region-box owner-extents owner-timer
|
|
||||||
target-state target-text)
|
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(setq owner-buffer
|
(setq first-buffer
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-live-owner*")
|
(generate-new-buffer-name " *ebox-live-first*")
|
||||||
source))
|
source))
|
||||||
(setq target-buffer
|
(setq second-buffer
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-live-target*")
|
(generate-new-buffer-name " *ebox-live-second*")
|
||||||
(ebox-create :content "target stays" :width 120)))
|
source))
|
||||||
(setq owner-state (ebox--buffer-render-state owner-buffer)
|
(let ((first-handle (ebox-region-resolve first-buffer "root"))
|
||||||
owner-root (plist-get owner-state :root-node)
|
(second-handle (ebox-region-resolve second-buffer "root")))
|
||||||
owner-region-id (car (ebox-region-ids source))
|
(should-not (eq first-handle second-handle))
|
||||||
owner-scroll-state (ebox--scroll-get-state owner-region-id)
|
(ebox-region-update first-handle :content "first changed")
|
||||||
owner-region-box (gethash owner-region-id ebox--region-box-table)
|
(with-current-buffer first-buffer
|
||||||
owner-extents (gethash owner-region-id ebox--box-extents)
|
(should (string-match-p "first changed" (buffer-string))))
|
||||||
target-state (ebox--buffer-render-state target-buffer))
|
(with-current-buffer second-buffer
|
||||||
(setq owner-timer (run-with-timer 3600 nil #'ignore))
|
(should-not (string-match-p "first changed" (buffer-string))))
|
||||||
(puthash owner-region-id owner-timer
|
(ebox-region-update second-handle :content "second changed")
|
||||||
ebox--scroll-idle-prefetch-timers)
|
(with-current-buffer second-buffer
|
||||||
(with-current-buffer target-buffer
|
(should (string-match-p "second changed" (buffer-string))))
|
||||||
(setq target-text (buffer-string)))
|
(should (equal (ebox-get source :content) "owned\nhidden"))
|
||||||
(dolist (root (list source retained-copy))
|
(should-not (plist-member source :region-id))))
|
||||||
(should-error
|
(dolist (buffer (list first-buffer second-buffer))
|
||||||
(ebox-render-to-buffer target-buffer root)
|
|
||||||
:type 'user-error)
|
|
||||||
(should (eq (ebox--buffer-render-state owner-buffer) owner-state))
|
|
||||||
(should (eq (plist-get owner-state :root-node) owner-root))
|
|
||||||
(should (eq (ebox--scroll-get-state owner-region-id)
|
|
||||||
owner-scroll-state))
|
|
||||||
(should (eq (gethash owner-region-id ebox--region-box-table)
|
|
||||||
owner-region-box))
|
|
||||||
(should (eq (gethash owner-region-id ebox--box-extents)
|
|
||||||
owner-extents))
|
|
||||||
(should (eq (gethash owner-region-id
|
|
||||||
ebox--scroll-idle-prefetch-timers)
|
|
||||||
owner-timer))
|
|
||||||
(should (eq (ebox--buffer-render-state target-buffer)
|
|
||||||
target-state))
|
|
||||||
(with-current-buffer target-buffer
|
|
||||||
(should (equal-including-properties (buffer-string)
|
|
||||||
target-text)))))
|
|
||||||
(should-error
|
|
||||||
(ebox-render-to-buffer missing-target-name retained-copy)
|
|
||||||
:type 'user-error)
|
|
||||||
(should-not (get-buffer missing-target-name))
|
|
||||||
(dolist (buffer (list owner-buffer target-buffer))
|
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))
|
(kill-buffer buffer))))))
|
||||||
(when (timerp owner-timer)
|
|
||||||
(cancel-timer owner-timer))
|
|
||||||
(when-let ((buffer (get-buffer missing-target-name)))
|
|
||||||
(kill-buffer buffer)))))
|
|
||||||
|
|
||||||
(ert-deftest ebox-live-mount-releases-old-source-after-target-replacement ()
|
(ert-deftest ebox-live-mount-kill-invalidates-handle ()
|
||||||
"Replacing a target root should release the old source ids for reuse."
|
"Killing a surface buffer should invalidate its region handles."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((old-source (ebox-create :content "old" :width 120))
|
(let* ((old-source (ebox-create :id "old" :content "old" :width 120))
|
||||||
(new-source (ebox-create :content "new" :width 120))
|
|
||||||
primary-buffer secondary-buffer)
|
primary-buffer secondary-buffer)
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
@ -496,19 +461,20 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-live-primary*")
|
(generate-new-buffer-name " *ebox-live-primary*")
|
||||||
old-source))
|
old-source))
|
||||||
(ebox-render-to-buffer primary-buffer new-source)
|
(let ((old-handle (ebox-region-resolve primary-buffer "old")))
|
||||||
|
(kill-buffer primary-buffer)
|
||||||
|
(setq primary-buffer nil)
|
||||||
|
(should-error (ebox-region-update old-handle :content "stale")
|
||||||
|
:type 'user-error))
|
||||||
(setq secondary-buffer
|
(setq secondary-buffer
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-live-secondary*")
|
(generate-new-buffer-name " *ebox-live-secondary*")
|
||||||
old-source))
|
old-source))
|
||||||
(kill-buffer secondary-buffer)
|
(ebox-region-update (ebox-region-resolve secondary-buffer "old")
|
||||||
(setq secondary-buffer nil)
|
:content "reused")
|
||||||
(setq secondary-buffer
|
(with-current-buffer secondary-buffer
|
||||||
(ebox-render-to-buffer
|
(should (string-match-p "reused" (buffer-string))))
|
||||||
(generate-new-buffer-name " *ebox-live-secondary*")
|
(should (equal (ebox-get old-source :content) "old")))
|
||||||
old-source))
|
|
||||||
(should (eq (ebox--region-buffer (car (ebox-region-ids old-source)))
|
|
||||||
secondary-buffer)))
|
|
||||||
(dolist (buffer (list primary-buffer secondary-buffer))
|
(dolist (buffer (list primary-buffer secondary-buffer))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer))))))
|
(kill-buffer buffer))))))
|
||||||
@ -565,7 +531,8 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
node))
|
node))
|
||||||
(let ((ids (ebox--runtime-node-ids node)))
|
(let ((ids (ebox--runtime-node-ids
|
||||||
|
(ebox-test--runtime-root buffer))))
|
||||||
(should (> (length ids) 1))
|
(should (> (length ids) 1))
|
||||||
(dolist (node-id ids)
|
(dolist (node-id ids)
|
||||||
(should (ebox--buffer-runtime-node buffer node-id)))))
|
(should (ebox--buffer-runtime-node buffer node-id)))))
|
||||||
@ -575,24 +542,26 @@
|
|||||||
(ert-deftest ebox-region-render-owner-index-maps-boxes-and-flex-wrapper ()
|
(ert-deftest ebox-region-render-owner-index-maps-boxes-and-flex-wrapper ()
|
||||||
"Region lookup should map each box to its smallest renderable owner."
|
"Region lookup should map each box to its smallest renderable owner."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((plain (ebox-create :content "Plain" :width '(80)))
|
(let* ((plain (ebox-create :id "plain" :content "Plain" :width '(80)))
|
||||||
(flex-child (ebox-create :content "Flex child" :width '(80)))
|
(flex-child (ebox-create :id "child" :content "Flex child"
|
||||||
|
:width '(80)))
|
||||||
(flex (ebox-flex :width '(120) :padding 1 flex-child))
|
(flex (ebox-flex :width '(120) :padding 1 flex-child))
|
||||||
(flex-wrapper (plist-get flex :box))
|
|
||||||
(layout (ebox-column plain flex)))
|
(layout (ebox-column plain flex)))
|
||||||
(ebox-test--with-rendered-buffer layout
|
(ebox-test--with-rendered-buffer layout
|
||||||
(let* ((buffer (current-buffer))
|
(let* ((buffer (current-buffer))
|
||||||
|
(runtime-root (ebox-test--runtime-root buffer))
|
||||||
|
(runtime-plain (car (plist-get runtime-root :children)))
|
||||||
|
(runtime-flex (cadr (plist-get runtime-root :children)))
|
||||||
|
(runtime-wrapper (plist-get runtime-flex :box))
|
||||||
|
(runtime-child (car (plist-get runtime-flex :children)))
|
||||||
(table (ebox--buffer-region-node-table buffer))
|
(table (ebox--buffer-region-node-table buffer))
|
||||||
(plain-region-id (ebox--ensure-region-id plain))
|
(plain-region-id (plist-get runtime-plain :region-id))
|
||||||
(wrapper-region-id (ebox--ensure-region-id flex-wrapper))
|
(wrapper-region-id (plist-get runtime-wrapper :region-id))
|
||||||
(child-region-id (ebox--ensure-region-id flex-child))
|
(child-region-id (plist-get runtime-child :region-id))
|
||||||
(plain-node-id (ebox--ensure-node-id plain))
|
(plain-node-id (plist-get runtime-plain :node-id))
|
||||||
(flex-node-id (ebox--ensure-node-id flex))
|
(flex-node-id (plist-get runtime-flex :node-id))
|
||||||
(wrapper-node-id (ebox--ensure-node-id flex-wrapper))
|
(wrapper-node-id (plist-get runtime-wrapper :node-id))
|
||||||
(child-node-id (ebox--ensure-node-id flex-child))
|
(child-node-id (plist-get runtime-child :node-id)))
|
||||||
(runtime-plain (ebox--buffer-runtime-node buffer plain-node-id))
|
|
||||||
(runtime-flex (ebox--buffer-runtime-node buffer flex-node-id))
|
|
||||||
(runtime-child (ebox--buffer-runtime-node buffer child-node-id)))
|
|
||||||
(should (hash-table-p table))
|
(should (hash-table-p table))
|
||||||
(should (equal (gethash plain-region-id table) plain-node-id))
|
(should (equal (gethash plain-region-id table) plain-node-id))
|
||||||
(should (equal (gethash wrapper-region-id table) flex-node-id))
|
(should (equal (gethash wrapper-region-id table) flex-node-id))
|
||||||
@ -663,8 +632,9 @@
|
|||||||
(let* ((index (ebox--runtime-index layout t))
|
(let* ((index (ebox--runtime-index layout t))
|
||||||
(index-table (plist-get index :region-box-table))
|
(index-table (plist-get index :region-box-table))
|
||||||
(ebox--region-box-table render-table)
|
(ebox--region-box-table render-table)
|
||||||
|
(ebox--surface-materialization-active t)
|
||||||
(ebox--scroll-global-state (make-hash-table :test 'equal)))
|
(ebox--scroll-global-state (make-hash-table :test 'equal)))
|
||||||
(ebox-render layout)
|
(ebox--render-layout layout)
|
||||||
(should (= (hash-table-count index-table)
|
(should (= (hash-table-count index-table)
|
||||||
(hash-table-count render-table)))
|
(hash-table-count render-table)))
|
||||||
(maphash
|
(maphash
|
||||||
@ -2387,7 +2357,8 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(let* ((root-id (plist-get layout :node-id))
|
(let* ((root-id (plist-get (ebox-test--runtime-root buffer)
|
||||||
|
:node-id))
|
||||||
(snapshot (ebox--layout-snapshot buffer root-id)))
|
(snapshot (ebox--layout-snapshot buffer root-id)))
|
||||||
(should snapshot)
|
(should snapshot)
|
||||||
(should (eq (plist-get snapshot :type) 'concat))
|
(should (eq (plist-get snapshot :type) 'concat))
|
||||||
@ -2623,7 +2594,9 @@
|
|||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
node))
|
node))
|
||||||
(let* ((snapshot (ebox--layout-snapshot
|
(let* ((snapshot (ebox--layout-snapshot
|
||||||
buffer (plist-get node :node-id)))
|
buffer
|
||||||
|
(plist-get (ebox-test--runtime-root buffer)
|
||||||
|
:node-id)))
|
||||||
(child-ids (plist-get snapshot :child-ids))
|
(child-ids (plist-get snapshot :child-ids))
|
||||||
(child-snapshot (ebox--layout-snapshot buffer (car child-ids))))
|
(child-snapshot (ebox--layout-snapshot buffer (car child-ids))))
|
||||||
(should (equal (plist-get snapshot :display) '(block flow)))
|
(should (equal (plist-get snapshot :display) '(block flow)))
|
||||||
@ -2813,7 +2786,8 @@
|
|||||||
(should (equal (plist-get report :constraint-owner-id)
|
(should (equal (plist-get report :constraint-owner-id)
|
||||||
'viewport))
|
'viewport))
|
||||||
(should (equal (plist-get report :constraint-root-node-id)
|
(should (equal (plist-get report :constraint-root-node-id)
|
||||||
(plist-get layout :node-id)))
|
(plist-get (ebox-test--runtime-root buffer)
|
||||||
|
:node-id)))
|
||||||
(should (equal (plist-get report :dirty-kinds)
|
(should (equal (plist-get report :dirty-kinds)
|
||||||
'(geometry)))))
|
'(geometry)))))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
@ -2837,22 +2811,25 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout)))
|
layout)))
|
||||||
(ebox-rerender-buffer-with-context buffer 90)
|
(let* ((runtime-root (ebox-test--runtime-root buffer))
|
||||||
(let ((report (ebox-test--buffer-update-report))
|
(runtime-top (car (plist-get runtime-root :children)))
|
||||||
(plain (with-current-buffer buffer
|
(runtime-bottom (cadr (plist-get runtime-root :children))))
|
||||||
(buffer-substring-no-properties
|
(ebox-rerender-buffer-with-context buffer 90)
|
||||||
(point-min) (point-max)))))
|
(let ((report (ebox-test--buffer-update-report))
|
||||||
(should (equal (plist-get report :patch-ops)
|
(plain (with-current-buffer buffer
|
||||||
'(owner-rerender owner-rerender)))
|
(buffer-substring-no-properties
|
||||||
(should (= (plist-get report :patch-count) 2))
|
(point-min) (point-max)))))
|
||||||
(should-not (member (plist-get layout :node-id)
|
(should (equal (plist-get report :patch-ops)
|
||||||
(plist-get report :owner-ids)))
|
'(owner-rerender owner-rerender)))
|
||||||
(should (member (plist-get top :node-id)
|
(should (= (plist-get report :patch-count) 2))
|
||||||
(plist-get report :owner-ids)))
|
(should-not (member (plist-get runtime-root :node-id)
|
||||||
(should (member (plist-get bottom :node-id)
|
(plist-get report :owner-ids)))
|
||||||
(plist-get report :owner-ids)))
|
(should (member (plist-get runtime-top :node-id)
|
||||||
(should (string-match-p "Top text" plain))
|
(plist-get report :owner-ids)))
|
||||||
(should (string-match-p "Bottom text" plain))))
|
(should (member (plist-get runtime-bottom :node-id)
|
||||||
|
(plist-get report :owner-ids)))
|
||||||
|
(should (string-match-p "Top text" plain))
|
||||||
|
(should (string-match-p "Bottom text" plain)))))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
@ -2889,8 +2866,11 @@
|
|||||||
(let ((report (ebox-test--buffer-update-report)))
|
(let ((report (ebox-test--buffer-update-report)))
|
||||||
(should (eq (plist-get report :constraint-source) 'region))
|
(should (eq (plist-get report :constraint-source) 'region))
|
||||||
(should (eq (plist-get report :constraint-owner-type) 'box))
|
(should (eq (plist-get report :constraint-owner-type) 'box))
|
||||||
(should (equal (plist-get report :constraint-owner-id)
|
(should
|
||||||
(plist-get left :node-id)))
|
(equal (plist-get report :constraint-owner-id)
|
||||||
|
(gethash left-id
|
||||||
|
(ebox--buffer-region-node-table
|
||||||
|
(current-buffer)))))
|
||||||
(should (equal (plist-get report :dirty-kinds)
|
(should (equal (plist-get report :dirty-kinds)
|
||||||
'(geometry)))))))
|
'(geometry)))))))
|
||||||
|
|
||||||
@ -3140,7 +3120,7 @@
|
|||||||
(should (> (length dirty) 8))
|
(should (> (length dirty) 8))
|
||||||
(should (= (length patches) 1))
|
(should (= (length patches) 1))
|
||||||
(should (equal (plist-get (car patches) :owner-id)
|
(should (equal (plist-get (car patches) :owner-id)
|
||||||
(plist-get layout :node-id)))
|
(plist-get root :node-id)))
|
||||||
(should (= (length source-ids) (length dirty)))
|
(should (= (length source-ids) (length dirty)))
|
||||||
(dolist (entry dirty)
|
(dolist (entry dirty)
|
||||||
(should (member (plist-get entry :node-id) source-ids)))))
|
(should (member (plist-get entry :node-id) source-ids)))))
|
||||||
@ -3161,7 +3141,8 @@
|
|||||||
(dirty-entry-count 0)
|
(dirty-entry-count 0)
|
||||||
(original (symbol-function 'ebox--promote-to-patchable-owner))
|
(original (symbol-function 'ebox--promote-to-patchable-owner))
|
||||||
(original-dirty-entry (symbol-function 'ebox--dirty-entry))
|
(original-dirty-entry (symbol-function 'ebox--dirty-entry))
|
||||||
buffer)
|
buffer
|
||||||
|
root-id)
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(let ((ebox-viewport-width 640))
|
(let ((ebox-viewport-width 640))
|
||||||
@ -3175,6 +3156,7 @@
|
|||||||
(source-ids
|
(source-ids
|
||||||
(ebox--dirty-provenance-node-ids
|
(ebox--dirty-provenance-node-ids
|
||||||
(plist-get (car patches) :dirty))))
|
(plist-get (car patches) :dirty))))
|
||||||
|
(setq root-id (plist-get root :node-id))
|
||||||
(should (> (length dirty) 8))
|
(should (> (length dirty) 8))
|
||||||
(should (= (length source-ids) (length dirty)))
|
(should (= (length source-ids) (length dirty)))
|
||||||
(dolist (entry dirty)
|
(dolist (entry dirty)
|
||||||
@ -3194,7 +3176,7 @@
|
|||||||
(should (= (plist-get report :dirty-count) 1))
|
(should (= (plist-get report :dirty-count) 1))
|
||||||
(should (= (plist-get report :patch-count) 1))
|
(should (= (plist-get report :patch-count) 1))
|
||||||
(should (equal (plist-get report :owner-ids)
|
(should (equal (plist-get report :owner-ids)
|
||||||
(list (plist-get layout :node-id))))))
|
(list root-id)))))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
@ -3264,7 +3246,9 @@
|
|||||||
(let ((report (ebox-test--buffer-update-report)))
|
(let ((report (ebox-test--buffer-update-report)))
|
||||||
(should (eq (plist-get report :strategy) 'owner-rerender))
|
(should (eq (plist-get report :strategy) 'owner-rerender))
|
||||||
(should (equal (plist-get report :owner-ids)
|
(should (equal (plist-get report :owner-ids)
|
||||||
(list (plist-get layout :node-id)))))
|
(list (plist-get
|
||||||
|
(ebox-test--runtime-root buffer)
|
||||||
|
:node-id)))))
|
||||||
(should (= slot-probe-count 0)))
|
(should (= slot-probe-count 0)))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
@ -4980,12 +4964,9 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(let* ((root-id (ebox--ensure-node-id layout))
|
(let* ((runtime-root (ebox-test--runtime-root buffer))
|
||||||
(left-node-id (ebox--ensure-node-id left))
|
(runtime-left (car (plist-get runtime-root :children)))
|
||||||
(right-node-id (ebox--ensure-node-id right))
|
(runtime-right (cadr (plist-get runtime-root :children))))
|
||||||
(runtime-root (ebox--buffer-runtime-node buffer root-id))
|
|
||||||
(runtime-left (ebox--buffer-runtime-node buffer left-node-id))
|
|
||||||
(runtime-right (ebox--buffer-runtime-node buffer right-node-id)))
|
|
||||||
(ebox--with-buffer-render-context buffer
|
(ebox--with-buffer-render-context buffer
|
||||||
(ebox--render-cache-node-body-signature runtime-root))
|
(ebox--render-cache-node-body-signature runtime-root))
|
||||||
(let* ((cache (ebox--buffer-render-signature-cache buffer))
|
(let* ((cache (ebox--buffer-render-signature-cache buffer))
|
||||||
@ -5027,12 +5008,9 @@
|
|||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-test*")
|
(generate-new-buffer-name " *ebox-test*")
|
||||||
layout))
|
layout))
|
||||||
(let* ((root-id (ebox--ensure-node-id layout))
|
(let* ((runtime-root (ebox-test--runtime-root buffer))
|
||||||
(scroll-node-id (ebox--ensure-node-id scroll-box))
|
(runtime-scroll (car (plist-get runtime-root :children)))
|
||||||
(stable-node-id (ebox--ensure-node-id stable))
|
(runtime-stable (cadr (plist-get runtime-root :children))))
|
||||||
(runtime-root (ebox--buffer-runtime-node buffer root-id))
|
|
||||||
(runtime-scroll (ebox--buffer-runtime-node buffer scroll-node-id))
|
|
||||||
(runtime-stable (ebox--buffer-runtime-node buffer stable-node-id)))
|
|
||||||
(ebox--with-buffer-render-context buffer
|
(ebox--with-buffer-render-context buffer
|
||||||
(ebox--render-cache-node-body-signature runtime-root))
|
(ebox--render-cache-node-body-signature runtime-root))
|
||||||
(let* ((cache (ebox--buffer-render-signature-cache buffer))
|
(let* ((cache (ebox--buffer-render-signature-cache buffer))
|
||||||
@ -5251,7 +5229,7 @@
|
|||||||
(apply original arguments))))
|
(apply original arguments))))
|
||||||
(setq fast (ebox-render box)))
|
(setq fast (ebox-render box)))
|
||||||
(should (= fast-path-calls 1))
|
(should (= fast-path-calls 1))
|
||||||
(should (eq (gethash 'fixed-wrapper ebox--region-box-table) box))
|
(should-not (gethash 'fixed-wrapper ebox--region-box-table))
|
||||||
(should-not (gethash 'fixed-wrapper ebox--scroll-global-state))
|
(should-not (gethash 'fixed-wrapper ebox--scroll-global-state))
|
||||||
(should (equal-including-properties fast generic))))
|
(should (equal-including-properties fast generic))))
|
||||||
|
|
||||||
@ -5604,7 +5582,8 @@
|
|||||||
(should (= register-count 0))
|
(should (= register-count 0))
|
||||||
(should (= snapshot-count 0))
|
(should (= snapshot-count 0))
|
||||||
(should (= runtime-node-ids-count 0))
|
(should (= runtime-node-ids-count 0))
|
||||||
(let* ((root-node-id (plist-get layout :node-id))
|
(let* ((root-node-id
|
||||||
|
(plist-get (ebox-test--runtime-root buffer) :node-id))
|
||||||
(target-id (ebox-test--selector-region-id
|
(target-id (ebox-test--selector-region-id
|
||||||
buffer "#lazy-initial-06")))
|
buffer "#lazy-initial-06")))
|
||||||
(should (ebox--layout-snapshot buffer root-node-id))
|
(should (ebox--layout-snapshot buffer root-node-id))
|
||||||
@ -6550,8 +6529,10 @@
|
|||||||
(ebox-test--with-rendered-buffer layout
|
(ebox-test--with-rendered-buffer layout
|
||||||
(let ((region-id (ebox-test--selector-region-id (current-buffer) "#target"))
|
(let ((region-id (ebox-test--selector-region-id (current-buffer) "#target"))
|
||||||
(unrelated-child-id
|
(unrelated-child-id
|
||||||
(ebox--ensure-node-id
|
(plist-get
|
||||||
(car (last (ebox--node-children layout)))))
|
(car (last (ebox--node-children
|
||||||
|
(ebox-test--runtime-root (current-buffer)))))
|
||||||
|
:node-id))
|
||||||
(span-snapshot-calls 0)
|
(span-snapshot-calls 0)
|
||||||
(measure-calls 0)
|
(measure-calls 0)
|
||||||
(original (symbol-function 'ebox--ensure-layout-snapshot-spans))
|
(original (symbol-function 'ebox--ensure-layout-snapshot-spans))
|
||||||
@ -6758,8 +6739,10 @@
|
|||||||
(ebox-region-update flex-wrapper-id :color "#7C3AED")
|
(ebox-region-update flex-wrapper-id :color "#7C3AED")
|
||||||
(should (eq (plist-get (ebox-test--buffer-update-report) :strategy)
|
(should (eq (plist-get (ebox-test--buffer-update-report) :strategy)
|
||||||
'paint-patch))
|
'paint-patch))
|
||||||
(should (equal (car (plist-get (ebox-test--buffer-update-report) :owner-ids))
|
(should
|
||||||
(ebox--ensure-node-id flex)))
|
(equal
|
||||||
|
(car (plist-get (ebox-test--buffer-update-report) :owner-ids))
|
||||||
|
(gethash flex-wrapper-id region-node-table)))
|
||||||
(should (= recursive-calls 0))
|
(should (= recursive-calls 0))
|
||||||
|
|
||||||
(ebox-incremental-begin-batch buffer)
|
(ebox-incremental-begin-batch buffer)
|
||||||
@ -9617,7 +9600,10 @@
|
|||||||
:content ,(format "prefix %02d" idx)
|
:content ,(format "prefix %02d" idx)
|
||||||
:width (100) :height 2)))))
|
:width (100) :height 2)))))
|
||||||
(limit 4)
|
(limit 4)
|
||||||
(full-lines (ebox-string-lines (ebox-render node)))
|
(_index (ebox--runtime-index node t))
|
||||||
|
(full-lines
|
||||||
|
(let ((ebox--surface-materialization-active t))
|
||||||
|
(ebox-string-lines (ebox--render-layout node))))
|
||||||
(window (ebox--render-flex-window-lines node limit)))
|
(window (ebox--render-flex-window-lines node limit)))
|
||||||
(should-not (plist-get window :complete))
|
(should-not (plist-get window :complete))
|
||||||
(should (equal (mapcar #'substring-no-properties
|
(should (equal (mapcar #'substring-no-properties
|
||||||
@ -9740,12 +9726,13 @@
|
|||||||
(lambda (&rest args)
|
(lambda (&rest args)
|
||||||
(cl-incf multiline-horizontal-concat-count)
|
(cl-incf multiline-horizontal-concat-count)
|
||||||
(apply original-horizontal-concat args))))
|
(apply original-horizontal-concat args))))
|
||||||
(let ((window (ebox--render-flex-window-lines node 4)))
|
(let* ((ebox--surface-materialization-active t)
|
||||||
|
(window (ebox--render-flex-window-lines node 4)))
|
||||||
(should window)
|
(should window)
|
||||||
(should-not (plist-get window :complete))
|
(should-not (plist-get window :complete))
|
||||||
(should (= (length (plist-get window :lines)) 4))
|
(should (= (length (plist-get window :lines)) 4))
|
||||||
(should
|
(should
|
||||||
(equal-including-properties
|
(ebox-test--rendered-visually-equal-p
|
||||||
(ebox-lines-join (seq-take full-lines 4))
|
(ebox-lines-join (seq-take full-lines 4))
|
||||||
(ebox-lines-join (plist-get window :lines))))))
|
(ebox-lines-join (plist-get window :lines))))))
|
||||||
(should (= full-wrapper-count 0))
|
(should (= full-wrapper-count 0))
|
||||||
@ -9766,7 +9753,10 @@
|
|||||||
collect
|
collect
|
||||||
`(box :content ,(format "complete %02d" idx)
|
`(box :content ,(format "complete %02d" idx)
|
||||||
:width (100) :height 2)))))
|
:width (100) :height 2)))))
|
||||||
(full (ebox-render node))
|
(_index (ebox--runtime-index node t))
|
||||||
|
(full
|
||||||
|
(let ((ebox--surface-materialization-active t))
|
||||||
|
(ebox--render-layout node)))
|
||||||
(full-wrapper-count 0)
|
(full-wrapper-count 0)
|
||||||
(chunk-wrapper-count 0)
|
(chunk-wrapper-count 0)
|
||||||
(original-full-wrapper
|
(original-full-wrapper
|
||||||
@ -9781,7 +9771,8 @@
|
|||||||
(lambda (&rest args)
|
(lambda (&rest args)
|
||||||
(cl-incf chunk-wrapper-count)
|
(cl-incf chunk-wrapper-count)
|
||||||
(apply original-chunk-wrapper args))))
|
(apply original-chunk-wrapper args))))
|
||||||
(let ((window (ebox--render-flex-window-lines node 8)))
|
(let* ((ebox--surface-materialization-active t)
|
||||||
|
(window (ebox--render-flex-window-lines node 8)))
|
||||||
(should window)
|
(should window)
|
||||||
(should (plist-get window :complete))
|
(should (plist-get window :complete))
|
||||||
(should
|
(should
|
||||||
@ -14010,7 +14001,8 @@
|
|||||||
(generate-new-buffer-name " *ebox-prewarm-test*") layout))
|
(generate-new-buffer-name " *ebox-prewarm-test*") layout))
|
||||||
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
(setq root-id (ebox-test--selector-region-id buffer "#root"))
|
||||||
(setq expected-viewport-ids
|
(setq expected-viewport-ids
|
||||||
(ebox--viewport-dependent-node-ids layout))
|
(ebox--viewport-dependent-node-ids
|
||||||
|
(ebox-test--runtime-root buffer)))
|
||||||
(let* ((state (ebox-scroll-state root-id))
|
(let* ((state (ebox-scroll-state root-id))
|
||||||
(before-lines (length (plist-get state :content-lines)))
|
(before-lines (length (plist-get state :content-lines)))
|
||||||
(materialize (plist-get state :materialize-content-lines)))
|
(materialize (plist-get state :materialize-content-lines)))
|
||||||
@ -14733,8 +14725,9 @@
|
|||||||
'minimal-spans))
|
'minimal-spans))
|
||||||
(should (plist-get (ebox-test--buffer-update-report)
|
(should (plist-get (ebox-test--buffer-update-report)
|
||||||
:published-buffer-spans))
|
:published-buffer-spans))
|
||||||
(let ((expected (let ((ebox-viewport-width 160))
|
(let ((expected
|
||||||
(ebox-render layout))))
|
(let ((ebox-viewport-width 160))
|
||||||
|
(ebox-render (ebox-test--runtime-root buffer)))))
|
||||||
(with-current-buffer buffer
|
(with-current-buffer buffer
|
||||||
(should (equal-including-properties
|
(should (equal-including-properties
|
||||||
(buffer-substring (point-min) (point-max))
|
(buffer-substring (point-min) (point-max))
|
||||||
|
|||||||
@ -548,9 +548,11 @@
|
|||||||
|
|
||||||
(ert-deftest ebox-flex-reuses-measured-non-box-child-render ()
|
(ert-deftest ebox-flex-reuses-measured-non-box-child-render ()
|
||||||
"Flex should not fully rerender a measured non-box child during one render."
|
"Flex should not fully rerender a measured non-box child during one render."
|
||||||
(let* ((child (ebox-column
|
(let* ((child (plist-put
|
||||||
(ebox-create :content "A" :width '(100))
|
(ebox-column
|
||||||
(ebox-create :content "B" :width '(100))))
|
(ebox-create :content "A" :width '(100))
|
||||||
|
(ebox-create :content "B" :width '(100)))
|
||||||
|
:key 'measured-child))
|
||||||
(layout (ebox-flex :width '(240) :flex-wrap 'wrap
|
(layout (ebox-flex :width '(240) :flex-wrap 'wrap
|
||||||
child
|
child
|
||||||
(ebox-column
|
(ebox-column
|
||||||
@ -560,7 +562,7 @@
|
|||||||
(original-render (symbol-function 'ebox-render)))
|
(original-render (symbol-function 'ebox-render)))
|
||||||
(cl-letf (((symbol-function 'ebox-render)
|
(cl-letf (((symbol-function 'ebox-render)
|
||||||
(lambda (node)
|
(lambda (node)
|
||||||
(when (eq node child)
|
(when (eq (plist-get node :key) 'measured-child)
|
||||||
(cl-incf calls))
|
(cl-incf calls))
|
||||||
(funcall original-render node))))
|
(funcall original-render node))))
|
||||||
(ebox-render layout))
|
(ebox-render layout))
|
||||||
@ -607,9 +609,11 @@
|
|||||||
|
|
||||||
(ert-deftest ebox-flex-reuses-natural-non-box-render-before-cross-padding ()
|
(ert-deftest ebox-flex-reuses-natural-non-box-render-before-cross-padding ()
|
||||||
"Final cross padding should reuse a naturally sized non-box render."
|
"Final cross padding should reuse a naturally sized non-box render."
|
||||||
(let* ((source (ebox-column
|
(let* ((source (plist-put
|
||||||
(ebox-create :content "A" :width '(80))
|
(ebox-column
|
||||||
(ebox-create :content "A2" :width '(80))))
|
(ebox-create :content "A" :width '(80))
|
||||||
|
(ebox-create :content "A2" :width '(80)))
|
||||||
|
:key 'natural-child))
|
||||||
(layout
|
(layout
|
||||||
(ebox-flex :width '(220)
|
(ebox-flex :width '(220)
|
||||||
(ebox-flex-item source :flex-grow 1 :flex-basis '(80))
|
(ebox-flex-item source :flex-grow 1 :flex-basis '(80))
|
||||||
@ -619,7 +623,7 @@
|
|||||||
rendered)
|
rendered)
|
||||||
(cl-letf (((symbol-function 'ebox-render)
|
(cl-letf (((symbol-function 'ebox-render)
|
||||||
(lambda (node)
|
(lambda (node)
|
||||||
(when (eq node source)
|
(when (eq (plist-get node :key) 'natural-child)
|
||||||
(cl-incf calls))
|
(cl-incf calls))
|
||||||
(funcall original node))))
|
(funcall original node))))
|
||||||
(setq rendered (ebox-render layout)))
|
(setq rendered (ebox-render layout)))
|
||||||
@ -655,14 +659,17 @@
|
|||||||
(box :content "B" :height 1)
|
(box :content "B" :height 1)
|
||||||
(box :content "C" :height 1)
|
(box :content "C" :height 1)
|
||||||
(box :content "D" :height 1))))))
|
(box :content "D" :height 1))))))
|
||||||
(root-id
|
(buffer (generate-new-buffer " *ebox-flex-scroll-state*")))
|
||||||
(plist-get (car (ebox-selector-query-all layout "#root"))
|
(unwind-protect
|
||||||
:region-id)))
|
(progn
|
||||||
(ebox-render layout)
|
(ebox-render-to-buffer buffer layout)
|
||||||
(let ((state (ebox-scroll-state root-id)))
|
(let* ((match (car (ebox-selector-query-buffer buffer "#root")))
|
||||||
(should (equal (ebox-get (plist-get state :box) :height) 2))
|
(state (ebox-scroll-state (plist-get match :region-id))))
|
||||||
(should-not (plist-get state :content-lines-complete-p))
|
(should (equal (ebox-get (plist-get state :box) :height) 2))
|
||||||
(should-not (plist-get state :render-content-prefix)))))
|
(should-not (plist-get state :content-lines-complete-p))
|
||||||
|
(should-not (plist-get state :render-content-prefix))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-flex-natural-row-padding-uses-rendered-overflow-width ()
|
(ert-deftest ebox-flex-natural-row-padding-uses-rendered-overflow-width ()
|
||||||
"Natural cross padding should preserve a row item's rendered overflow width."
|
"Natural cross padding should preserve a row item's rendered overflow width."
|
||||||
|
|||||||
@ -172,15 +172,14 @@
|
|||||||
(right (ebox-create :content "B" :id "right" :width 20))
|
(right (ebox-create :content "B" :id "right" :width 20))
|
||||||
(node (ebox-grid :grid-template-columns '((20) (20)) left right))
|
(node (ebox-grid :grid-template-columns '((20) (20)) left right))
|
||||||
(buffer (ebox-render-to-buffer
|
(buffer (ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-grid-update*") node))
|
(generate-new-buffer-name " *ebox-grid-update*") node)))
|
||||||
(region-id (car (ebox-region-ids left))))
|
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(should (with-current-buffer buffer
|
(should (with-current-buffer buffer
|
||||||
(and (string-match-p "A" (buffer-string))
|
(and (string-match-p "A" (buffer-string))
|
||||||
(string-match-p "B" (buffer-string)))))
|
(string-match-p "B" (buffer-string)))))
|
||||||
(with-current-buffer buffer
|
(ebox-region-update (ebox-region-resolve buffer "left")
|
||||||
(ebox-region-update region-id :content "Updated"))
|
:content "Updated")
|
||||||
(let ((updated (with-current-buffer buffer (buffer-string))))
|
(let ((updated (with-current-buffer buffer (buffer-string))))
|
||||||
(should (string-match-p "Updated" updated))
|
(should (string-match-p "Updated" updated))
|
||||||
(should (string-match-p "B" updated))))
|
(should (string-match-p "B" updated))))
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"ebox-layout.el"
|
"ebox-layout.el"
|
||||||
"ebox-flex.el"
|
"ebox-flex.el"
|
||||||
"ebox-grid.el"
|
"ebox-grid.el"
|
||||||
|
"ebox-selector.el"
|
||||||
"ebox-native-reflow.el")
|
"ebox-native-reflow.el")
|
||||||
"Source files that currently own public autoload entry points.")
|
"Source files that currently own public autoload entry points.")
|
||||||
|
|
||||||
@ -126,6 +127,7 @@
|
|||||||
("ebox-scroll-down" . "defun")
|
("ebox-scroll-down" . "defun")
|
||||||
("ebox-scroll-up" . "defun")
|
("ebox-scroll-up" . "defun")
|
||||||
("ebox-region-update" . "defun")
|
("ebox-region-update" . "defun")
|
||||||
|
("ebox-region-resolve" . "defun")
|
||||||
("ebox-buffer-update-report" . "defun")
|
("ebox-buffer-update-report" . "defun")
|
||||||
("ebox-byte-compile" . "defun")
|
("ebox-byte-compile" . "defun")
|
||||||
("ebox-native-build" . "defun")
|
("ebox-native-build" . "defun")
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
(node (funcall builder)))
|
(node (funcall builder)))
|
||||||
(if projector-p
|
(if projector-p
|
||||||
(tp-surface-materialize-string
|
(tp-surface-materialize-string
|
||||||
(ebox-surface--producer node))
|
(ebox-surface-producer node))
|
||||||
(ebox-render node))))
|
(ebox-render node))))
|
||||||
|
|
||||||
(defun ebox-surface-test--walk-runtime (node function)
|
(defun ebox-surface-test--walk-runtime (node function)
|
||||||
@ -141,9 +141,9 @@
|
|||||||
|
|
||||||
(ert-deftest ebox-surface-assigns-object-identity-before-layout ()
|
(ert-deftest ebox-surface-assigns-object-identity-before-layout ()
|
||||||
"Every candidate runtime node should own a TP object before layout starts."
|
"Every candidate runtime node should own a TP object before layout starts."
|
||||||
(let ((original (symbol-function 'ebox-render))
|
(let ((original (symbol-function 'ebox--render-layout))
|
||||||
checked captured)
|
checked captured)
|
||||||
(cl-letf (((symbol-function 'ebox-render)
|
(cl-letf (((symbol-function 'ebox--render-layout)
|
||||||
(lambda (node)
|
(lambda (node)
|
||||||
(unless checked
|
(unless checked
|
||||||
(setq checked t)
|
(setq checked t)
|
||||||
@ -199,7 +199,7 @@
|
|||||||
(setq surface
|
(setq surface
|
||||||
(tp-surface-mount
|
(tp-surface-mount
|
||||||
buffer
|
buffer
|
||||||
(ebox-surface--producer
|
(ebox-surface-producer
|
||||||
(funcall (cdr (assq 'box (ebox-surface-test--fixtures)))))
|
(funcall (cdr (assq 'box (ebox-surface-test--fixtures)))))
|
||||||
'(:capability content)))
|
'(:capability content)))
|
||||||
(let* ((state (tp-surface-client-state surface))
|
(let* ((state (tp-surface-client-state surface))
|
||||||
@ -228,11 +228,11 @@
|
|||||||
(progn
|
(progn
|
||||||
(setq first
|
(setq first
|
||||||
(tp-surface-mount
|
(tp-surface-mount
|
||||||
first-buffer (ebox-surface--producer source)
|
first-buffer (ebox-surface-producer source)
|
||||||
'(:capability content)))
|
'(:capability content)))
|
||||||
(setq second
|
(setq second
|
||||||
(tp-surface-mount
|
(tp-surface-mount
|
||||||
second-buffer (ebox-surface--producer source)
|
second-buffer (ebox-surface-producer source)
|
||||||
'(:capability content)))
|
'(:capability content)))
|
||||||
(let* ((first-state (tp-surface-client-state first))
|
(let* ((first-state (tp-surface-client-state first))
|
||||||
(second-state (tp-surface-client-state second))
|
(second-state (tp-surface-client-state second))
|
||||||
@ -264,7 +264,7 @@
|
|||||||
(_mount
|
(_mount
|
||||||
(setq surface
|
(setq surface
|
||||||
(tp-surface-mount
|
(tp-surface-mount
|
||||||
buffer (ebox-surface--producer first-source)
|
buffer (ebox-surface-producer first-source)
|
||||||
'(:capability content))))
|
'(:capability content))))
|
||||||
(first-state (tp-surface-client-state surface))
|
(first-state (tp-surface-client-state surface))
|
||||||
(left (ebox-surface-test--object-by-key first-state 'left))
|
(left (ebox-surface-test--object-by-key first-state 'left))
|
||||||
@ -274,7 +274,7 @@
|
|||||||
(ebox-create :key 'right :content "Right!" :width '(60))
|
(ebox-create :key 'right :content "Right!" :width '(60))
|
||||||
(ebox-create :key 'left :content "Left!" :width '(60)))))
|
(ebox-create :key 'left :content "Left!" :width '(60)))))
|
||||||
(tp-surface-update
|
(tp-surface-update
|
||||||
surface (ebox-surface--producer next-source first-state))
|
surface (ebox-surface-producer next-source first-state))
|
||||||
(let ((next-state (tp-surface-client-state surface)))
|
(let ((next-state (tp-surface-client-state surface)))
|
||||||
(should (eq left
|
(should (eq left
|
||||||
(ebox-surface-test--object-by-key next-state 'left)))
|
(ebox-surface-test--object-by-key next-state 'left)))
|
||||||
@ -289,6 +289,252 @@
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-render-materializes-through-tp-surface ()
|
||||||
|
"The public string renderer should materialize one ephemeral TP surface."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((original (symbol-function 'tp-surface-materialize-string))
|
||||||
|
(calls 0))
|
||||||
|
(cl-letf (((symbol-function 'tp-surface-materialize-string)
|
||||||
|
(lambda (producer)
|
||||||
|
(cl-incf calls)
|
||||||
|
(funcall original producer))))
|
||||||
|
(should (stringp
|
||||||
|
(ebox-render
|
||||||
|
(ebox-create :content "Materialized" :width '(100))))))
|
||||||
|
(should (= calls 1))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-render-is-repeatable-without-consuming-runtime-identities ()
|
||||||
|
"Ephemeral rendering should be exact and leave live identity counters alone."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let* ((source (ebox-create :content "Repeatable" :width '(100)))
|
||||||
|
(first (ebox-render source))
|
||||||
|
(second (ebox-render source)))
|
||||||
|
(should (equal-including-properties first second))
|
||||||
|
(should (= ebox--region-id-counter 0))
|
||||||
|
(should (= ebox--runtime-node-id-counter 0))
|
||||||
|
(should-not (plist-member source :region-id))
|
||||||
|
(should-not (plist-member source :node-id))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-render-to-buffer-mounts-one-tp-surface ()
|
||||||
|
"The public buffer renderer should expose TP's committed client state."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-public-surface*"))
|
||||||
|
surface)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :content "Mounted" :width '(100)))
|
||||||
|
(setq surface
|
||||||
|
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||||
|
(should (tp-surface-live-p surface))
|
||||||
|
(should (eq (ebox--buffer-render-state buffer)
|
||||||
|
(tp-surface-client-state surface))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))
|
||||||
|
(should-not (tp-surface-live-p surface))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-render-to-buffer-reuses-one-source-across-buffers ()
|
||||||
|
"The public mount path should never transfer ownership of its source tree."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let* ((source
|
||||||
|
(ebox-create :key 'shared :content "Shared" :width '(100)))
|
||||||
|
(first (generate-new-buffer " *ebox-public-first*"))
|
||||||
|
(second (generate-new-buffer " *ebox-public-second*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer first source)
|
||||||
|
(ebox-render-to-buffer second source)
|
||||||
|
(should (equal
|
||||||
|
(with-current-buffer first
|
||||||
|
(substring-no-properties (buffer-string)))
|
||||||
|
(with-current-buffer second
|
||||||
|
(substring-no-properties (buffer-string)))))
|
||||||
|
(should-not
|
||||||
|
(equal (ebox-region-ids (ebox--buffer-root-node first))
|
||||||
|
(ebox-region-ids (ebox--buffer-root-node second))))
|
||||||
|
(should-not
|
||||||
|
(eq (plist-get (ebox--buffer-root-node first) :surface-object)
|
||||||
|
(plist-get (ebox--buffer-root-node second) :surface-object)))
|
||||||
|
(should-not (plist-member source :node-id))
|
||||||
|
(should-not (plist-member source :region-id))
|
||||||
|
(should-not (plist-member source :surface-object)))
|
||||||
|
(dolist (buffer (list first second))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-publishes-through-the-mounted-tp-surface ()
|
||||||
|
"Declarative commits should bypass every legacy Ebox buffer publisher."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-surface-commit*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "Before" :width '(100)))
|
||||||
|
(let* ((surface (with-current-buffer
|
||||||
|
buffer ebox-surface--buffer-surface))
|
||||||
|
(revision (tp-surface-revision surface))
|
||||||
|
report)
|
||||||
|
(cl-letf (((symbol-function 'ebox-incremental-commit)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(error "Legacy Ebox publisher was called"))))
|
||||||
|
(setq report
|
||||||
|
(ebox-commit
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "After"
|
||||||
|
:width '(100)))))
|
||||||
|
(should (= (tp-surface-revision surface) (1+ revision)))
|
||||||
|
(should (plist-get report :runtime-published))
|
||||||
|
(should (equal report (ebox-buffer-update-report buffer)))
|
||||||
|
(should (string-match-p "After" (with-current-buffer
|
||||||
|
buffer (buffer-string))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-callback-failure-rolls-back-tp-and-ebox-state ()
|
||||||
|
"A failed publication callback should restore one shared old generation."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-surface-rollback*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "Stable" :width '(100)))
|
||||||
|
(let* ((surface (with-current-buffer
|
||||||
|
buffer ebox-surface--buffer-surface))
|
||||||
|
(state (tp-surface-client-state surface))
|
||||||
|
(revision (tp-surface-revision surface))
|
||||||
|
(contents (with-current-buffer
|
||||||
|
buffer
|
||||||
|
(buffer-substring (point-min) (point-max)))))
|
||||||
|
(should-error
|
||||||
|
(ebox-commit
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "Rejected" :width '(100))
|
||||||
|
(lambda (_report) (error "Reject publication"))))
|
||||||
|
(should (= (tp-surface-revision surface) revision))
|
||||||
|
(should (eq (tp-surface-client-state surface) state))
|
||||||
|
(should (equal-including-properties
|
||||||
|
(with-current-buffer
|
||||||
|
buffer
|
||||||
|
(buffer-substring (point-min) (point-max)))
|
||||||
|
contents))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-killed-buffer-rollback-does-not-revive-runtime ()
|
||||||
|
"A failed commit must not restore runtime state for a killed buffer."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-surface-killed-rollback*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "Stable" :width '(100)))
|
||||||
|
(should (gethash buffer ebox--buffer-render-state-table))
|
||||||
|
(should-error
|
||||||
|
(ebox-commit
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :content "Rejected" :width '(100))
|
||||||
|
(lambda (_report)
|
||||||
|
(kill-buffer buffer)
|
||||||
|
(error "Reject publication after teardown"))))
|
||||||
|
(should-not (buffer-live-p buffer))
|
||||||
|
(should-not (gethash buffer ebox--buffer-render-state-table)))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-logical-candidate-report-is-the-published-client-report ()
|
||||||
|
"Logical candidates should publish one report through TP client state."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-surface-candidate*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'root :host-ref 'root
|
||||||
|
:content "Before" :width '(100)))
|
||||||
|
(let ((candidate (ebox-candidate-begin buffer)))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'root
|
||||||
|
(ebox-create :key 'root :host-ref 'root
|
||||||
|
:content "After" :width '(100)))
|
||||||
|
(let ((report (ebox-commit buffer candidate)))
|
||||||
|
(should (equal report (ebox-buffer-update-report buffer)))
|
||||||
|
(should (eq (plist-get report :constraint-source)
|
||||||
|
'declarative))
|
||||||
|
(should (string-match-p "After" (with-current-buffer
|
||||||
|
buffer (buffer-string)))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-region-handles-are-surface-scoped ()
|
||||||
|
"One logical id should resolve to distinct handles on independent surfaces."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let* ((source
|
||||||
|
(ebox-create :id "status" :content "Ready" :width '(100)))
|
||||||
|
(first (generate-new-buffer " *ebox-handle-first*"))
|
||||||
|
(second (generate-new-buffer " *ebox-handle-second*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer first source)
|
||||||
|
(ebox-render-to-buffer second source)
|
||||||
|
(let ((first-handle (ebox-region-resolve first "status"))
|
||||||
|
(second-handle (ebox-region-resolve second 'status)))
|
||||||
|
(should (ebox-region-handle-p first-handle))
|
||||||
|
(should (ebox-region-handle-p second-handle))
|
||||||
|
(should-not (eq first-handle second-handle))
|
||||||
|
(ebox-region-update first-handle :content "Changed")
|
||||||
|
(should (string-match-p "Changed"
|
||||||
|
(with-current-buffer first
|
||||||
|
(buffer-string))))
|
||||||
|
(should (string-match-p "Ready"
|
||||||
|
(with-current-buffer second
|
||||||
|
(buffer-string))))))
|
||||||
|
(dolist (buffer (list first second))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-selector-query-returns-an-editable-region-handle ()
|
||||||
|
"A live selector match should carry the same handle accepted by updates."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-selector-handle*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :id "action" :content "Closed" :width '(100)))
|
||||||
|
(let* ((match (car (ebox-selector-query-buffer buffer "#action")))
|
||||||
|
(handle (plist-get match :region-handle)))
|
||||||
|
(should (ebox-region-handle-p handle))
|
||||||
|
(ebox-region-update handle :content "Open")
|
||||||
|
(should (string-match-p "Open"
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(buffer-string))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-region-handle-becomes-stale-with-its-object ()
|
||||||
|
"A handle should fail after a commit removes its retained object."
|
||||||
|
(ebox-surface-test--reset-render-state)
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-stale-handle*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'old :id "old"
|
||||||
|
:content "Old" :width '(100)))
|
||||||
|
(let ((handle (ebox-region-resolve buffer "old")))
|
||||||
|
(ebox-commit
|
||||||
|
buffer
|
||||||
|
(ebox-create :key 'new :id "new"
|
||||||
|
:content "New" :width '(100)))
|
||||||
|
(should-error (ebox-region-update handle :content "Invalid")
|
||||||
|
:type 'user-error)))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(provide 'ebox-surface-tests)
|
(provide 'ebox-surface-tests)
|
||||||
|
|
||||||
;;; ebox-surface-tests.el ends here
|
;;; ebox-surface-tests.el ends here
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user