tp/tp-surface.el

4518 lines
210 KiB
EmacsLisp

;;; tp-surface.el --- Retained text surfaces and publication -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Geekinney
;; Author: Geekinney (kinneyzhang666@gmail.com)
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of
;; the License, or (at your option) any later version.
;;; Commentary:
;; Generic retained plans, candidate object identity, marker-backed mounts,
;; range property ownership, side indexes, and atomic buffer publication.
;; This module knows nothing about a consumer's layout or domain vocabulary.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'tp-core)
(require 'tp-style)
(require 'tp-reactive)
(define-error 'tp-surface-error "TP retained surface error")
(define-error 'tp-invalid-surface-plan "Invalid TP surface plan"
'tp-surface-error)
(define-error 'tp-duplicate-object-key "Duplicate TP object key"
'tp-invalid-surface-plan)
(define-error 'tp-invalid-prepare-context "Invalid TP prepare context"
'tp-surface-error)
(define-error 'tp-stale-object "Stale TP object" 'tp-surface-error)
(define-error 'tp-cross-surface-object "Cross-surface TP object"
'tp-surface-error)
(define-error 'tp-orphan-object "Orphan TP object" 'tp-surface-error)
(define-error 'tp-capability-error "TP mount capability violation"
'tp-surface-error)
(define-error 'tp-stale-mount "Stale TP mount" 'tp-surface-error)
(define-error 'tp-property-conflict "TP property ownership conflict"
'tp-surface-error)
(define-error 'tp-dead-surface "Dead TP surface" 'tp-surface-error)
(define-error 'tp-invalid-range-anchor "Invalid TP range anchor"
'tp-surface-error)
(define-error 'tp-invalid-content-range "Invalid TP content leaf range"
'tp-surface-error)
(define-error 'tp-producer-buffer-mutation
"TP producer mutated a live surface buffer during prepare"
'tp-surface-error)
(define-error 'tp-publication-mismatch
"TP publication was changed by an external buffer hook"
'tp-surface-error)
(define-error 'tp-scope-mismatch "Scoped TP update changed outside its objects"
'tp-surface-error)
(define-error 'tp-owned-result-error "Invalid TP owned producer result"
'tp-surface-error)
(define-error 'tp-surface-rollback-error "TP surface rollback phase failed"
'tp-surface-error)
(cl-defstruct (tp-surface-plan (:constructor tp--make-surface-plan))
"Pure retained node data accepted by a TP surface."
key kind text props children tags capability)
(cl-defstruct (tp-surface-result
(:constructor tp--make-surface-result
(plan client-state &optional plan-owned-p
owner-context)))
"A producer result carrying PLAN and opaque CLIENT-STATE.
PLAN-OWNED-P marks an explicit candidate-local ownership transfer. The
owned result is bound to OWNER-CONTEXT and consumed once during prepare."
plan client-state plan-owned-p owner-context consumed-p)
(cl-defstruct (tp-retained-content-candidate
(:constructor tp--make-retained-content-candidate))
"Candidate for a content-only update on an unchanged TP object tree.
PLAN is the candidate's unchanged object topology with a new text leaf,
RENDERED is the candidate text, RANGES are the candidate-owned content-range
attachments, and PROPERTY-CONTRIBUTIONS are ordered relative range property
layers composed by TP during prepare. CONTEXT authenticates the one-shot
producer result to the active prepare transaction."
plan rendered ranges property-contributions client-state context
full-surface-p consumed-p)
(cl-defstruct (tp-commit-batch (:constructor tp--make-commit-batch))
"One precomputed content commit for a stable retained surface topology."
base-revision target-revision base-extent target-extent patches
coordinate-patches client-state)
(cl-defstruct (tp-commit-batch-candidate
(:constructor tp--make-commit-batch-candidate))
"One prepare-context-authenticated precomputed commit result."
batch context mount-specs exact-mount-specs-p client-state
exact-client-state-p reuse-mount-projection-p consumed-p)
(cl-defstruct (tp-surface
(:constructor tp--make-surface)
(:conc-name tp--surface-))
"One live retained tree mounted in a buffer."
id buffer capability start end options producer producer-binding plan objects
mounts index mount-index ledger client-state revision report live stale
observers)
(cl-defstruct (tp-object
(:constructor tp--make-surface-object)
(:conc-name tp--surface-object-))
"Opaque retained identity local to one surface."
id surface parent key kind path live disposed candidate-context)
(cl-defstruct (tp-prepare-context
(:constructor tp--make-prepare-context)
(:conc-name tp--context-))
"Short-lived candidate identity and attachment owner."
surface objects touched retained bindings attachments fragment-attachments
content-range-attachments child-seen
child-positions new-objects created-anchors active ephemeral)
(cl-defstruct (tp-range-anchor
(:constructor tp--make-range-anchor)
(:conc-name tp--anchor-))
"Opaque marker-backed host text range."
id buffer start end boundary-policy live stale surfaces candidate-context)
(cl-defstruct (tp--surface-mount (:constructor tp--make-surface-mount))
id object start end tags capability anchor)
(defun tp--mount-position (position)
"Return numeric POSITION for a marker or coordinate mount endpoint."
(if (markerp position) (marker-position position) position))
(cl-defstruct (tp--property-ledger (:constructor tp--make-property-ledger))
start end property baseline-present baseline-value published-present
published-value anchors)
(cl-defstruct (tp--prepared-surface (:constructor tp--make-prepared-surface))
surface context plan rendered mount-specs ledger-specs property-operations
objects client-state producer initial created removed moved reconciled
scope-objects scope-patches scope-fallback live-mounts live-mount-index
live-ledger report retained-content-p commit-batch
retained-mount-state-p mount-coordinate-updates mount-coordinate-undo)
(cl-defstruct (tp--surface-snapshot (:constructor tp--make-surface-snapshot))
plan objects mounts index mount-index ledger client-state producer revision
report live stale)
(defvar tp--surface-id-counter 0)
(defvar tp--object-id-counter 0)
(defvar tp--mount-id-counter 0)
(defvar tp--anchor-id-counter 0)
(defvar tp--surface-transaction-id 0)
(defvar tp--surfaces (make-hash-table :test #'eql :weakness 'value))
(defvar tp--current-prepare-context nil)
(defvar tp--surface-publishing nil)
(defvar tp--surface-guarding-prepare nil)
(defvar tp--surface-publication-step-function nil)
(defvar tp--surface-precommit-step-function nil)
(defvar tp--surface-cleanup-step-function nil)
(defvar tp--content-published-ranges nil)
(defvar-local tp--buffer-surfaces nil)
(defvar-local tp--surface-character-tick 0)
(defvar-local tp--surface-before-change-state nil)
(defconst tp--surface-producer-key '(tp/surface . producer))
(defconst tp--surface-extension-key 'tp-surface)
(defun tp--normalize-surface-observers (observers)
"Validate OBSERVERS and return them once each, in declaration order."
(unless (listp observers)
(signal 'wrong-type-argument (list 'listp observers)))
(let (normalized)
(dolist (observer observers)
(unless (functionp observer)
(signal 'wrong-type-argument (list 'functionp observer)))
(unless (memq observer normalized)
(push observer normalized)))
(nreverse normalized)))
(defun tp--surface-observation-clock ()
"Return the current time for optional surface publication observation."
(float-time))
(defun tp--plist-shape-p (value)
"Return non-nil when VALUE is an even property list with symbol keys."
(and (listp value)
(zerop (% (length value) 2))
(cl-loop for (key _value) on value by #'cddr always (symbolp key))))
(defun tp--validate-plan-fields (kind text props children capability)
"Validate plan KIND, TEXT, PROPS, CHILDREN, and CAPABILITY."
(unless kind
(signal 'tp-invalid-surface-plan (list :kind kind)))
(unless (or (null text) (stringp text))
(signal 'tp-invalid-surface-plan (list :text text)))
(unless (tp--plist-shape-p props)
(signal 'tp-invalid-surface-plan (list :props props)))
(unless (and (listp children) (cl-every #'tp-surface-plan-p children))
(signal 'tp-invalid-surface-plan (list :children children)))
(when (and text children)
(signal 'tp-invalid-surface-plan (list :text-and-children kind)))
(unless (memq capability '(nil content properties))
(signal 'tp-capability-error (list capability))))
(defun tp--validate-sibling-keys (children)
"Reject duplicate explicit keys among CHILDREN."
(let ((seen (make-hash-table :test #'equal)))
(dolist (child children)
(when-let* ((key (tp-surface-plan-key child)))
(when (gethash key seen)
(signal 'tp-duplicate-object-key (list key)))
(puthash key t seen)))))
(defun tp--validate-surface-plan-tree (plan)
"Validate PLAN recursively without copying its caller-owned values."
(unless (tp-surface-plan-p plan)
(signal 'wrong-type-argument (list 'tp-surface-plan-p plan)))
(let ((children (tp-surface-plan-children plan)))
(tp--validate-plan-fields
(tp-surface-plan-kind plan)
(tp-surface-plan-text plan)
(tp-surface-plan-props plan)
children
(tp-surface-plan-capability plan))
(tp--validate-sibling-keys children)
(dolist (child children)
(tp--validate-surface-plan-tree child)))
plan)
(defun tp--copy-surface-plan (plan &optional cache)
"Return a validated defensive snapshot of PLAN.
CACHE is shared by one top-level snapshot so repeated source identities retain
one copied identity across sibling nodes without canonicalizing equal values."
(unless (tp-surface-plan-p plan)
(signal 'wrong-type-argument (list 'tp-surface-plan-p plan)))
(let* ((cache (or cache (make-hash-table :test #'eq)))
(children
(mapcar (lambda (child)
(tp--copy-surface-plan child cache))
(tp-surface-plan-children plan)))
(kind (tp--copy-property-value (tp-surface-plan-kind plan) cache))
(text (tp--copy-property-value (tp-surface-plan-text plan) cache))
(props (tp--copy-property-value (tp-surface-plan-props plan) cache))
(capability (tp-surface-plan-capability plan)))
(tp--validate-plan-fields kind text props children capability)
(tp--validate-sibling-keys children)
(tp--make-surface-plan
:key (tp--copy-property-value (tp-surface-plan-key plan) cache)
:kind kind :text text :props props :children children
:tags (tp--copy-property-value (tp-surface-plan-tags plan) cache)
:capability capability)))
(cl-defun tp-surface-plan-create
(&key key kind text props children tags capability)
"Create a defensive surface plan node.
KEY is sibling-local identity, KIND is an opaque discriminator, TEXT is a
leaf string, PROPS are final direct text properties, CHILDREN are ordered
plans, TAGS are opaque metadata, and CAPABILITY is `content' or `properties'."
(tp--copy-surface-plan
(tp--make-surface-plan
:key key :kind kind :text text :props props :children children
:tags tags :capability capability)))
(cl-defun tp-surface-plan-create-owned
(&key key kind text props children tags capability)
"Create a candidate-local surface plan without copying its values.
KEY, KIND, TEXT, PROPS, CHILDREN, TAGS, and CAPABILITY have the same meanings
as in `tp-surface-plan-create'. The caller transfers ownership of those
values; after this call they must not be mutated or exposed to another owner.
Use `tp-surface-plan-create' for ordinary caller-owned values."
(tp--validate-surface-plan-tree
(tp--make-surface-plan
:key key :kind kind :text text :props props :children children
:tags tags :capability capability)))
(defun tp-surface-result-create (plan &optional client-state)
"Return a producer result containing PLAN and opaque CLIENT-STATE.
TP always renders the submitted plan inside its prepare transaction. This
keeps the plan as the single authoritative source for published content."
(tp--make-surface-result
(tp--copy-surface-plan plan) client-state))
(defun tp-surface-result-create-owned (context plan &optional client-state)
"Create a one-shot result by adopting candidate-local PLAN.
CONTEXT must be the active prepare context that owns PLAN. PLAN and all
nested values must be freshly allocated for that candidate and must not be
mutated or exposed after this call. CLIENT-STATE is opaque producer state.
Use `tp-surface-result-create' for ordinary caller-owned plans."
(tp--validate-prepare-context context)
(unless (eq context tp--current-prepare-context)
(signal 'tp-owned-result-error
(list :context-not-current context tp--current-prepare-context)))
(tp--make-surface-result
(tp--validate-surface-plan-tree plan) client-state t context))
(defun tp-surface-retained-content-result-create
(context plan rendered ranges &optional client-state full-surface-p
property-contributions)
"Create a one-shot retained content candidate for CONTEXT.
PLAN must preserve the committed TP object topology and contain the new text
leaf. RENDERED is the final propertized text and RANGES are candidate-local
content attachments already associated with PLAN's text leaf.
CLIENT-STATE is opaque owner state and FULL-SURFACE-P asserts complete scope.
PROPERTY-CONTRIBUTIONS is an ordered list of relative `:start', `:end', and
`:props' plists. TP composes them over RENDERED using registered property
merge policy before diff and publication. This entry point is intentionally
narrow: callers must prove that object topology is unchanged; TP still
validates every object, range, scope, and publication phase."
(tp--validate-prepare-context context)
(unless (eq context tp--current-prepare-context)
(signal 'tp-owned-result-error
(list :context-not-current context tp--current-prepare-context)))
(unless (and (tp-surface-plan-p plan)
(stringp rendered)
(proper-list-p ranges))
(signal 'tp-invalid-surface-plan
(list :retained-content plan rendered ranges)))
(tp--make-retained-content-candidate
:plan plan :rendered rendered :ranges ranges
:property-contributions property-contributions
:client-state client-state :context context
:full-surface-p full-surface-p))
(defun tp--compose-relative-property-contributions (rendered contributions)
"Compose ordered relative CONTRIBUTIONS over RENDERED.
Each contribution contains `:start', `:end', and direct `:props'."
(unless (proper-list-p contributions)
(signal 'tp-invalid-content-range
(list :property-contributions contributions)))
(let ((result (copy-sequence rendered))
(limit (length rendered)))
(dolist (contribution contributions)
(let ((start (plist-get contribution :start))
(end (plist-get contribution :end))
(props (plist-get contribution :props)))
(unless (and (integerp start) (integerp end)
(<= 0 start) (<= start end) (<= end limit))
(signal 'tp-invalid-content-range
(list :property-contribution contribution
:leaf-length limit)))
(unless (tp--plist-shape-p props)
(signal 'tp-invalid-surface-plan
(list :property-contribution-props props)))
(cl-loop for (property value) on props by #'cddr
do
(let ((position start)
(merge
(tp-property-policy-merge
(tp-register-text-property property))))
(while (< position end)
(let* ((next
(or (next-single-property-change
position property result end)
end))
(cell
(plist-member
(text-properties-at position result) property))
(target
(cond
((null value) nil)
(cell (funcall merge (cadr cell) value))
(t (tp--copy-property-value value)))))
(put-text-property position next property target result)
(setq position next)))))))
result))
(defun tp--property-value-equal-p (property left right)
"Return non-nil when PROPERTY values LEFT and RIGHT are policy-equal."
(funcall (tp-property-policy-equality (tp-register-text-property property))
left right))
(defun tp--plan-props-equal-p (left right)
"Return non-nil when text property plists LEFT and RIGHT are policy-equal."
(and (= (length left) (length right))
(cl-loop for (property value) on left by #'cddr
for cell = (plist-member right property)
always (and cell
(tp--property-value-equal-p
property value (cadr cell))))))
(defun tp--plan-equal-p (left right)
"Return non-nil when LEFT and RIGHT plans are semantically equal."
(and (equal (tp-surface-plan-key left) (tp-surface-plan-key right))
(equal (tp-surface-plan-kind left) (tp-surface-plan-kind right))
(let ((a (tp-surface-plan-text left))
(b (tp-surface-plan-text right)))
(if (and (stringp a) (stringp b))
(equal-including-properties a b)
(equal a b)))
(tp--plan-props-equal-p (tp-surface-plan-props left)
(tp-surface-plan-props right))
(equal (tp-surface-plan-tags left) (tp-surface-plan-tags right))
(eq (tp-surface-plan-capability left)
(tp-surface-plan-capability right))
(let ((a (tp-surface-plan-children left))
(b (tp-surface-plan-children right)))
(and (= (length a) (length b))
(cl-every #'identity
(cl-mapcar #'tp--plan-equal-p a b))))))
(defun tp--hash-copy (table)
"Return a shallow copy of hash TABLE."
(let ((copy (make-hash-table :test (hash-table-test table))))
(maphash (lambda (key value) (puthash key value copy)) table)
copy))
(defun tp--context-parent-key (context parent)
"Return the child-table key for PARENT in CONTEXT."
(or parent (tp--context-surface context)))
(defun tp--context-child-table (context parent)
"Return CONTEXT's explicit child-key table for PARENT."
(let* ((owner (tp--context-parent-key context parent))
(tables (tp--context-child-seen context)))
(or (gethash owner tables)
(let ((table (make-hash-table :test #'equal)))
(puthash owner table tables)
table))))
(defun tp--context-next-position (context parent)
"Return and advance CONTEXT's unkeyed child position for PARENT."
(let* ((owner (tp--context-parent-key context parent))
(positions (tp--context-child-positions context))
(position (gethash owner positions 0)))
(puthash owner (1+ position) positions)
position))
(defun tp--object-path-segment (context parent key kind)
"Return CONTEXT's candidate path segment for KEY and KIND below PARENT."
(if key
(let ((seen (tp--context-child-table context parent)))
(when (gethash key seen)
(signal 'tp-duplicate-object-key (list key)))
(puthash key t seen)
key)
(list :position (tp--context-next-position context parent) :kind kind)))
(defun tp--validate-context-parent (context parent)
"Validate PARENT for candidate CONTEXT."
(when parent
(unless (tp-object-p parent)
(signal 'wrong-type-argument (list 'tp-object-p parent)))
(unless (eq (tp--surface-object-surface parent)
(tp--context-surface context))
(signal 'tp-cross-surface-object (list parent)))
(unless (gethash (tp--surface-object-path parent)
(tp--context-objects context))
(signal 'tp-stale-object (list parent)))))
(defun tp--validate-prepare-context (context)
"Signal unless CONTEXT is active."
(unless (and (tp-prepare-context-p context) (tp--context-active context))
(signal 'tp-invalid-prepare-context (list context))))
(defun tp--new-candidate-object (context parent key kind path)
"Create a candidate object for KEY and KIND in CONTEXT below PARENT at PATH."
(let ((object (tp--make-surface-object
:id (cl-incf tp--object-id-counter)
:surface (tp--context-surface context) :parent parent
:key (tp--copy-property-value key)
:kind kind :path path
:candidate-context context)))
(push object (tp--context-new-objects context))
object))
(defun tp-object-ensure (context parent key kind)
"Return candidate identity for KEY and KIND below PARENT in CONTEXT."
(tp--validate-prepare-context context)
(tp--validate-context-parent context parent)
(unless kind
(signal 'tp-invalid-surface-plan (list :kind kind)))
(let* ((segment (tp--object-path-segment context parent key kind))
(path (append (and parent (tp--surface-object-path parent))
(list segment)))
(objects (tp--context-objects context))
(old (gethash path objects))
(object (if (and old (equal kind (tp--surface-object-kind old)))
old
(tp--new-candidate-object context parent key kind path))))
(puthash path object objects)
(puthash object t (tp--context-touched context))
object))
(defun tp-object-ensure-at (context parent key kind position)
"Return CONTEXT identity at explicit sibling POSITION below PARENT.
KEY and KIND identify normal keyed objects; POSITION is used only
for anonymous objects. This is the compiled-topology entry point and does not
depend on replaying preceding siblings to discover the same slot."
(tp--validate-prepare-context context)
(tp--validate-context-parent context parent)
(unless kind
(signal 'tp-invalid-surface-plan (list :kind kind)))
(unless (and (integerp position) (>= position 0))
(signal 'tp-invalid-surface-plan (list :position position)))
(let* ((segment
(if key
(let ((seen (tp--context-child-table context parent)))
(when (gethash key seen)
(signal 'tp-duplicate-object-key (list key)))
(puthash key t seen)
key)
(list :position position :kind kind)))
(path (append (and parent (tp--surface-object-path parent))
(list segment)))
(objects (tp--context-objects context))
(old (gethash path objects))
(object (if (and old (equal kind (tp--surface-object-kind old)))
old
(tp--new-candidate-object context parent key kind path))))
(puthash path object objects)
(puthash object t (tp--context-touched context))
object))
(defun tp-object-live-p (object)
"Return non-nil when OBJECT is committed on a live surface."
(and (tp-object-p object) (tp--surface-object-live object)
(not (tp--surface-object-disposed object))
(tp-surface-live-p (tp--surface-object-surface object))))
(defun tp-object-resolve (surface key-path)
"Resolve live object in SURFACE by explicit KEY-PATH without scanning text."
(tp--validate-live-surface surface)
(let ((object (gethash key-path (tp--surface-objects surface))))
(and (tp-object-live-p object) object)))
(defun tp-object-mounted-p (object)
"Return non-nil when live OBJECT owns at least one published mount.
This predicate does not copy mount tags; use `tp-object-mounts' when callers
need the public coordinate and metadata snapshots."
(unless (tp-object-live-p object)
(signal 'tp-stale-object (list object)))
(and (gethash object
(tp--surface-mount-index
(tp--surface-object-surface object)))
t))
(defun tp-object-mounts (object)
"Return OBJECT's live numeric mount ranges and opaque tags.
Each result is a plist with `:start', `:end', and `:tags'. Marker objects
remain private so callers cannot mutate TP's publication coordinates."
(unless (tp-object-live-p object)
(signal 'tp-stale-object (list object)))
(let* ((surface (tp--surface-object-surface object))
(mounts (gethash object (tp--surface-mount-index surface))))
(mapcar
(lambda (mount)
(list :start (tp--mount-position (tp--surface-mount-start mount))
:end (tp--mount-position (tp--surface-mount-end mount))
:tags (tp--copy-property-value
(tp--surface-mount-tags mount))))
mounts)))
(defun tp--make-context (surface &optional ephemeral)
"Create a prepare context for SURFACE.
When EPHEMERAL is non-nil, no identity may be promoted."
(tp--make-prepare-context
:surface surface
:objects (if (tp--surface-objects surface)
(tp--hash-copy (tp--surface-objects surface))
(make-hash-table :test #'equal))
:touched (make-hash-table :test #'eq)
:retained (make-hash-table :test #'eq)
:bindings (make-hash-table :test #'eq)
:attachments (make-hash-table :test #'eq)
:fragment-attachments (make-hash-table :test #'eq)
:content-range-attachments (make-hash-table :test #'eq)
:child-seen (make-hash-table :test #'eq)
:child-positions (make-hash-table :test #'eq)
:active t :ephemeral ephemeral))
(defun tp--validate-context-object (context object)
"Require OBJECT to be a candidate in active CONTEXT."
(tp--validate-prepare-context context)
(unless (and (tp-object-p object)
(eq (tp--surface-object-surface object)
(tp--context-surface context))
(or (gethash object (tp--context-touched context))
(gethash object (tp--context-retained context))))
(signal 'tp-stale-object (list object))))
(defun tp-object-retain (context object)
"Retain candidate OBJECT in CONTEXT even when it owns no output fragment."
(tp--validate-context-object context object)
(puthash object t (tp--context-retained context))
object)
(defun tp-object-reuse (context object)
"Reuse one proven unchanged live OBJECT in CONTEXT.
The object and its live bindings remain in the candidate without being marked
as touched. The caller owns the proof that identity, parent path, output, and
lifecycle are unchanged."
(tp--validate-prepare-context context)
(unless (tp-object-p object)
(signal 'wrong-type-argument (list 'tp-object-p object)))
(let* ((surface (tp--context-surface context))
(path (tp--surface-object-path object))
(objects (tp--context-objects context)))
(unless (and (tp-object-live-p object)
(eq (tp--surface-object-surface object) surface)
(eq object (gethash path objects)))
(signal 'tp-stale-object (list object)))
(puthash object t (tp--context-retained context))
(dolist (binding (tp-binding-owner-bindings object))
(when (tp-binding-live-p binding)
(puthash binding t (tp--context-bindings context)))))
object)
(defun tp--object-path-prefix-p (prefix path)
"Return non-nil when PREFIX is a path prefix of PATH."
(and (<= (length prefix) (length path))
(equal prefix (cl-subseq path 0 (length prefix)))))
(defun tp-object-retain-subtree (context root)
"Retain the existing object subtree rooted at live ROOT in CONTEXT.
ROOT must be a live object already owned by CONTEXT's surface. The operation
marks ROOT and every already-known descendant as touched and retained, and
also keeps their live bindings in the candidate lifecycle. It does not
create missing objects or prove that an omitted subtree is unchanged; the
producer must provide that proof and a complete candidate plan separately."
(tp--validate-prepare-context context)
(unless (tp-object-p root)
(signal 'wrong-type-argument (list 'tp-object-p root)))
(let* ((surface (tp--context-surface context))
(path (tp--surface-object-path root))
(objects (tp--context-objects context)))
(unless (and (tp-object-live-p root)
(eq root (gethash path objects)))
(signal 'tp-stale-object (list root)))
(maphash
(lambda (_candidate-path object)
(when (tp--object-path-prefix-p
path (tp--surface-object-path object))
(unless (and (eq (tp--surface-object-surface object) surface)
(tp-object-live-p object))
(signal 'tp-stale-object (list object)))
(puthash object t (tp--context-touched context))
(puthash object t (tp--context-retained context))
(dolist (binding (tp-binding-owner-bindings object))
(when (tp-binding-live-p binding)
(puthash binding t (tp--context-bindings context))))))
objects))
root)
(defun tp-object-reuse-subtree (context root)
"Reuse a proven unchanged live subtree in CONTEXT.
ROOT must be a live object already owned by CONTEXT's surface. The subtree's
objects and live bindings remain in the candidate, but are not marked touched
or reconciled. The caller must still provide a complete candidate plan and
prove that the subtree's identity, output, and lifecycle are unchanged."
(tp--validate-prepare-context context)
(unless (tp-object-p root)
(signal 'wrong-type-argument (list 'tp-object-p root)))
(let* ((surface (tp--context-surface context))
(path (tp--surface-object-path root))
(objects (tp--context-objects context)))
(unless (and (tp-object-live-p root)
(eq root (gethash path objects)))
(signal 'tp-stale-object (list root)))
(maphash
(lambda (_candidate-path object)
(when (tp--object-path-prefix-p
path (tp--surface-object-path object))
(unless (and (eq (tp--surface-object-surface object) surface)
(tp-object-live-p object))
(signal 'tp-stale-object (list object)))
(puthash object t (tp--context-retained context))
(dolist (binding (tp-binding-owner-bindings object))
(when (tp-binding-live-p binding)
(puthash binding t (tp--context-bindings context))))))
objects))
root)
(defun tp-object-attach-fragment (context object fragment &optional tags)
"Attach logical OBJECT to output FRAGMENT with opaque TAGS in CONTEXT.
OBJECT and FRAGMENT must be candidate handles from the same content surface.
The plan remains pure; this side attachment may give OBJECT several disjoint
marker-backed mounts after publication."
(tp--validate-context-object context object)
(tp--validate-context-object context fragment)
(unless (eq (tp--surface-capability (tp--context-surface context)) 'content)
(signal 'tp-capability-error (list :fragment-attachment)))
(let ((attachments (gethash fragment
(tp--context-fragment-attachments context))))
(when (assq object attachments)
(signal 'tp-surface-error (list :duplicate-fragment object fragment)))
(puthash fragment
(cons (cons object (tp--copy-property-value tags)) attachments)
(tp--context-fragment-attachments context)))
(tp-object-retain context object))
(defun tp--validate-content-range (start end)
"Signal unless START and END describe a nonempty local leaf range."
(unless (and (integerp start) (integerp end)
(>= start 0) (> end start))
(signal 'tp-invalid-content-range (list :start start :end end))))
(defun tp--copy-content-range-tags (tags &optional copies)
"Return a defensive copy of non-nil content-range TAGS.
COPIES, when non-nil, is a call-local `eq' cache owned by one bulk attach
operation. A singular attach always snapshots TAGS independently, so later
caller mutation cannot alter an earlier attachment."
(if (null tags)
nil
(let ((copy (and copies (gethash tags copies))))
(or copy
(let ((copy (tp--copy-property-value tags)))
(when copies
(puthash tags copy copies))
copy)))))
(defun tp-object-attach-content-range
(context object leaf start end &optional tags)
"Attach OBJECT to [START, END) in candidate content LEAF for CONTEXT.
START and END are character offsets relative to LEAF's text, not buffer
positions. LEAF must be a candidate text leaf in the same content surface;
the range is validated against its rendered length during prepare. TAGS are
opaque metadata copied into the eventual marker-backed mount."
(tp--validate-context-object context object)
(tp--validate-context-object context leaf)
(tp--validate-content-range start end)
(let ((attachments
(gethash leaf (tp--context-content-range-attachments context))))
(puthash leaf
(cons (list :object object :start start :end end
:tags (tp--copy-content-range-tags tags))
attachments)
(tp--context-content-range-attachments context)))
(unless (gethash object (tp--context-retained context))
(tp-object-retain context object)))
(defun tp--attach-content-ranges (context leaf ranges copy-tags-p)
"Attach RANGES to LEAF for CONTEXT, copying TAGS when COPY-TAGS-P is non-nil."
(tp--validate-context-object context leaf)
(let ((attachments (gethash leaf
(tp--context-content-range-attachments context)))
(copies (and copy-tags-p (make-hash-table :test #'eq)))
added)
(dolist (range ranges)
(let ((object (plist-get range :object))
(start (plist-get range :start))
(end (plist-get range :end))
(tags (plist-get range :tags)))
(tp--validate-context-object context object)
(tp--validate-content-range start end)
(push (list :object object :start start :end end
:tags (if copy-tags-p
(tp--copy-content-range-tags tags copies)
tags))
added)
(unless (gethash object (tp--context-retained context))
(tp-object-retain context object))))
(puthash leaf (nconc added attachments)
(tp--context-content-range-attachments context)))
ranges)
(defun tp-object-attach-content-ranges (context leaf ranges)
"Attach multiple OBJECT ranges to candidate content LEAF for CONTEXT.
RANGES contains plists with `:object', `:start', `:end', and optional `:tags'.
TP defensively copies every non-nil TAGS value. The ranges retain input order
after publication and reuse a snapshot only when the same TAGS object recurs
in one bulk attach."
(tp--attach-content-ranges context leaf ranges t))
(defun tp-object-attach-content-ranges-owned (context leaf ranges)
"Attach candidate RANGES to LEAF for CONTEXT with owned TAGS snapshots.
Use this only when each non-nil TAGS value was freshly allocated for the
candidate and will not be mutated or exposed to caller mutation after this
call. TP transfers those values without copying; failed candidates discard
the transferred values with the rest of their prepare state."
(tp--attach-content-ranges context leaf ranges nil))
(defun tp--touch-context-binding (context binding)
"Record BINDING as touched by CONTEXT."
(let ((owner (tp-binding-owner binding)))
(unless (and (tp-object-p owner)
(eq (tp--surface-object-surface owner)
(tp--context-surface context)))
(signal 'tp-cross-surface-object (list owner)))
(puthash binding t (tp--context-bindings context))))
(defun tp--plist-overlay (parent child &optional owned-p)
"Return a fresh plist where CHILD values override PARENT values.
When OWNED-P is non-nil, reuse candidate-owned property values while copying
only the plist spine. The non-owned path keeps the defensive value copies
needed by ordinary TP callers."
(let ((result (if owned-p
(copy-sequence parent)
(tp--copy-property-value parent))))
(cl-loop for (property value) on child by #'cddr
do (setq result
(plist-put result property
(if owned-p
value
(tp--copy-property-value value)))))
result))
(defun tp--plan-segment (plan position)
"Return PLAN's path segment at sibling POSITION."
(or (tp-surface-plan-key plan)
(list :position position :kind (tp-surface-plan-kind plan))))
(defun tp--render-plan (plan context &optional owned-p)
"Return PLAN's propertized string and side records in CONTEXT.
OWNED-P transfers candidate-local property values through the render pass."
(let (records)
(cl-labels
((walk (node path inherited offset)
(let* ((props (tp--plist-overlay
inherited (tp-surface-plan-props node) owned-p))
(begin offset)
(children (tp-surface-plan-children node))
pieces)
(if children
(cl-loop for child in children for position from 0
for child-path =
(append path (list (tp--plan-segment child position)))
for result = (walk child child-path props offset)
do (push (car result) pieces)
do (setq offset (cdr result)))
(let ((text (copy-sequence
(or (tp-surface-plan-text node) ""))))
(when (> (length text) 0)
(add-text-properties 0 (length text) props text))
(push text pieces)
(setq offset (+ offset (length text)))))
(let ((object (gethash path (tp--context-objects context))))
(unless object
(signal 'tp-orphan-object (list path)))
(push (list :path path :object object :start begin :end offset
:props props :tags (tp-surface-plan-tags node)
:leaf (null children))
records))
(cons (apply #'concat (nreverse pieces)) offset))))
(let* ((path (list (tp--plan-segment plan 0)))
(result (walk plan path nil 0)))
(cons (car result) (nreverse records))))))
(defun tp--ensure-plan-objects (context plan &optional parent)
"Ensure identities for PLAN recursively below PARENT in CONTEXT."
(let ((object (tp-object-ensure
context parent (tp-surface-plan-key plan)
(tp-surface-plan-kind plan))))
(dolist (child (tp-surface-plan-children plan))
(tp--ensure-plan-objects context child object))
object))
(defun tp--plan-paths (plan)
"Return PLAN's object paths in traversal order."
(let (paths)
(cl-labels
((walk (node path)
(push path paths)
(cl-loop for child in (tp-surface-plan-children node)
for position from 0
do (walk child
(append path
(list (tp--plan-segment child position)))))))
(walk plan (list (tp--plan-segment plan 0))))
(nreverse paths)))
(defun tp--validate-context-tree (context plan)
"Validate PLAN and CONTEXT's retained and fragment object paths."
(let ((expected (make-hash-table :test #'equal))
(actual (make-hash-table :test #'equal)))
(dolist (path (tp--plan-paths plan)) (puthash path t expected))
(maphash
(lambda (_path object)
(when (or (gethash object (tp--context-touched context))
(gethash object (tp--context-retained context)))
(puthash (tp--surface-object-path object) object actual)))
(tp--context-objects context))
(maphash
(lambda (path _present)
(unless (gethash path actual)
(signal 'tp-orphan-object (list :missing path))))
expected)
(maphash
(lambda (path object)
(unless (or (gethash path expected)
(gethash object (tp--context-retained context)))
(signal 'tp-orphan-object (list :extra path))))
actual)
(maphash
(lambda (fragment _attachments)
(unless (gethash (tp--surface-object-path fragment) expected)
(signal 'tp-orphan-object
(list :unplanned-fragment
(tp--surface-object-path fragment)))))
(tp--context-fragment-attachments context))))
(defun tp--validate-plan-capability (plan capability)
"Validate PLAN recursively against mount CAPABILITY."
(let ((declared (tp-surface-plan-capability plan)))
(when (and declared (not (eq declared capability)))
(signal 'tp-capability-error (list declared capability)))
(when (and (eq capability 'properties) (tp-surface-plan-text plan))
(signal 'tp-capability-error (list :properties-text)))
(dolist (child (tp-surface-plan-children plan))
(tp--validate-plan-capability child capability))))
(defun tp--producer-result (value surface options context)
"Normalize producer VALUE for SURFACE and active CONTEXT using OPTIONS."
(cond
((tp-commit-batch-candidate-p value)
(unless (and context
(tp--context-active context)
(eq context tp--current-prepare-context)
(eq context (tp-commit-batch-candidate-context value))
(not (tp-commit-batch-candidate-consumed-p value)))
(signal 'tp-owned-result-error (list value context)))
(setf (tp-commit-batch-candidate-consumed-p value) t
(tp-commit-batch-candidate-context value) nil)
(list :commit-batch value))
((tp-retained-content-candidate-p value)
(unless (and context
(tp--context-active context)
(eq context tp--current-prepare-context)
(eq context
(tp-retained-content-candidate-context value))
(not (tp-retained-content-candidate-consumed-p value)))
(signal 'tp-owned-result-error (list value context)))
(setf (tp-retained-content-candidate-consumed-p value) t
(tp-retained-content-candidate-context value) nil)
(list :retained-content value))
((tp-surface-result-p value)
(let ((owned-p (tp-surface-result-plan-owned-p value)))
(if owned-p
(progn
(unless (and context
(tp--context-active context)
(eq context tp--current-prepare-context)
(eq context (tp-surface-result-owner-context value))
(not (tp-surface-result-consumed-p value)))
(signal 'tp-owned-result-error (list value context)))
(let ((plan (tp-surface-result-plan value))
(client-state (tp-surface-result-client-state value)))
(setf (tp-surface-result-consumed-p value) t
(tp-surface-result-plan value) nil
(tp-surface-result-client-state value) nil
(tp-surface-result-owner-context value) nil)
(list plan client-state t)))
(list (tp--copy-surface-plan (tp-surface-result-plan value))
(tp-surface-result-client-state value) nil))))
((tp-surface-plan-p value)
(list (tp--copy-surface-plan value)
(if (plist-member options :client-state)
(plist-get options :client-state)
(tp--surface-client-state surface))
nil))
(t (signal 'tp-invalid-surface-plan (list value)))))
(defun tp--context-binding-removals (context objects)
"Return default-lifecycle bindings omitted from CONTEXT among OBJECTS."
(cl-loop for object in objects append
(cl-loop for binding in (tp-binding-owner-bindings object)
unless (or (gethash binding (tp--context-bindings context))
(eq (tp-binding-lifecycle binding) 'retain))
collect binding)))
(defun tp--context-live-objects (surface)
"Return SURFACE's current objects."
(let (objects)
(when (tp--surface-objects surface)
(maphash (lambda (_path object) (push object objects))
(tp--surface-objects surface)))
objects))
(defun tp--object-set-difference (left right)
"Return objects in LEFT that are not `eq' to an object in RIGHT."
(cl-remove-if (lambda (object) (memq object right)) left))
(defun tp--plan-key-positions (plan)
"Return an alist from PLAN keyed paths to sibling positions."
(let (positions)
(cl-labels
((walk (node path)
(cl-loop for child in (tp-surface-plan-children node)
for position from 0
for key = (tp-surface-plan-key child)
for child-path = (and key (append path (list key)))
when key do (push (cons child-path position) positions)
do (walk child
(or child-path
(append path
(list (tp--plan-segment
child position))))))))
(walk plan (list (tp--plan-segment plan 0))))
positions))
(defun tp--plan-moved-count (old new)
"Return the number of keyed objects moved between OLD and NEW plans."
(if (null old)
0
(let ((old-positions (tp--plan-key-positions old)))
(cl-loop for (path . position) in (tp--plan-key-positions new)
for old-position = (alist-get path old-positions nil nil #'equal)
count (and old-position (/= old-position position))))))
(defun tp--candidate-object-list (context)
"Return CONTEXT's touched and retained candidate objects."
(let (objects)
(maphash
(lambda (_path object)
(when (or (gethash object (tp--context-touched context))
(gethash object (tp--context-retained context)))
(push object objects)))
(tp--context-objects context))
objects))
(defun tp--candidate-reconciled-count (context created)
"Return the number of touched retained objects in CONTEXT excluding CREATED."
(- (hash-table-count (tp--context-touched context))
(length created)))
(defun tp--content-mount-spec (object record &optional tags tags-owned-p)
"Return one content mount spec for OBJECT over RECORD with TAGS.
TAGS-OWNED-P skips a second snapshot when TAGS already belongs to the candidate."
(list :object object :start (plist-get record :start)
:end (plist-get record :end)
:tags (if tags-owned-p
tags
(tp--copy-property-value
(if tags tags (plist-get record :tags))))))
(defun tp--content-mount-specs (records context)
"Return direct and logical range mount specs for RECORDS in CONTEXT."
(let (specs)
(dolist (record records)
(let* ((leaf (plist-get record :object))
(fragment-attachments
(nreverse
(copy-sequence
(gethash leaf
(tp--context-fragment-attachments context)))))
(range-attachments
(nreverse
(copy-sequence
(gethash leaf
(tp--context-content-range-attachments context))))))
(push (tp--content-mount-spec leaf record) specs)
(dolist (attachment fragment-attachments)
(push (tp--content-mount-spec
(car attachment) record (cdr attachment) t)
specs))
(when range-attachments
(unless (plist-get record :leaf)
(signal 'tp-invalid-content-range
(list :leaf leaf :reason 'not-a-text-leaf)))
(let ((leaf-start (plist-get record :start))
(leaf-length (- (plist-get record :end)
(plist-get record :start))))
(dolist (attachment range-attachments)
(let ((start (plist-get attachment :start))
(end (plist-get attachment :end)))
(unless (and (<= end leaf-length)
(<= start end))
(signal 'tp-invalid-content-range
(list :start start :end end
:leaf-length leaf-length)))
(push (list :object (plist-get attachment :object)
:start (+ leaf-start start)
:end (+ leaf-start end)
:tags (plist-get attachment :tags))
specs)))))))
(nreverse specs)))
(defun tp--plan-record-mount-specs (records capability context)
"Build candidate mount specs from RECORDS for CAPABILITY in CONTEXT."
(if (eq capability 'content)
(tp--content-mount-specs records context)
(tp--properties-mount-specs records context)))
(defun tp--ranges-overlap-p (left-start left-end right-start right-end)
"Return non-nil when LEFT-START..LEFT-END overlaps RIGHT-START..RIGHT-END."
(and (< left-start left-end) (< right-start right-end)
(< left-start right-end) (< right-start left-end)))
(defun tp--candidate-ranges (surface mount-specs rendered)
"Return SURFACE ranges described by MOUNT-SPECS and RENDERED."
(if (eq (tp--surface-capability surface) 'content)
(let ((start (marker-position (tp--surface-start surface))))
(list (cons start (+ start (length rendered)))))
(mapcar (lambda (spec)
(cons (plist-get spec :start) (plist-get spec :end)))
mount-specs)))
(defun tp--validate-cross-surface-ranges (surface mount-specs rendered)
"Reject MOUNT-SPECS and RENDERED when SURFACE overlaps another surface."
(let ((ranges (tp--candidate-ranges surface mount-specs rendered)))
(with-current-buffer (tp--surface-buffer surface)
(dolist (other tp--buffer-surfaces)
(unless (eq other surface)
(dolist (mount (tp--surface-mounts other))
(let ((start (tp--mount-position (tp--surface-mount-start mount)))
(end (tp--mount-position (tp--surface-mount-end mount))))
(when (cl-some (lambda (range)
(tp--ranges-overlap-p
(car range) (cdr range) start end))
ranges)
(signal 'tp-capability-error
(list :cross-surface-overlap
(tp--surface-id other)))))))))))
(defun tp--prepare-input (surface input options context)
"Run INPUT for SURFACE in CONTEXT and normalize its result."
(let ((tp--current-prepare-context context)
(tp--binding-touch-function
(lambda (binding) (tp--touch-context-binding context binding))))
(if (functionp input)
(tp--producer-result (funcall input context) surface options context)
(let ((plan (tp--copy-surface-plan input)))
(tp--ensure-plan-objects context plan)
(list plan
(if (plist-member options :client-state)
(plist-get options :client-state)
(tp--surface-client-state surface)))))))
(defun tp--call-with-prepare-buffer-guard (surface function)
"Call FUNCTION while rejecting producer edits to SURFACE's buffer."
(let* ((buffer (tp--surface-buffer surface))
(before-tick (with-current-buffer buffer
(buffer-modified-tick)))
(group (tp--prepare-change-group-for-buffers (list buffer)))
(tp--surface-guarding-prepare t))
(unwind-protect
(let ((result (funcall function)))
(unless (= before-tick
(with-current-buffer buffer
(buffer-modified-tick)))
(signal 'tp-producer-buffer-mutation
(list (tp--surface-id surface))))
result)
(tp--cancel-change-group-safely group))))
(defun tp--normalize-surface-scopes (surface objects)
"Return validated retained OBJECTS owned by SURFACE."
(unless (and (proper-list-p objects) objects)
(signal 'wrong-type-argument (list 'non-empty-proper-list-p objects)))
(setq objects
(cl-delete-duplicates (copy-sequence objects) :test #'eq))
(dolist (object objects)
(unless (tp-object-p object)
(signal 'wrong-type-argument (list 'tp-object-p object)))
(unless (tp-object-live-p object)
(signal 'tp-stale-object (list object)))
(unless (eq (tp--surface-object-surface object) surface)
(signal 'tp-cross-surface-object (list object surface))))
objects)
(defun tp--coalesce-ranges (ranges)
"Return sorted, merged nonempty RANGES."
(setq ranges
(sort (cl-remove-if (lambda (range) (>= (car range) (cdr range)))
(mapcar (lambda (range)
(cons (car range) (cdr range)))
ranges))
(lambda (left right) (< (car left) (car right)))))
(let (merged)
(dolist (range ranges (nreverse merged))
(if (and merged (<= (car range) (cdar merged)))
(setcdr (car merged) (max (cdar merged) (cdr range)))
(push range merged)))))
(defun tp--subtract-ranges (ranges exclusions)
"Return RANGES with EXCLUSIONS removed.
Both arguments contain numeric half-open relative intervals."
(let (result)
(dolist (range ranges (nreverse result))
(let ((cursor (car range))
(end (cdr range)))
(dolist (exclusion exclusions)
(when (and (< cursor end) (> (cdr exclusion) cursor))
(when (< cursor (car exclusion))
(push (cons cursor (min end (car exclusion))) result))
(setq cursor (max cursor (cdr exclusion)))))
(when (< cursor end)
(push (cons cursor end) result))))))
(defun tp--live-scope-ranges (surface objects &optional relative)
"Return live mount ranges for OBJECTS on SURFACE.
When RELATIVE is non-nil, return offsets from the surface start."
(let ((base (if relative
(marker-position (tp--surface-start surface))
0))
ranges)
(dolist (object objects)
(dolist (mount (gethash object (tp--surface-mount-index surface)))
(let ((start (tp--mount-position (tp--surface-mount-start mount)))
(end (tp--mount-position (tp--surface-mount-end mount))))
(unless (and start end)
(signal 'tp-stale-mount (list object)))
(push (cons (- start base) (- end base)) ranges))))
(tp--coalesce-ranges ranges)))
(defun tp--candidate-scope-ranges (mount-specs objects)
"Return relative candidate MOUNT-SPECS ranges owned by OBJECTS."
(let ((scope-set (make-hash-table :test #'eq)) ranges)
(dolist (object objects) (puthash object t scope-set))
(dolist (spec mount-specs)
(when (gethash (plist-get spec :object) scope-set)
(push (cons (plist-get spec :start) (plist-get spec :end)) ranges)))
(tp--coalesce-ranges ranges)))
(defun tp--surface-content-string (surface)
"Return SURFACE's current propertized content string."
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(with-current-buffer (tp--surface-buffer surface)
(save-restriction
(widen)
(buffer-substring start end)))))
(defun tp--complement-ranges (length ranges)
"Return the nonempty complement of RANGES inside zero to LENGTH."
(let ((cursor 0) complement)
(dolist (range ranges)
(when (< cursor (car range))
(push (cons cursor (car range)) complement))
(setq cursor (cdr range)))
(when (< cursor length)
(push (cons cursor length) complement))
(nreverse complement)))
(defun tp--substring-ranges (string ranges)
"Return STRING content from RANGES concatenated with properties."
(apply #'concat
(mapcar (lambda (range)
(substring string (car range) (cdr range)))
ranges)))
(defun tp--pair-outside-ranges (old-ranges new-ranges)
"Pair equal-length pieces of OLD-RANGES and NEW-RANGES in order."
(let ((old-ranges (copy-tree old-ranges))
(new-ranges (copy-tree new-ranges))
pairs)
(while (and old-ranges new-ranges)
(let* ((old (car old-ranges))
(new (car new-ranges))
(length (min (- (cdr old) (car old))
(- (cdr new) (car new)))))
(push (list (car old) (+ (car old) length)
(car new) (+ (car new) length))
pairs)
(setcar old-ranges (cons (+ (car old) length) (cdr old)))
(setcar new-ranges (cons (+ (car new) length) (cdr new)))
(when (= (caar old-ranges) (cdar old-ranges)) (pop old-ranges))
(when (= (caar new-ranges) (cdar new-ranges)) (pop new-ranges))))
(nreverse pairs)))
(defun tp--text-properties-canonical-at (text position)
"Return TEXT properties at POSITION in deterministic key order."
(sort
(cl-loop for (property value) on (text-properties-at position text)
by #'cddr
collect (cons property value))
(lambda (left right)
(string< (symbol-name (car left))
(symbol-name (car right))))))
(defun tp--text-property-semantic-equal-p (left right)
"Return non-nil when propertized strings LEFT and RIGHT are policy-equal."
(when (and (stringp left)
(stringp right)
(= (length left) (length right)))
;; Compare characters once. The property loop below visits only
;; property-boundary positions, avoiding a substring/all-runs allocation
;; on every scoped update.
(and (equal (substring-no-properties left)
(substring-no-properties right))
(let ((position 0)
(limit (length left))
equal-p)
(setq equal-p t)
(while (and equal-p (< position limit))
(unless (equal-including-properties
(tp--text-properties-canonical-at left position)
(tp--text-properties-canonical-at right position))
(setq equal-p nil))
(setq position
(min (or (next-property-change position left limit) limit)
(or (next-property-change position right limit) limit))))
equal-p))))
(defun tp--layout-separator-only-p (text)
"Return non-nil when TEXT contains only layout line separators."
(and (stringp text)
(cl-every (lambda (char) (= char ?\n))
(substring-no-properties text))))
(defun tp--scope-outside-separator-equivalent-p (left right)
"Return non-nil when scoped outside gaps LEFT and RIGHT are separators.
Logical Ebox owners may absorb a newline between two owned rendered runs when
one candidate line grows or shrinks. Treating that delimiter as part of the
owner preserves scoped publication while still rejecting every non-separator
outside change."
(and (tp--layout-separator-only-p left)
(tp--layout-separator-only-p right)))
(defun tp--scope-patches-between-anchors (old new anchors)
"Return patches between equal outside ANCHORS in OLD and NEW."
(let ((old-position 0) (new-position 0) patches)
(dolist (anchor (append anchors
(list (list (length old) (length old)
(length new) (length new)))))
(let* ((old-end (nth 0 anchor))
(new-end (nth 2 anchor))
(replacement (substring new new-position new-end)))
(unless (tp--text-property-semantic-equal-p
(substring old old-position old-end) replacement)
(push (list :old-start old-position :old-end old-end
:new-start new-position :new-end new-end
:replacement replacement)
patches)))
(setq old-position (nth 1 anchor)
new-position (nth 3 anchor)))
(nreverse patches)))
(defun tp--scope-replacement-analysis (old new old-ranges new-ranges)
"Compare OLD-RANGES and NEW-RANGES from OLD to NEW.
Return scoped replacement metadata, or nil on mismatch."
(let* ((old-outside (tp--complement-ranges (length old) old-ranges))
(new-outside (tp--complement-ranges (length new) new-ranges)))
(let ((old-gap (tp--substring-ranges old old-outside))
(new-gap (tp--substring-ranges new new-outside)))
(when (or (tp--text-property-semantic-equal-p old-gap new-gap)
(tp--scope-outside-separator-equivalent-p old-gap new-gap))
(list :patches
(tp--scope-patches-between-anchors
old new (tp--pair-outside-ranges old-outside new-outside)))))))
(defun tp--prepare-content-scope
(surface rendered mount-specs objects options)
"Return scoped publication metadata for RENDERED on SURFACE.
MOUNT-SPECS describe the candidate OBJECTS. OPTIONS accepts
`:on-mismatch' as `error' or `root'."
(let* ((old (tp--surface-content-string surface))
(old-ranges (tp--live-scope-ranges surface objects t))
(new-ranges (tp--candidate-scope-ranges mount-specs objects))
(analysis
(tp--scope-replacement-analysis
old rendered old-ranges new-ranges)))
(cond
(analysis (list :patches (plist-get analysis :patches) :fallback nil))
((eq (plist-get options :on-mismatch) 'root)
(list :patches nil :fallback t))
(t
(signal 'tp-scope-mismatch
(list :surface (tp--surface-id surface)
:old-ranges old-ranges :new-ranges new-ranges))))))
(defun tp--range-contained-in-p (start end ranges)
"Return non-nil when START..END is inside one of RANGES."
(cl-some (lambda (range)
(and (<= (car range) start) (<= end (cdr range))))
ranges))
(defun tp--prepare-properties-scope
(surface mount-specs operations objects options)
"Validate scoped property OPERATIONS for OBJECTS on SURFACE.
MOUNT-SPECS describe the candidate object ranges and OPTIONS controls mismatch."
(let* ((old-ranges (tp--live-scope-ranges surface objects))
(new-ranges (tp--candidate-scope-ranges mount-specs objects))
(ranges (tp--coalesce-ranges
(append old-ranges new-ranges)))
(outside
(cl-find-if
(lambda (operation)
(not (tp--range-contained-in-p
(plist-get operation :start) (plist-get operation :end)
ranges)))
operations)))
(cond
((not outside) (list :patches ranges :fallback nil))
((eq (plist-get options :on-mismatch) 'root)
(list :patches nil :fallback t))
(t
(signal 'tp-scope-mismatch
(list :surface (tp--surface-id surface)
:operation outside :ranges ranges))))))
(defun tp--prepare-retained-content
(surface candidate context scope-objects scope-options initial)
"Prepare CANDIDATE for SURFACE in CONTEXT without traversing its plan.
SCOPE-OBJECTS and SCOPE-OPTIONS constrain publication; INITIAL must be nil.
The candidate is valid only when its plan has the same three-level content
surface topology as the committed plan. Its text and content attachments are
still validated and published through the ordinary TP transaction phases."
(when initial
(signal 'tp-invalid-surface-plan
(list :retained-content-initial t)))
(unless (eq (tp--surface-capability surface) 'content)
(signal 'tp-capability-error (list :retained-content 'properties)))
(let* ((plan (tp-retained-content-candidate-plan candidate))
(rendered
(tp--compose-relative-property-contributions
(tp-retained-content-candidate-rendered candidate)
(tp-retained-content-candidate-property-contributions candidate)))
(ranges (tp-retained-content-candidate-ranges candidate))
(objects (tp--context-objects context))
(root-path (list (tp--plan-segment plan 0)))
(fragment-plan (car (tp-surface-plan-children plan)))
(fragment-path (append root-path
(list (tp--plan-segment fragment-plan 0))))
(text-plan (car (tp-surface-plan-children fragment-plan)))
(text-path (append fragment-path
(list (tp--plan-segment text-plan 0))))
(surface-root (gethash root-path objects))
(fragment-root (gethash fragment-path objects))
(text-leaf (gethash text-path objects)))
(unless (and surface-root fragment-root text-leaf
(null (tp-surface-plan-props plan))
(null (tp-surface-plan-props fragment-plan))
(null (tp-surface-plan-props text-plan))
(null (tp-surface-plan-children text-plan))
(equal (tp-surface-plan-key plan)
(tp-surface-plan-key (tp--surface-plan surface))))
(signal 'tp-invalid-surface-plan
(list :retained-content-topology plan)))
(tp--validate-plan-capability plan (tp--surface-capability surface))
;; `ebox-surface--owned-ranges' transfers freshly allocated range plists
;; for this candidate. Install that list directly after validating the
;; same invariants as the public attach operation; copying it again here
;; would recreate the hot-path allocation we are removing.
(when (and ranges
(null (gethash text-leaf
(tp--context-content-range-attachments context))))
(tp--validate-context-object context text-leaf)
(dolist (range ranges)
(let ((object (plist-get range :object))
(start (plist-get range :start))
(end (plist-get range :end)))
(tp--validate-context-object context object)
(tp--validate-content-range start end)
(unless (<= end (length rendered))
(signal 'tp-invalid-content-range
(list :start start :end end
:leaf-length (length rendered))))))
(puthash text-leaf ranges
(tp--context-content-range-attachments context)))
(let* ((length (length rendered))
(records (list (list :object text-leaf :start 0 :end length
:props nil :tags nil :leaf t)
(list :object fragment-root :start 0 :end length
:props nil :tags nil :leaf nil)
(list :object surface-root :start 0 :end length
:props nil :tags nil :leaf nil)))
(mount-specs (tp--content-mount-specs records context))
(_ranges (tp--validate-cross-surface-ranges
surface mount-specs rendered))
(full-surface-p
(tp-retained-content-candidate-full-surface-p candidate))
(scope-analysis
(cond
;; A root Ebox scroll owner is the only retained-content caller
;; allowed to widen this proof. Its semantic root owns every
;; rendered line, including separator characters that are not
;; tagged by a child mount, so the full surface replacement is
;; the exact scope rather than a fallback.
((and full-surface-p scope-objects)
(let ((old-length (length (tp--surface-content-string surface))))
(list :patches
(list (list :old-start 0 :old-end old-length
:new-start 0 :new-end (length rendered)
:replacement rendered))
:fallback nil)))
(scope-objects
(tp--prepare-content-scope
surface rendered mount-specs scope-objects scope-options))))
(scope-patches (plist-get scope-analysis :patches))
(retained-content-p
(and full-surface-p
scope-analysis
(not (plist-get scope-analysis :fallback))))
(old-objects (tp--context-live-objects surface))
(candidate-objects (tp--candidate-object-list context)))
(unless (equal (sort (copy-sequence old-objects)
(lambda (left right)
(< (tp--surface-object-id left)
(tp--surface-object-id right))))
(sort (copy-sequence candidate-objects)
(lambda (left right)
(< (tp--surface-object-id left)
(tp--surface-object-id right)))))
(signal 'tp-orphan-object (list :retained-content-object-set)))
(tp--make-prepared-surface
:surface surface :context context :plan plan :rendered rendered
:mount-specs mount-specs :objects candidate-objects
:client-state (tp-retained-content-candidate-client-state candidate)
:producer candidate :initial nil :created nil :removed nil :moved 0
:reconciled 0 :scope-objects scope-objects
:scope-patches scope-patches
:scope-fallback (plist-get scope-analysis :fallback)
:retained-content-p retained-content-p))))
(defun tp--commit-batch-rebase-position (patches position end-p)
"Map old POSITION through ordered coordinate PATCHES.
END-P selects the right boundary when POSITION lies inside a replacement."
(let ((delta 0) result)
(while (and patches (null result))
(let* ((patch (car patches))
(old-start (plist-get patch :old-start))
(old-end (plist-get patch :old-end))
(new-start (plist-get patch :new-start))
(new-end (plist-get patch :new-end)))
(cond
((< position old-start) (setq result (+ position delta)))
((= position old-start) (setq result new-start))
((< position old-end) (setq result (if end-p new-end new-start)))
((= position old-end) (setq result new-end))
(t (setq delta (- new-end old-end)
patches (cdr patches))))))
(or result (+ position delta))))
(defun tp--commit-batch-mount-specs (surface batch)
"Return SURFACE coordinate mounts rebased through BATCH."
(let ((base (marker-position (tp--surface-start surface))))
(mapcar
(lambda (mount)
(let ((start (- (tp--mount-position
(tp--surface-mount-start mount)) base))
(end (- (tp--mount-position
(tp--surface-mount-end mount)) base))
(patches (tp-commit-batch-coordinate-patches batch)))
(list :object (tp--surface-mount-object mount)
:start (tp--commit-batch-rebase-position patches start nil)
:end (tp--commit-batch-rebase-position patches end t)
:tags (copy-tree (tp--surface-mount-tags mount)))))
(tp--surface-mounts surface))))
(defun tp--commit-batch-retained-mount-state
(surface mount-specs context target-extent)
"Return exact coordinate updates when SURFACE can retain MOUNT-SPECS.
CONTEXT authenticates candidate objects and TARGET-EXTENT bounds coordinates.
The returned cons distinguishes an exact empty update set from a proof miss."
(let ((base (marker-position (tp--surface-start surface)))
(mounts (tp--surface-mounts surface))
(specs mount-specs)
(retainable
(and (plist-get (tp--surface-options surface) :coordinate-mounts) t))
(updates
(and (plist-get (tp--surface-options surface) :coordinate-mounts)
(make-vector (* 3 (length mount-specs)) nil)))
(update-index 0))
(while specs
(let* ((spec (pop specs))
(mount (and mounts (pop mounts)))
(object (plist-get spec :object))
(start (plist-get spec :start))
(end (plist-get spec :end)))
(unless (and (tp-object-p object)
(or (gethash object (tp--context-touched context))
(gethash object (tp--context-retained context)))
(integerp start) (integerp end)
(<= 0 start end target-extent))
(signal 'tp-surface-error
(list :commit-mount-range start end target-extent)))
(if (and retainable mount
(integerp (tp--surface-mount-start mount))
(integerp (tp--surface-mount-end mount))
(eq (tp--surface-mount-object mount) object)
(eq (tp--surface-mount-tags mount)
(plist-get spec :tags)))
(progn
(aset updates update-index mount)
(aset updates (1+ update-index) (+ base start))
(aset updates (+ update-index 2) (+ base end))
(setq update-index (+ update-index 3)))
(setq retainable nil
updates nil))))
(when mounts (setq retainable nil))
(and retainable (cons t updates))))
(defun tp--commit-batch-stable-object-set-p (surface objects)
"Return non-nil when OBJECTS are exactly SURFACE's retained identities."
(let ((live (tp--surface-objects surface)))
(and (hash-table-p live)
(= (hash-table-count live) (length objects))
(cl-every
(lambda (object)
(eq (gethash (tp--surface-object-path object) live) object))
objects))))
(defun tp--commit-batch-coordinate-identity-p (batch)
"Return non-nil when BATCH cannot move any retained coordinate."
(and (= (tp-commit-batch-base-extent batch)
(tp-commit-batch-target-extent batch))
(cl-every
(lambda (patch)
(and (= (plist-get patch :old-start)
(plist-get patch :new-start))
(= (plist-get patch :old-end)
(plist-get patch :new-end))))
(tp-commit-batch-coordinate-patches batch))))
(defun tp--prepare-commit-batch
(surface candidate context scope-objects input initial)
"Prepare strict equal-coordinate CANDIDATE for stable SURFACE topology."
(when initial
(signal 'tp-invalid-surface-plan (list :commit-batch-initial t)))
(unless (and (eq (tp--surface-capability surface) 'content)
(plist-get (tp--surface-options surface) :coordinate-mounts))
(signal 'tp-capability-error (list :commit-batch-coordinate-surface)))
(let* ((batch (tp-commit-batch-candidate-batch candidate))
(extent (- (marker-position (tp--surface-end surface))
(marker-position (tp--surface-start surface))))
(objects (tp--candidate-object-list context))
(reuse-mount-projection-p
(tp-commit-batch-candidate-reuse-mount-projection-p candidate))
(coordinate-identity-p
(and (not reuse-mount-projection-p)
(not (tp-commit-batch-candidate-exact-mount-specs-p candidate))
(tp--commit-batch-coordinate-identity-p batch)))
(mount-specs
(cond
((tp-commit-batch-candidate-exact-mount-specs-p candidate)
(tp-commit-batch-candidate-mount-specs candidate))
(reuse-mount-projection-p nil)
(coordinate-identity-p nil)
(t (tp--commit-batch-mount-specs surface batch))))
(retained-mount-state
(if (or reuse-mount-projection-p coordinate-identity-p)
(cons t [])
(tp--commit-batch-retained-mount-state
surface mount-specs context
(tp-commit-batch-target-extent batch)))))
(unless (and (= (tp-commit-batch-base-revision batch)
(tp--surface-revision surface))
(= (tp-commit-batch-target-revision batch)
(1+ (tp--surface-revision surface)))
(= extent (tp-commit-batch-base-extent batch))
(or (not reuse-mount-projection-p)
(= (tp-commit-batch-base-extent batch)
(tp-commit-batch-target-extent batch))))
(signal 'tp-surface-error (list :unsupported-commit-batch batch)))
(unless (tp--commit-batch-stable-object-set-p surface objects)
(signal 'tp-orphan-object (list :commit-batch-object-set)))
(tp--make-prepared-surface
:surface surface :context context :plan (tp--surface-plan surface)
:rendered nil :mount-specs mount-specs
:objects objects
:client-state
(if (tp-commit-batch-candidate-exact-client-state-p candidate)
(tp-commit-batch-candidate-client-state candidate)
(tp-commit-batch-client-state batch))
:producer input :initial nil :created nil :removed nil :moved 0
:reconciled 0 :scope-objects scope-objects
:scope-patches (tp-commit-batch-patches batch)
:scope-fallback nil :commit-batch batch
:retained-mount-state-p (and retained-mount-state t)
:mount-coordinate-updates (cdr retained-mount-state))))
(defun tp--prepare-surface (surface input options initial)
"Prepare INPUT for SURFACE without publishing it.
OPTIONS configure the mount and INITIAL is non-nil for first publication."
(tp--validate-surface-buffer surface initial)
(when (and (not initial) (tp--surface-stale surface))
(signal 'tp-stale-mount (list (tp--surface-id surface))))
(let* ((scope-request (tp--surface-scope-request surface))
(context (tp--make-context surface))
(scope-objects
(and scope-request
(tp--normalize-surface-scopes
surface (plist-get scope-request :objects))))
(scope-options (plist-get scope-request :options))
(success nil)
result)
(unwind-protect
(let ((normalized
(tp--call-with-prepare-buffer-guard
surface
(lambda ()
(tp--prepare-input surface input options context)))))
(if (memq (car normalized) '(:retained-content :commit-batch))
(setq result
(if (eq (car normalized) :retained-content)
(tp--prepare-retained-content
surface (cadr normalized) context scope-objects
scope-options initial)
(tp--prepare-commit-batch
surface (cadr normalized) context scope-objects
input initial)))
(let* ((plan (nth 0 normalized))
(client-state (nth 1 normalized))
(owned-p (nth 2 normalized))
(_capability (tp--validate-plan-capability
plan (tp--surface-capability surface)))
(_tree (tp--validate-context-tree context plan))
(render-result (tp--render-plan plan context owned-p))
(rendered (car render-result))
(records (cdr render-result))
(objects (tp--candidate-object-list context))
(old-objects (tp--context-live-objects surface))
(created (tp--object-set-difference objects old-objects))
(removed (tp--object-set-difference old-objects objects))
(mount-specs (tp--plan-record-mount-specs
records (tp--surface-capability surface)
context))
(_ranges (tp--validate-cross-surface-ranges
surface mount-specs rendered))
(property-result
(when (eq (tp--surface-capability surface) 'properties)
(tp--prepare-property-ledger surface mount-specs)))
(scope-analysis
(when scope-objects
(if (eq (tp--surface-capability surface) 'content)
(tp--prepare-content-scope
surface rendered mount-specs scope-objects
scope-options)
(tp--prepare-properties-scope
surface mount-specs (cdr property-result)
scope-objects scope-options)))))
(setq result
(tp--make-prepared-surface
:surface surface :context context :plan plan
:rendered rendered :mount-specs mount-specs
:ledger-specs (car property-result)
:property-operations (cdr property-result)
:objects objects :client-state client-state
:producer input :initial initial :created created
:removed removed
:moved (tp--plan-moved-count
(tp--surface-plan surface) plan)
:reconciled
(tp--candidate-reconciled-count context created)
:scope-objects scope-objects
:scope-patches (plist-get scope-analysis :patches)
:scope-fallback
(plist-get scope-analysis :fallback)))))
(setq success t)
result)
(unless success (tp--discard-context context)))))
(cl-defun tp-range-anchor-create
(buffer start end &key (start-insertion-type nil) (end-insertion-type t)
(boundary-policy 'stale))
"Create an opaque marker-backed range in BUFFER from START to END.
START-INSERTION-TYPE and END-INSERTION-TYPE control marker movement.
BOUNDARY-POLICY is `stale', `shorten', or `remove'."
(tp--validate-buffer-range buffer start end)
(unless (memq boundary-policy '(stale shorten remove))
(signal 'tp-invalid-range-anchor (list boundary-policy)))
(let ((anchor (tp--make-range-anchor
:id (cl-incf tp--anchor-id-counter) :buffer buffer
:start (copy-marker start start-insertion-type)
:end (copy-marker end end-insertion-type)
:boundary-policy boundary-policy :live t
:candidate-context tp--current-prepare-context)))
(when tp--current-prepare-context
(push anchor (tp--context-created-anchors tp--current-prepare-context)))
anchor))
(defun tp-range-anchor-live-p (anchor)
"Return non-nil when ANCHOR still has live markers."
(and (tp-range-anchor-p anchor) (tp--anchor-live anchor)
(buffer-live-p (tp--anchor-buffer anchor))
(marker-position (tp--anchor-start anchor))
(marker-position (tp--anchor-end anchor))))
(defun tp--validate-anchor (anchor surface)
"Validate ANCHOR for SURFACE."
(unless (tp-range-anchor-live-p anchor)
(signal 'tp-invalid-range-anchor (list anchor)))
(unless (eq (tp--anchor-buffer anchor) (tp--surface-buffer surface))
(signal 'tp-cross-surface-object (list anchor surface)))
(when (and (tp--anchor-surfaces anchor)
(not (memq surface (tp--anchor-surfaces anchor))))
(signal 'tp-cross-surface-object (list anchor surface)))
(when (tp--anchor-stale anchor)
(signal 'tp-stale-mount (list (tp--anchor-id anchor)))))
(defun tp-object-attach-range (context object anchor)
"Attach OBJECT to marker-backed ANCHOR in candidate CONTEXT."
(tp--validate-context-object context object)
(tp--validate-anchor anchor (tp--context-surface context))
(when (gethash object (tp--context-attachments context))
(signal 'tp-invalid-range-anchor (list :duplicate object)))
(puthash object anchor (tp--context-attachments context))
object)
(defun tp--properties-mount-specs (records context)
"Return properties mount specs from RECORDS and CONTEXT attachments."
(let (specs)
(dolist (record records)
(let* ((object (plist-get record :object))
(anchor (gethash object (tp--context-attachments context))))
(when anchor
(push (list :object object :anchor anchor
:start (marker-position (tp--anchor-start anchor))
:end (marker-position (tp--anchor-end anchor))
:props (plist-get record :props)
:tags (tp--copy-property-value
(plist-get record :tags)))
specs))))
(maphash
(lambda (object _anchor)
(unless (cl-find object records :key (lambda (item)
(plist-get item :object))
:test #'eq)
(signal 'tp-orphan-object (list object))))
(tp--context-attachments context))
(nreverse specs)))
(defun tp--property-state-at (buffer position property)
"Return direct PROPERTY presence and value at POSITION in BUFFER."
(with-current-buffer buffer
(let ((cell (plist-member (text-properties-at position buffer) property)))
(cons (and cell t) (and cell (cadr cell))))))
(defun tp--property-state-equal-p (left right)
"Return non-nil when property states LEFT and RIGHT are equal."
(and (eq (car left) (car right))
(equal (cdr left) (cdr right))))
(defun tp--property-state-policy-equal-p (property left right)
"Return non-nil when PROPERTY states LEFT and RIGHT are policy-equal."
(and (eq (car left) (car right))
(or (not (car left))
(tp--property-value-equal-p property (cdr left) (cdr right)))))
(defun tp--ledger-position (marker)
"Return live MARKER position or signal a stale-mount error."
(or (marker-position marker)
(signal 'tp-stale-mount (list marker))))
(defun tp--old-ledger-at (surface position property)
"Return SURFACE ledger entry covering POSITION for PROPERTY."
(cl-find-if
(lambda (entry)
(and (eq property (tp--property-ledger-property entry))
(<= (tp--ledger-position (tp--property-ledger-start entry)) position)
(< position (tp--ledger-position (tp--property-ledger-end entry)))))
(tp--surface-ledger surface)))
(defun tp--contribution-properties (mount-specs surface)
"Return all property names in MOUNT-SPECS and SURFACE's old ledger."
(let (properties)
(dolist (spec mount-specs)
(cl-loop for (property _value) on (plist-get spec :props) by #'cddr
do (cl-pushnew property properties :test #'eq)))
(dolist (entry (tp--surface-ledger surface))
(cl-pushnew (tp--property-ledger-property entry) properties :test #'eq))
properties))
(defun tp--property-boundaries (buffer mount-specs surface properties)
"Return sorted interval boundaries in BUFFER.
MOUNT-SPECS and SURFACE provide ranges for PROPERTIES."
(let (boundaries intervals)
(dolist (spec mount-specs)
(let ((start (plist-get spec :start))
(end (plist-get spec :end)))
(push start boundaries)
(push end boundaries)
(push (cons start end) intervals)))
(dolist (entry (tp--surface-ledger surface))
(let ((start (tp--ledger-position (tp--property-ledger-start entry)))
(end (tp--ledger-position (tp--property-ledger-end entry))))
(push start boundaries)
(push end boundaries)
(push (cons start end) intervals)))
(setq boundaries (sort (delete-dups boundaries) #'<))
(with-current-buffer buffer
(dolist (interval (tp--coalesce-ranges intervals))
(dolist (property properties)
(let ((position (car interval))
(maximum (cdr interval)))
(while (< position maximum)
(setq position (next-single-property-change
position property buffer maximum))
(push position boundaries))))))
(sort (delete-dups boundaries) #'<)))
(defun tp--covering-contributions (mount-specs start end property)
"Return ordered MOUNT-SPECS covering START..END and declaring PROPERTY."
(cl-remove-if-not
(lambda (spec)
(and (<= (plist-get spec :start) start)
(>= (plist-get spec :end) end)
(plist-member (plist-get spec :props) property)))
mount-specs))
(defun tp--merge-property-contributions (baseline contributions property)
"Merge PROPERTY CONTRIBUTIONS over BASELINE property state."
(let ((state baseline)
(merge (tp-property-policy-merge
(tp-register-text-property property))))
(dolist (spec contributions)
(let ((value (plist-get (plist-get spec :props) property)))
(setq state
(cons t (if (null value)
nil
(if (car state)
(funcall merge (cdr state) value)
value))))))
state))
(defun tp--ledger-baseline-state (old current)
"Return OLD ledger baseline state, or CURRENT when there is no OLD entry."
(if old
(cons (tp--property-ledger-baseline-present old)
(tp--property-ledger-baseline-value old))
current))
(defun tp--ledger-published-state (entry)
"Return ENTRY's last published property state."
(cons (tp--property-ledger-published-present entry)
(tp--property-ledger-published-value entry)))
(defun tp--property-segment-result
(surface mount-specs start end property)
"Prepare one PROPERTY segment from START to END for SURFACE and MOUNT-SPECS."
(let* ((buffer (tp--surface-buffer surface))
(current (tp--property-state-at buffer start property))
(old (tp--old-ledger-at surface start property))
(contributions
(tp--covering-contributions mount-specs start end property)))
(when (and old (not (tp--property-state-policy-equal-p
property current (tp--ledger-published-state old))))
(signal 'tp-property-conflict
(list (tp--surface-id surface) start end property current)))
(let* ((baseline (tp--ledger-baseline-state old current))
(target (tp--merge-property-contributions
baseline contributions property))
(anchors (delete-dups
(mapcar (lambda (spec) (plist-get spec :anchor))
contributions))))
(list :operation
(unless (tp--property-state-policy-equal-p property current target)
(list :start start :end end :property property
:present (car target) :value (cdr target)))
:ledger
(when contributions
(list :start start :end end :property property
:baseline-present (car baseline)
:baseline-value (cdr baseline)
:published-present (car target)
:published-value (cdr target) :anchors anchors))))))
(defun tp--prepare-property-ledger (surface mount-specs)
"Return ledger specs and property operations for SURFACE and MOUNT-SPECS."
(let* ((buffer (tp--surface-buffer surface))
(properties (tp--contribution-properties mount-specs surface))
(boundaries (tp--property-boundaries
buffer mount-specs surface properties))
ledger operations)
(cl-loop for (start end) on boundaries while end do
(dolist (property properties)
(let ((result (tp--property-segment-result
surface mount-specs start end property)))
(when-let* ((entry (plist-get result :ledger)))
(push entry ledger))
(when-let* ((operation (plist-get result :operation)))
(push operation operations)))))
(cons (nreverse ledger) (nreverse operations))))
(defun tp--validate-live-surface (surface)
"Signal unless SURFACE is live and its buffer exists."
(unless (tp-surface-p surface)
(signal 'wrong-type-argument (list 'tp-surface-p surface)))
(unless (and (tp--surface-live surface)
(buffer-live-p (tp--surface-buffer surface)))
(signal 'tp-dead-surface (list (and (tp-surface-p surface)
(tp--surface-id surface))))))
(defun tp-surface-live-p (surface)
"Return non-nil when SURFACE is mounted in a live buffer."
(and (tp-surface-p surface) (tp--surface-live surface)
(buffer-live-p (tp--surface-buffer surface))))
(defun tp-surface-revision (surface)
"Return SURFACE's committed revision."
(unless (tp-surface-p surface)
(signal 'wrong-type-argument (list 'tp-surface-p surface)))
(tp--surface-revision surface))
(defun tp-surface-client-state (surface)
"Return SURFACE's opaque committed client state."
(tp--validate-live-surface surface)
(tp--surface-client-state surface))
(cl-defun tp-commit-batch-create
(&key base-revision target-revision base-extent target-extent
patches coordinate-patches client-state)
"Create a validated precomputed content commit batch.
BASE-REVISION and TARGET-REVISION bind the transition; BASE-EXTENT and
TARGET-EXTENT bind its coordinates. COORDINATE-PATCHES project retained
mounts, and CLIENT-STATE is opaque owner state.
PATCHES are ordered plists containing :old-start, :old-end, :new-start,
:new-end, and a propertized :replacement string. Optional patch-local
:property-contributions use ordered :start, :end, and :props ranges within
that replacement. Registered merge policies are evaluated now, at batch
construction, and only the final propertized replacement is retained."
(unless (and (integerp base-revision) (>= base-revision 0)
(integerp target-revision)
(= target-revision (1+ base-revision)))
(signal 'tp-surface-error
(list :commit-revision base-revision target-revision)))
(unless (and (integerp base-extent) (>= base-extent 0)
(integerp target-extent) (>= target-extent 0)
(proper-list-p patches) (proper-list-p coordinate-patches))
(signal 'tp-surface-error
(list :commit-extents base-extent target-extent)))
(let ((old-cursor 0) (new-cursor 0) copy coordinate-copy)
(dolist (patch patches)
(let ((old-start (plist-get patch :old-start))
(old-end (plist-get patch :old-end))
(new-start (plist-get patch :new-start))
(new-end (plist-get patch :new-end))
(replacement (plist-get patch :replacement)))
(unless (and (integerp old-start) (integerp old-end)
(integerp new-start) (integerp new-end)
(<= old-cursor old-start old-end base-extent)
(<= new-cursor new-start new-end target-extent)
(= (- old-start old-cursor)
(- new-start new-cursor))
(stringp replacement)
(= (length replacement) (- new-end new-start)))
(signal 'tp-surface-error (list :commit-patch patch)))
(let ((candidate
(if (plist-member patch :property-contributions)
(tp--copy-property-value patch)
(copy-tree patch))))
(when (plist-member patch :property-contributions)
(plist-put candidate :replacement
(tp--compose-relative-property-contributions
(plist-get candidate :replacement)
(plist-get candidate :property-contributions)))
(cl-remf candidate :property-contributions))
(push candidate copy))
(setq old-cursor old-end new-cursor new-end)))
(unless (= (- base-extent old-cursor)
(- target-extent new-cursor))
(signal 'tp-surface-error
(list :commit-tail old-cursor new-cursor)))
(setq old-cursor 0 new-cursor 0)
(dolist (patch coordinate-patches)
(let ((old-start (plist-get patch :old-start))
(old-end (plist-get patch :old-end))
(new-start (plist-get patch :new-start))
(new-end (plist-get patch :new-end)))
(unless (and (integerp old-start) (integerp old-end)
(integerp new-start) (integerp new-end)
(<= old-cursor old-start old-end base-extent)
(<= new-cursor new-start new-end target-extent)
(= (- old-start old-cursor)
(- new-start new-cursor)))
(signal 'tp-surface-error
(list :commit-coordinate-patch patch)))
(push (copy-tree patch) coordinate-copy)
(setq old-cursor old-end new-cursor new-end)))
(unless (= (- base-extent old-cursor)
(- target-extent new-cursor))
(signal 'tp-surface-error
(list :commit-coordinate-tail old-cursor new-cursor)))
(tp--make-commit-batch
:base-revision base-revision :target-revision target-revision
:base-extent base-extent :target-extent target-extent
:patches (nreverse copy)
:coordinate-patches (nreverse coordinate-copy)
:client-state (tp--copy-property-value client-state))))
(cl-defun tp-commit-batch-result-create
(context batch
&key (mount-specs nil mount-specs-p)
(client-state nil client-state-p)
reuse-mount-projection)
"Return one authenticated producer result carrying precomputed BATCH.
CONTEXT must be the active prepare context.
When MOUNT-SPECS is supplied, it is the complete target coordinate projection
for the batch. Each spec contains `:object', `:start', `:end', and optional
`:tags'. When CLIENT-STATE is supplied, it replaces BATCH's defensive state
snapshot. The active prepare context owns and transfers these candidate
values without another materialization. REUSE-MOUNT-PROJECTION asserts that
the producer proved the complete target mount topology and coordinates equal
to the committed projection, as with `tp-object-reuse-subtree'."
(unless (and (tp-prepare-context-p context)
(tp--context-active context)
(eq context tp--current-prepare-context)
(tp-commit-batch-p batch))
(signal 'tp-owned-result-error (list :commit-batch context batch)))
(when (and mount-specs-p reuse-mount-projection)
(signal 'tp-surface-error (list :conflicting-commit-mount-projection)))
(when mount-specs-p
(unless (proper-list-p mount-specs)
(signal 'tp-surface-error (list :commit-mount-specs))))
(tp--make-commit-batch-candidate
:batch batch :context context :mount-specs mount-specs
:exact-mount-specs-p mount-specs-p :client-state client-state
:exact-client-state-p client-state-p
:reuse-mount-projection-p (and reuse-mount-projection t)))
(defun tp--commit-batch-apply-patches (surface batch)
"Apply BATCH's already validated patches to SURFACE."
(let ((buffer (tp--surface-buffer surface))
(base (marker-position (tp--surface-start surface))))
(with-current-buffer buffer
(save-restriction
(widen)
(let ((inhibit-read-only
(plist-get (tp--surface-options surface)
:inhibit-read-only)))
(dolist (patch (reverse (tp-commit-batch-patches batch)))
(let ((start (+ base (plist-get patch :old-start)))
(end (+ base (plist-get patch :old-end)))
(replacement (plist-get patch :replacement)))
(delete-region start end)
(goto-char start)
(insert replacement))))))
(set-marker (tp--surface-start surface) base buffer)
(set-marker (tp--surface-end surface)
(+ base (tp-commit-batch-target-extent batch)) buffer)))
(defun tp--commit-batch-validate-buffer (surface batch)
"Validate BATCH's published extent and changed spans on SURFACE."
(let ((buffer (tp--surface-buffer surface))
(base (marker-position (tp--surface-start surface))))
(let ((actual (- (marker-position (tp--surface-end surface)) base))
(expected (tp-commit-batch-target-extent batch)))
(unless (= actual expected)
(signal 'tp-publication-mismatch
(list :commit-extent :actual actual :expected expected))))
(dolist (patch (tp-commit-batch-patches batch))
(let ((start (+ base (plist-get patch :new-start)))
(end (+ base (plist-get patch :new-end)))
(replacement (plist-get patch :replacement)))
(unless (equal-including-properties
(with-current-buffer buffer (buffer-substring start end))
replacement)
(signal 'tp-publication-mismatch
(list :commit-patch patch)))))))
(defun tp--commit-batch-validate-mounts (surface batch mounts)
"Validate target MOUNTS for BATCH inside SURFACE."
(let ((base (marker-position (tp--surface-start surface)))
(limit (+ (marker-position (tp--surface-start surface))
(tp-commit-batch-target-extent batch))))
(dolist (mount mounts)
(let ((start (tp--mount-position (tp--surface-mount-start mount)))
(end (tp--mount-position (tp--surface-mount-end mount))))
(unless (and start end (<= base start end limit))
(signal 'tp-publication-mismatch
(list :commit-mount mount)))))))
;;;###autoload
(defun tp-surface-commit-batch (surface batch)
"Atomically apply precomputed BATCH to stable content SURFACE."
(tp--validate-live-surface surface)
(unless (and (tp-commit-batch-p batch)
(eq (tp--surface-capability surface) 'content)
(= (tp--surface-revision surface)
(tp-commit-batch-base-revision batch))
(= (- (marker-position (tp--surface-end surface))
(marker-position (tp--surface-start surface)))
(tp-commit-batch-base-extent batch)))
(signal 'tp-surface-error (list :stale-commit-batch surface batch)))
(let* ((buffer (tp--surface-buffer surface))
(old-revision (tp--surface-revision surface))
(old-client-state (tp--surface-client-state surface))
(old-report (tp--surface-report surface))
(views (tp--capture-view-state (list buffer)))
(group (tp--prepare-change-group-for-buffers (list buffer)))
success)
(unwind-protect
(progn
(tp--commit-batch-apply-patches surface batch)
(tp--commit-batch-validate-buffer surface batch)
(tp--commit-batch-validate-mounts
surface batch (tp--surface-mounts surface))
(setf (tp--surface-client-state surface)
(tp-commit-batch-client-state batch)
(tp--surface-revision surface)
(tp-commit-batch-target-revision batch)
(tp--surface-report surface)
(list :transaction-id (cl-incf tp--surface-transaction-id)
:surface-id (tp--surface-id surface)
:old-revision old-revision
:new-revision (tp-commit-batch-target-revision batch)
:text-operations (length (tp-commit-batch-patches batch))
:property-operations 0 :commit-batch t
:rolled-back nil :failure nil))
(accept-change-group group)
(setq success t)
(tp-surface-report surface))
(unless success
(tp--cancel-change-group-safely group)
(setf (tp--surface-client-state surface) old-client-state
(tp--surface-revision surface) old-revision
(tp--surface-report surface) old-report))
(tp--restore-view-state views))))
(defun tp--validate-surface-buffer (surface initial)
"Validate SURFACE's buffer before prepare; INITIAL permits a candidate."
(let ((buffer (tp--surface-buffer surface)))
(unless (buffer-live-p buffer)
(signal 'tp-dead-surface (list (tp--surface-id surface))))
(when (buffer-base-buffer buffer)
(signal 'tp-unsupported-buffer (list :indirect buffer)))
(unless (or initial (tp--surface-live surface))
(signal 'tp-dead-surface (list (tp--surface-id surface))))
(with-current-buffer buffer
(when (and buffer-read-only
(not (plist-get (tp--surface-options surface)
:inhibit-read-only)))
(signal 'buffer-read-only (list buffer))))))
(defun tp--surface-range (surface)
"Return SURFACE's live start and end positions."
(let ((start (marker-position (tp--surface-start surface)))
(end (marker-position (tp--surface-end surface))))
(unless (and start end)
(signal 'tp-stale-mount (list (tp--surface-id surface))))
(cons start end)))
(defun tp--create-surface (buffer capability options)
"Create an unmounted BUFFER surface with CAPABILITY and OPTIONS."
(unless (memq capability '(content properties))
(signal 'tp-capability-error (list capability)))
(when (buffer-base-buffer buffer)
(signal 'tp-unsupported-buffer (list :indirect buffer)))
(let* ((range (with-current-buffer buffer
(save-restriction
(widen)
(cons (or (plist-get options :start) (point-min))
(or (plist-get options :end) (point-max))))))
(_valid (tp--validate-buffer-range buffer (car range) (cdr range))))
(tp--make-surface
:id (cl-incf tp--surface-id-counter) :buffer buffer
:capability capability :start (copy-marker (car range) nil)
:end (copy-marker (cdr range) t)
:options (tp--copy-property-value options)
:objects (make-hash-table :test #'equal) :mounts nil :index nil
:mount-index (make-hash-table :test #'eq)
:ledger nil :revision 0 :live nil :stale nil
:observers
(tp--normalize-surface-observers (plist-get options :observers)))))
(defun tp--surface-compute-function (surface input options initial)
"Return SURFACE's producer binding for INPUT, OPTIONS, and INITIAL state."
(lambda ()
(tp--prepare-surface
surface input options (and initial (not (tp--surface-live surface))))))
(defun tp--install-surface-producer (surface input options initial)
"Install INPUT as SURFACE's producer binding."
(tp--clear-surface-scope-request surface)
(let ((binding
(tp-bind surface tp--surface-producer-key
(tp--surface-compute-function surface input options initial)
:equality (lambda (_old _new) nil) :lifecycle 'retain)))
(setf (tp--surface-producer-binding surface) binding)
binding))
(defun tp--dispose-marker (marker)
"Detach MARKER when it is live."
(when (markerp marker) (set-marker marker nil)))
(defun tp--dispose-anchor (anchor)
"Invalidate ANCHOR and detach its markers."
(when (and (tp-range-anchor-p anchor) (tp--anchor-live anchor))
(tp--dispose-marker (tp--anchor-start anchor))
(tp--dispose-marker (tp--anchor-end anchor))
(setf (tp--anchor-live anchor) nil
(tp--anchor-stale anchor) t
(tp--anchor-surfaces anchor) nil)))
(defun tp--discard-new-objects (context)
"Invalidate CONTEXT's uncommitted object handles."
(dolist (object (tp--context-new-objects context))
(unless (tp--surface-object-live object)
(tp-binding-dispose-owner object)
(setf (tp--surface-object-disposed object) t
(tp--surface-object-candidate-context object) nil))))
(defun tp--discard-context (context)
"Release unpromoted handles owned by CONTEXT."
(when (and (tp-prepare-context-p context) (tp--context-active context))
(setf (tp--context-active context) nil)
(tp--discard-new-objects context)
(dolist (anchor (tp--context-created-anchors context))
(unless (tp--anchor-surfaces anchor) (tp--dispose-anchor anchor)))))
(defun tp--surface-mount-cleanup (surface)
"Release candidate markers and bindings for failed SURFACE mount."
(tp-binding-dispose-owner surface)
(tp--dispose-marker (tp--surface-start surface))
(tp--dispose-marker (tp--surface-end surface))
(setf (tp--surface-live surface) nil))
;;;###autoload
(defun tp-surface-mount (buffer plan-or-producer &optional options)
"Mount PLAN-OR-PRODUCER into BUFFER according to OPTIONS.
OPTIONS accepts :capability (`content' or `properties'), :start, :end,
:inhibit-read-only, :coordinate-mounts, :client-state, and :observers. When
:coordinate-mounts is non-nil, content mounts store numeric coordinates rather
than private markers; use it only when host edits make the whole content
surface stale and all writes are published through TP. Return a surface
handle."
(let* ((target (get-buffer buffer))
(_live (unless (buffer-live-p target)
(signal 'tp-unsupported-buffer (list buffer))))
(capability (or (plist-get options :capability)
(and (tp-surface-plan-p plan-or-producer)
(tp-surface-plan-capability plan-or-producer))
'content))
(surface (tp--create-surface target capability options))
success)
(unwind-protect
(progn
(tp--install-surface-producer surface plan-or-producer options t)
(setq success t)
surface)
(unless success (tp--surface-mount-cleanup surface)))))
;;;###autoload
(defun tp-surface-update (surface plan-or-producer)
"Atomically update SURFACE from PLAN-OR-PRODUCER and return its report."
(tp--validate-live-surface surface)
(tp--install-surface-producer
surface plan-or-producer (tp--surface-options surface) nil)
(tp-surface-report surface))
;;;###autoload
(defun tp-surface-add-observer (surface observer)
"Add function OBSERVER to live SURFACE and return SURFACE.
OBSERVER receives SURFACE and a defensive REPORT copy after each successful
publication. Adding the same function more than once has no effect."
(tp--validate-live-surface surface)
(unless (functionp observer)
(signal 'wrong-type-argument (list 'functionp observer)))
(unless (memq observer (tp--surface-observers surface))
(setf (tp--surface-observers surface)
(append (tp--surface-observers surface) (list observer))))
surface)
;;;###autoload
(defun tp-surface-remove-observer (surface observer)
"Remove function OBSERVER from live SURFACE and return SURFACE.
Removing a function that is not registered has no effect."
(tp--validate-live-surface surface)
(unless (functionp observer)
(signal 'wrong-type-argument (list 'functionp observer)))
(setf (tp--surface-observers surface)
(delq observer (tp--surface-observers surface)))
surface)
;;;###autoload
(defun tp-surface-update-scoped
(surface objects plan-or-producer &optional options)
"Atomically update SURFACE within retained OBJECTS.
PLAN-OR-PRODUCER has the same full candidate contract as
`tp-surface-update'. TP resolves OBJECTS through its mount index and rejects
any candidate output change outside their ranges before publication.
OPTIONS accepts `:on-mismatch' and `:return-report'. Its default mismatch
policy, `error', signals `tp-scope-mismatch'; `root' permits an explicit
full-surface fallback. The report is returned as a defensive snapshot by
default; `:return-report nil' avoids materializing that snapshot when the
caller reads the scalar summary from the surface instead."
(tp--validate-live-surface surface)
(setq objects (tp--normalize-surface-scopes surface objects))
(let ((on-mismatch (or (plist-get options :on-mismatch) 'error))
(return-report (if (plist-member options :return-report)
(plist-get options :return-report)
t)))
(unless (memq on-mismatch '(error root))
(signal 'wrong-type-argument
(list '(member error root) on-mismatch)))
(tp--call-with-transaction
(lambda ()
(tp--install-surface-producer
surface plan-or-producer (tp--surface-options surface) nil)
(tp--set-surface-scope-request
surface objects (list :on-mismatch on-mismatch))))
(when return-report
(tp-surface-report surface))))
(defun tp--edit-touches-span-p (beg edit-end start end)
"Return non-nil when BEG..EDIT-END touches the old START..END span."
(if (= beg edit-end)
(and (> beg start) (< beg end))
(and (< beg end) (> edit-end start))))
(defun tp--surface-before-change (beg end)
"Capture retained ranges before a BUFFER edit from BEG to END."
(setq tp--surface-before-change-state nil)
(unless (or tp--surface-publishing tp--surface-guarding-prepare)
(let ((anchors (make-hash-table :test #'eq)) content anchor-ranges)
(dolist (surface tp--buffer-surfaces)
(when (tp-surface-live-p surface)
(if (eq (tp--surface-capability surface) 'content)
(pcase-let ((`(,start . ,finish) (tp--surface-range surface)))
(push (list surface start finish) content))
(dolist (mount (tp--surface-mounts surface))
(when-let* ((anchor (tp--surface-mount-anchor mount)))
(unless (gethash anchor anchors)
(puthash anchor t anchors)
(push (list anchor
(marker-position (tp--anchor-start anchor))
(marker-position (tp--anchor-end anchor)))
anchor-ranges)))))))
(setq tp--surface-before-change-state
(list :beg beg :end end
:content content :anchors anchor-ranges)))))
(defun tp--mark-anchor-from-old-range (entry beg end)
"Apply ENTRY anchor policy for an old edit range BEG..END."
(pcase-let ((`(,anchor ,start ,finish) entry))
(when (and start finish
(tp--edit-touches-span-p beg end start finish))
(pcase (tp--anchor-boundary-policy anchor)
('shorten nil)
('remove (setf (tp--anchor-stale anchor) 'remove))
(_ (setf (tp--anchor-stale anchor) t))))))
(defun tp--surface-after-change (_beg _end _old-length)
"Maintain mount staleness after a host text edit."
(let* ((tick (buffer-chars-modified-tick))
(character-change (/= tick tp--surface-character-tick))
(state tp--surface-before-change-state))
(setq tp--surface-character-tick tick)
(setq tp--surface-before-change-state nil)
(when (and character-change state
(not tp--surface-publishing)
(not tp--surface-guarding-prepare))
(let ((old-beg (plist-get state :beg))
(old-end (plist-get state :end)))
(dolist (entry (plist-get state :content))
(pcase-let ((`(,surface ,start ,end) entry))
(when (and (tp-surface-live-p surface)
(tp--edit-touches-span-p old-beg old-end start end))
(setf (tp--surface-stale surface) t))))
(dolist (entry (plist-get state :anchors))
(tp--mark-anchor-from-old-range entry old-beg old-end))))))
(defun tp--surface-buffer-killed ()
"Dispose every retained surface owned by the current buffer."
(let ((surfaces tp--buffer-surfaces))
(setq tp--buffer-surfaces nil)
(dolist (surface surfaces) (tp--teardown-surface surface t))))
(defun tp--register-live-surface (surface)
"Register committed SURFACE under its buffer lifecycle owner."
(puthash (tp--surface-id surface) surface tp--surfaces)
(with-current-buffer (tp--surface-buffer surface)
(cl-pushnew surface tp--buffer-surfaces :test #'eq)
(setq tp--surface-character-tick (buffer-chars-modified-tick))
(add-hook 'before-change-functions #'tp--surface-before-change nil t)
(add-hook 'after-change-functions #'tp--surface-after-change nil t)
(add-hook 'kill-buffer-hook #'tp--surface-buffer-killed nil t)))
(defun tp--surface-extension-state ()
"Return the surface extension state for the active transaction."
(tp--transaction-extension tp--surface-extension-key t))
(defun tp--surface-scope-table (&optional create)
"Return the transaction-local scope table, creating it when CREATE is non-nil."
(when-let* ((state (tp--transaction-extension
tp--surface-extension-key create)))
(or (gethash 'scopes state)
(when create
(let ((table (make-hash-table :test #'eq)))
(puthash 'scopes table state)
table)))))
(defun tp--surface-scope-request (surface)
"Return SURFACE's one-shot scope request in the active transaction."
(when-let* ((table (tp--surface-scope-table)))
(gethash surface table)))
(defun tp--clear-surface-scope-request (surface)
"Clear SURFACE's pending one-shot scope request when one exists."
(when tp--transaction-active
(when-let* ((table (tp--surface-scope-table)))
(remhash surface table))))
(defun tp--set-surface-scope-request (surface objects options)
"Set SURFACE's one-shot scope request to OBJECTS and OPTIONS."
(puthash surface
(list :objects (copy-sequence objects)
:options (tp--copy-property-value options))
(tp--surface-scope-table t)))
(defun tp--surface-prepared-table ()
"Return the transaction-local surface candidate table."
(let ((state (tp--surface-extension-state)))
(or (gethash 'prepared state)
(let ((table (make-hash-table :test #'eq)))
(puthash 'prepared table state)
table))))
(defun tp--surface-binding-changed (binding _old new)
"Queue NEW when BINDING is a retained surface producer."
(when (and (equal (tp-binding-key binding) tp--surface-producer-key)
(tp-surface-p (tp-binding-owner binding)))
(unless (tp--prepared-surface-p new)
(signal 'tp-invalid-surface-plan (list :producer-result new)))
(let* ((table (tp--surface-prepared-table))
(surface (tp-binding-owner binding))
(previous (gethash surface table)))
(when previous
(tp--discard-context (tp--prepared-surface-context previous)))
(puthash surface new table))))
(defun tp--prepared-mount-signature (prepared &optional retained-signature)
"Return PREPARED's exact target mount attachment signature.
RETAINED-SIGNATURE may supply the already captured live signature."
(if (tp--prepared-surface-retained-mount-state-p prepared)
(let* ((surface (tp--prepared-surface-surface prepared))
(updates (tp--prepared-surface-mount-coordinate-updates prepared))
(base (marker-position (tp--surface-start surface))))
(if (zerop (length updates))
(or retained-signature (tp--live-mount-signature surface))
(cl-loop for index from 0 below (length updates) by 3
for mount = (aref updates index)
collect (list (tp--surface-mount-object mount)
(tp--surface-mount-anchor mount)
(- (aref updates (1+ index)) base)
(- (aref updates (+ index 2)) base)
(tp--surface-mount-tags mount)))))
(mapcar (lambda (spec)
(list (plist-get spec :object) (plist-get spec :anchor)
(plist-get spec :start) (plist-get spec :end)
(plist-get spec :tags)))
(tp--prepared-surface-mount-specs prepared))))
(defun tp--live-mount-signature (surface)
"Return SURFACE's stable live mount signature."
(let ((base (if (eq (tp--surface-capability surface) 'content)
(marker-position (tp--surface-start surface))
0)))
(mapcar (lambda (mount)
(list (tp--surface-mount-object mount)
(tp--surface-mount-anchor mount)
(- (tp--mount-position (tp--surface-mount-start mount)) base)
(- (tp--mount-position (tp--surface-mount-end mount)) base)
(tp--surface-mount-tags mount)))
(tp--surface-mounts surface))))
(defun tp--ensure-surface-mount-id (mount)
"Return MOUNT's stable private id, allocating it before publication if absent."
(or (tp--surface-mount-id mount)
(setf (tp--surface-mount-id mount) (cl-incf tp--mount-id-counter))))
(defun tp--mount-match-queues (mounts object capability seen)
"Group matching MOUNTS by anchor identity and equal tags in live order.
Only include OBJECT and CAPABILITY by identity. SEEN prevents enqueuing the
same mount record twice. Queue spines are private; no mount IDs are allocated."
(let ((anchors (make-hash-table :test #'eq)))
(dolist (mount mounts)
(when (and (eq object (tp--surface-mount-object mount))
(eq capability (tp--surface-mount-capability mount))
(not (gethash mount seen)))
(puthash mount t seen)
(let* ((anchor (tp--surface-mount-anchor mount))
(tags (tp--surface-mount-tags mount))
(by-tags (or (gethash anchor anchors)
(puthash anchor (make-hash-table :test #'equal)
anchors))))
(push mount (gethash tags by-tags)))))
(maphash
(lambda (_anchor by-tags)
(maphash (lambda (tags queue)
(puthash tags (nreverse queue) by-tags))
by-tags))
anchors)
anchors))
(defun tp--assign-prepared-mount-ids (prepared)
"Bind PREPARED mount specs to stable live or fresh private mount ids."
(let* ((surface (tp--prepared-surface-surface prepared))
(live (tp--surface-mounts surface)))
(if (tp--prepared-surface-retained-mount-state-p prepared)
(dolist (mount live) (tp--ensure-surface-mount-id mount))
(let ((capability (tp--surface-capability surface))
(live-by-object (tp--surface-mount-index surface))
(queues-by-object (make-hash-table :test #'eq))
(seen (make-hash-table :test #'eq)))
(setf
(tp--prepared-surface-mount-specs prepared)
(mapcar
(lambda (spec)
(let* ((object (plist-get spec :object))
(anchors
(or (gethash object queues-by-object)
;; Cache empty queues too: later unmatched specs must
;; not rebuild the same object's live bucket.
(puthash object
(tp--mount-match-queues
(gethash object live-by-object)
object capability seen)
queues-by-object)))
(by-tags (gethash (plist-get spec :anchor) anchors))
(tags (plist-get spec :tags))
(queue (and by-tags (gethash tags by-tags)))
(match (car queue)))
(when match (puthash tags (cdr queue) by-tags))
(plist-put
spec :mount-id
(if match
(tp--ensure-surface-mount-id match)
(cl-incf tp--mount-id-counter)))))
(tp--prepared-surface-mount-specs prepared)))))
prepared))
(defun tp--prepared-target-mount-ids (prepared)
"Return PREPARED's exact ordered target mount identities."
(if (tp--prepared-surface-retained-mount-state-p prepared)
(mapcar #'tp--ensure-surface-mount-id
(tp--surface-mounts
(tp--prepared-surface-surface prepared)))
(mapcar (lambda (spec) (plist-get spec :mount-id))
(tp--prepared-surface-mount-specs prepared))))
(defun tp--live-mount-ids (surface)
"Return SURFACE's exact ordered private mount identities."
(mapcar #'tp--ensure-surface-mount-id (tp--surface-mounts surface)))
(defun tp--content-output-equal-p (prepared)
"Return non-nil when PREPARED already matches its content mount."
(let ((surface (tp--prepared-surface-surface prepared)))
(if (not (eq (tp--surface-capability surface) 'content))
t
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(with-current-buffer (tp--surface-buffer surface)
(save-restriction
(widen)
(equal-including-properties
(buffer-substring start end)
(tp--prepared-surface-rendered prepared))))))))
(defun tp--prepared-changed-p (prepared)
"Return non-nil when PREPARED differs from its committed surface."
(let ((surface (tp--prepared-surface-surface prepared)))
(or (tp--prepared-surface-initial prepared)
(not (tp--plan-equal-p (tp--surface-plan surface)
(tp--prepared-surface-plan prepared)))
(not (equal (tp--surface-client-state surface)
(tp--prepared-surface-client-state prepared)))
(not (eq (tp--surface-producer surface)
(tp--prepared-surface-producer prepared)))
(tp--prepared-surface-created prepared)
(tp--prepared-surface-removed prepared)
(not (tp--content-output-equal-p prepared))
(not (equal (tp--live-mount-signature surface)
(tp--prepared-mount-signature prepared))))))
(defun tp--sorted-prepared-surfaces (table)
"Return changed candidates in TABLE ordered by surface id."
(let (prepared)
(maphash (lambda (_surface candidate)
(when (tp--prepared-changed-p candidate)
(push candidate prepared)))
table)
(sort prepared
(lambda (left right)
(< (tp--surface-id (tp--prepared-surface-surface left))
(tp--surface-id (tp--prepared-surface-surface right)))))))
(defun tp--all-prepared-surfaces (table)
"Return every candidate in TABLE ordered by stable surface id."
(let (prepared)
(maphash (lambda (_surface candidate) (push candidate prepared)) table)
(sort prepared
(lambda (left right)
(< (tp--surface-id (tp--prepared-surface-surface left))
(tp--surface-id (tp--prepared-surface-surface right)))))))
(defun tp--publication-step (step surface)
"Run the internal publication failure hook for STEP and SURFACE."
(when tp--surface-publication-step-function
(funcall tp--surface-publication-step-function step surface)))
(defun tp--common-prefix-length (left right)
"Return the common character prefix length of LEFT and RIGHT."
(let ((limit (min (length left) (length right))) (index 0))
(while (and (< index limit) (= (aref left index) (aref right index)))
(setq index (1+ index)))
index))
(defun tp--common-suffix-length (left right prefix)
"Return LEFT and RIGHT's common suffix length after PREFIX."
(let ((limit (- (min (length left) (length right)) prefix)) (count 0))
(while (and (< count limit)
(= (aref left (- (length left) count 1))
(aref right (- (length right) count 1))))
(setq count (1+ count)))
count))
(defun tp--validate-published-content-range (buffer start rendered)
"Validate inserted BUFFER text from START with RENDERED.
Return whether the properties match.
An external `after-change-functions' hook may alter the inserted properties;
return nil for that recoverable case so TP reapplies the rendered runs. A
character rewrite is not recoverable at the property layer and signals before
the transaction can commit."
(with-current-buffer buffer
(let ((end (+ start (length rendered))))
(unless (and (<= (point-min) start) (<= end (point-max))
(equal (buffer-substring-no-properties start end)
(substring-no-properties rendered)))
(signal 'tp-publication-mismatch
(list :buffer buffer :start start :end end)))
(equal-including-properties
(buffer-substring start end) rendered))))
(defun tp--content-text-operation (surface rendered)
"Publish SURFACE's minimal character replacement for RENDERED."
(pcase-let* ((`(,start . ,end) (tp--surface-range surface))
(old (buffer-substring-no-properties start end))
(new (substring-no-properties rendered))
(prefix (tp--common-prefix-length old new))
(suffix (tp--common-suffix-length old new prefix)))
(unless (equal old new)
(delete-region (+ start prefix) (- end suffix))
(goto-char (+ start prefix))
(let ((inserted (substring rendered prefix (- (length rendered) suffix))))
(insert inserted)
(when (tp--validate-published-content-range
(tp--surface-buffer surface) (+ start prefix) inserted)
(push (cons prefix (- (length new) suffix))
tp--content-published-ranges))))
(set-marker (tp--surface-start surface) start)
(set-marker (tp--surface-end surface) (+ start (length new)))
(if (equal old new) 0 1)))
(defun tp--string-property-run-diff-p (buffer start rendered from to)
"Return non-nil when BUFFER at START differs from RENDERED on FROM..TO.
The rendered interval is already one direct-property run. Compare its
boundary value once and reject an interior buffer boundary; scanning every
character makes a large rendered surface quadratic in its number of runs."
(let* ((buffer-start (+ start from))
(buffer-end (+ start to))
(buffer-next (or (next-property-change
buffer-start buffer buffer-end)
buffer-end)))
(or (not (tp--plan-props-equal-p
(text-properties-at buffer-start buffer)
(text-properties-at from rendered)))
(< buffer-next buffer-end))))
(defun tp--content-property-operations-in-range
(buffer start rendered from to)
"Return property operations for RENDERED FROM..TO at BUFFER position START."
(let ((offset from) operations)
(while (< offset to)
(let* ((next (next-property-change offset rendered to))
(props (text-properties-at offset rendered)))
(when (tp--string-property-run-diff-p
buffer start rendered offset next)
(push (list :start (+ start offset) :end (+ start next)
:props props)
operations))
(setq offset next)))
(nreverse operations)))
(defun tp--content-property-operations
(surface rendered &optional ranges scoped)
"Return exact direct-property operations for SURFACE and RENDERED.
When SCOPED is non-nil, inspect only RANGES, including an empty set."
(let* ((buffer (tp--surface-buffer surface))
(start (marker-position (tp--surface-start surface)))
(ranges (if scoped ranges (list (cons 0 (length rendered)))))
(ranges
(if tp--content-published-ranges
(tp--subtract-ranges
ranges (tp--coalesce-ranges tp--content-published-ranges))
ranges))
operations)
(dolist (range ranges operations)
(setq operations
(nconc operations
(tp--content-property-operations-in-range
buffer start rendered (car range) (cdr range)))))))
(defun tp--apply-content-property-operations (buffer operations)
"Apply content property OPERATIONS in BUFFER silently."
(with-current-buffer buffer
(with-silent-modifications
(dolist (operation operations)
(set-text-properties (plist-get operation :start)
(plist-get operation :end)
(plist-get operation :props) buffer)))))
(defun tp--apply-property-operation (buffer operation)
"Apply one host-range property OPERATION in BUFFER."
(let ((start (plist-get operation :start))
(end (plist-get operation :end))
(property (plist-get operation :property)))
(if (plist-get operation :present)
(put-text-property start end property
(plist-get operation :value) buffer)
(remove-list-of-text-properties start end (list property) buffer))))
(defun tp--apply-properties-operations (surface operations)
"Apply properties-only OPERATIONS for SURFACE silently."
(let ((buffer (tp--surface-buffer surface)))
(with-current-buffer buffer
(with-silent-modifications
(dolist (operation operations)
(tp--apply-property-operation buffer operation))))))
(defun tp--make-content-mounts (prepared)
"Create content mounts for PREPARED using its declared coordinate policy."
(let* ((surface (tp--prepared-surface-surface prepared))
(buffer (tp--surface-buffer surface))
(base (marker-position (tp--surface-start surface)))
(coordinate-p (plist-get (tp--surface-options surface)
:coordinate-mounts)))
(mapcar
(lambda (spec)
(tp--make-surface-mount
:id (or (plist-get spec :mount-id)
(cl-incf tp--mount-id-counter))
:object (plist-get spec :object)
:start (if coordinate-p
(+ base (plist-get spec :start))
(with-current-buffer buffer
(copy-marker (+ base (plist-get spec :start)) nil)))
:end (if coordinate-p
(+ base (plist-get spec :end))
(with-current-buffer buffer
(copy-marker (+ base (plist-get spec :end)) t)))
:tags (plist-get spec :tags) :capability 'content))
(tp--prepared-surface-mount-specs prepared))))
(defun tp--make-properties-mounts (prepared)
"Create property mounts for PREPARED from its live anchors."
(mapcar
(lambda (spec)
(let ((anchor (plist-get spec :anchor)))
(tp--make-surface-mount
:id (or (plist-get spec :mount-id)
(cl-incf tp--mount-id-counter))
:object (plist-get spec :object)
:start (tp--anchor-start anchor) :end (tp--anchor-end anchor)
:tags (plist-get spec :tags) :capability 'properties :anchor anchor)))
(tp--prepared-surface-mount-specs prepared)))
(defun tp--make-live-ledger (prepared)
"Create marker-backed property ledger entries for PREPARED."
(let ((buffer (tp--surface-buffer (tp--prepared-surface-surface prepared))))
(mapcar
(lambda (spec)
(tp--make-property-ledger
:start (with-current-buffer buffer
(copy-marker (plist-get spec :start) nil))
:end (with-current-buffer buffer
(copy-marker (plist-get spec :end) t))
:property (plist-get spec :property)
:baseline-present (plist-get spec :baseline-present)
:baseline-value (plist-get spec :baseline-value)
:published-present (plist-get spec :published-present)
:published-value (plist-get spec :published-value)
:anchors (plist-get spec :anchors)))
(tp--prepared-surface-ledger-specs prepared))))
(defun tp--index-mounts-by-object (mounts)
"Return an object-keyed index of marker-backed MOUNTS."
(let ((index (make-hash-table :test #'eq)))
(dolist (mount mounts)
(push mount (gethash (tp--surface-mount-object mount) index)))
(maphash (lambda (object entries)
(puthash object (nreverse entries) index))
index)
index))
(defun tp--create-candidate-mount-state (prepared)
"Create PREPARED's marker-backed mount, ledger, and index candidates."
(let* ((surface (tp--prepared-surface-surface prepared))
(mounts (if (eq (tp--surface-capability surface) 'content)
(tp--make-content-mounts prepared)
(tp--make-properties-mounts prepared)))
(ledger (and (eq (tp--surface-capability surface) 'properties)
(tp--make-live-ledger prepared))))
(setf (tp--prepared-surface-live-mounts prepared) mounts
(tp--prepared-surface-live-mount-index prepared)
(tp--index-mounts-by-object mounts)
(tp--prepared-surface-live-ledger prepared) ledger)
mounts))
(defun tp--retain-candidate-mount-state (prepared)
"Apply PREPARED coordinate updates while retaining mount identity and index."
(let* ((surface (tp--prepared-surface-surface prepared))
(mounts (tp--surface-mounts surface))
(updates (tp--prepared-surface-mount-coordinate-updates prepared))
(undo (make-vector (* 2 (/ (length updates) 3)) nil))
(update-index 0)
(undo-index 0))
(while (< update-index (length updates))
(let ((mount (aref updates update-index)))
(aset undo undo-index (tp--surface-mount-start mount))
(aset undo (1+ undo-index) (tp--surface-mount-end mount))
(setf (tp--surface-mount-start mount)
(aref updates (1+ update-index))
(tp--surface-mount-end mount)
(aref updates (+ update-index 2)))
(setq update-index (+ update-index 3)
undo-index (+ undo-index 2))))
(setf (tp--prepared-surface-mount-coordinate-undo prepared)
undo
(tp--prepared-surface-live-mounts prepared) mounts
(tp--prepared-surface-live-mount-index prepared)
(tp--surface-mount-index surface)
(tp--prepared-surface-live-ledger prepared)
(tp--surface-ledger surface))
mounts))
(defun tp--restore-retained-mount-state (prepared)
"Restore PREPARED retained mount coordinates after rollback."
(let ((updates (tp--prepared-surface-mount-coordinate-updates prepared))
(undo (tp--prepared-surface-mount-coordinate-undo prepared))
(update-index 0)
(undo-index 0))
(when (and (vectorp updates) (vectorp undo))
(while (< update-index (length updates))
(let ((mount (aref updates update-index)))
(setf (tp--surface-mount-start mount) (aref undo undo-index)
(tp--surface-mount-end mount) (aref undo (1+ undo-index)))
(setq update-index (+ update-index 3)
undo-index (+ undo-index 2))))))
(setf (tp--prepared-surface-mount-coordinate-undo prepared) nil))
(defun tp--surface-snapshot (surface)
"Return a rollback snapshot of SURFACE side state."
(tp--make-surface-snapshot
:plan (tp--surface-plan surface) :objects (tp--surface-objects surface)
:mounts (tp--surface-mounts surface) :index (tp--surface-index surface)
:mount-index (tp--surface-mount-index surface)
:ledger (tp--surface-ledger surface)
:client-state (tp--surface-client-state surface)
:producer (tp--surface-producer surface)
:revision (tp--surface-revision surface) :report (tp--surface-report surface)
:live (tp--surface-live surface) :stale (tp--surface-stale surface)))
(defun tp--surface-object-table (objects)
"Return a path-indexed hash table containing OBJECTS."
(let ((table (make-hash-table :test #'equal)))
(dolist (object objects)
(puthash (tp--surface-object-path object) object table))
table))
(defun tp--surface-report-value (prepared text-ops property-ops)
"Build PREPARED's report from TEXT-OPS and PROPERTY-OPS."
(let* ((surface (tp--prepared-surface-surface prepared))
(old-revision (tp--surface-revision surface))
(new-revision (1+ old-revision))
(scoped (tp--prepared-surface-scope-objects prepared))
(fallback (tp--prepared-surface-scope-fallback prepared))
(patches (tp--prepared-surface-scope-patches prepared))
(touched
(if-let* ((batch (tp--prepared-surface-commit-batch prepared)))
(cl-loop for patch in (tp-commit-batch-patches batch)
sum (length (plist-get patch :replacement)))
(if (and scoped (not fallback))
(cl-loop for patch in patches
sum (if (eq (tp--surface-capability surface) 'content)
(length (plist-get patch :replacement))
(- (cdr patch) (car patch))))
(length (tp--prepared-surface-rendered prepared))))))
(list :transaction-id tp--surface-transaction-id
:surface-id (tp--surface-id surface)
:old-revision old-revision :new-revision new-revision
:candidate-source-writes (length tp--transaction-signals)
:invalidated-bindings (tp--transaction-counter-delta :invalidated)
:recomputed-bindings (tp--transaction-counter-delta :recomputed)
:skipped-bindings (tp--transaction-counter-delta :skipped)
:reconciled-objects (tp--prepared-surface-reconciled prepared)
:created-objects (length (tp--prepared-surface-created prepared))
:removed-objects (length (tp--prepared-surface-removed prepared))
:moved-objects (tp--prepared-surface-moved prepared)
:text-operations text-ops :property-operations property-ops
:retained-content-p
(tp--prepared-surface-retained-content-p prepared)
:touched-characters touched
:full-root (or (null scoped) fallback)
:scope-count (length scoped)
:scope-range-count (and scoped (length patches))
:scope-fallback fallback
:property-conflicts nil :rolled-back nil
:commit-batch (and (tp--prepared-surface-commit-batch prepared) t)
:retained-mount-state
(and (tp--prepared-surface-retained-mount-state-p prepared) t)
:failure nil :observer-errors nil :timing nil)))
(defun tp--scoped-content-ranges (patches)
"Return candidate relative ranges from scoped PATCHES."
(mapcar (lambda (patch)
(cons (plist-get patch :new-start)
(plist-get patch :new-end)))
patches))
(defun tp--publish-scoped-content-text (surface rendered patches)
"Publish scoped PATCHES from RENDERED for SURFACE.
Return the number of text operations."
(let ((base (marker-position (tp--surface-start surface)))
(count 0))
(dolist (patch (reverse patches))
(let* ((start (+ base (plist-get patch :old-start)))
(end (+ base (plist-get patch :old-end)))
(replacement (plist-get patch :replacement))
(old-plain (buffer-substring-no-properties start end))
(new-plain (substring-no-properties replacement))
(prefix (tp--common-prefix-length old-plain new-plain))
(suffix (tp--common-suffix-length old-plain new-plain prefix))
(old-end (- end suffix))
(new-end (- (length replacement) suffix)))
(unless (equal old-plain new-plain)
(delete-region (+ start prefix) old-end)
(goto-char (+ start prefix))
(let ((inserted (substring replacement prefix new-end)))
(insert inserted)
(when (tp--validate-published-content-range
(tp--surface-buffer surface) (+ start prefix) inserted)
(push (cons (+ (plist-get patch :new-start) prefix)
(+ (plist-get patch :new-start) new-end))
tp--content-published-ranges)))
(cl-incf count))))
(set-marker (tp--surface-start surface) base)
(set-marker (tp--surface-end surface) (+ base (length rendered)))
count))
(defun tp--publish-retained-content-text (prepared)
"Publish PREPARED retained content in one full-surface text operation.
The candidate proof guarantees that the retained surface scope is exactly the
surface range. RENDERED already carries its final text properties, so a
delete/insert preserves the authoritative property runs without constructing
the generic property-operation ledger."
(let* ((surface (tp--prepared-surface-surface prepared))
(buffer (tp--surface-buffer surface))
(rendered (tp--prepared-surface-rendered prepared))
(range (tp--surface-range surface))
(start (car range))
(end (cdr range))
(changed (not (equal-including-properties
(with-current-buffer buffer
(buffer-substring start end))
rendered))))
(with-current-buffer buffer
(let ((inhibit-read-only
(plist-get (tp--surface-options surface) :inhibit-read-only)))
(unless (= start (point))
(goto-char start))
(when changed
(delete-region start end)
(goto-char start)
(insert rendered))
(set-marker (tp--surface-start surface) start)
(set-marker (tp--surface-end surface) (+ start (length rendered)))))
(when changed
(with-current-buffer buffer
(unless (tp--validate-published-content-range buffer start rendered)
(signal 'tp-publication-mismatch
(list :surface (tp--surface-id surface)
:reason 'retained-content-properties)))))
(if changed 1 0)))
(defun tp--publish-buffer-content (prepared)
"Publish PREPARED's text and properties, returning operation counts."
(cl-block publish
(when-let* ((batch (tp--prepared-surface-commit-batch prepared)))
(let ((surface (tp--prepared-surface-surface prepared)))
(tp--commit-batch-apply-patches surface batch)
(tp--publication-step 'text surface)
(tp--commit-batch-validate-buffer surface batch)
(tp--publication-step 'property surface)
(cl-return-from publish
(cons (length (tp-commit-batch-patches batch)) 0))))
(let* ((surface (tp--prepared-surface-surface prepared))
(buffer (tp--surface-buffer surface))
(rendered (tp--prepared-surface-rendered prepared))
(scoped (tp--prepared-surface-scope-objects prepared))
(fallback (tp--prepared-surface-scope-fallback prepared))
(patches (tp--prepared-surface-scope-patches prepared))
(text-operations 0)
property-operations)
(let (tp--content-published-ranges)
(with-current-buffer buffer
(let ((point-before (point)))
(unwind-protect
(save-restriction
(widen)
(let ((inhibit-read-only
(plist-get (tp--surface-options surface) :inhibit-read-only)))
(setq text-operations
(if (tp--prepared-surface-retained-content-p prepared)
(tp--publish-retained-content-text prepared)
(if (and scoped (not fallback))
(tp--publish-scoped-content-text
surface rendered patches)
(tp--content-text-operation surface rendered))))
(tp--publication-step 'text surface)
(unless (tp--prepared-surface-retained-content-p prepared)
(setq property-operations
(tp--content-property-operations
surface rendered
(and scoped (not fallback)
(tp--scoped-content-ranges patches))
(and scoped (not fallback))))
(tp--apply-content-property-operations buffer property-operations)
(tp--publication-step 'property surface))
(when (tp--prepared-surface-retained-content-p prepared)
(tp--publication-step 'property surface))))
(goto-char (min point-before (point-max)))))
(cons text-operations (length property-operations)))))))
(defun tp--publish-buffer-properties (prepared)
"Publish PREPARED's host-range property operations."
(let* ((surface (tp--prepared-surface-surface prepared))
(operations (tp--prepared-surface-property-operations prepared)))
(tp--publication-step 'text surface)
(tp--apply-properties-operations surface operations)
(tp--publication-step 'property surface)
(cons 0 (length operations))))
(defun tp--promote-prepared-objects (prepared)
"Promote PREPARED's touched identities to live state."
(let ((surface (tp--prepared-surface-surface prepared))
(context (tp--prepared-surface-context prepared)))
(dolist (object (tp--prepared-surface-objects prepared))
(setf (tp--surface-object-surface object) surface
(tp--surface-object-live object) t
(tp--surface-object-disposed object) nil
(tp--surface-object-candidate-context object) nil))
(setf (tp--context-active context) nil)))
(defun tp--swap-surface-state (prepared counts)
"Promote PREPARED side state using operation COUNTS."
(let* ((surface (tp--prepared-surface-surface prepared))
(mounts (tp--prepared-surface-live-mounts prepared))
(ledger (tp--prepared-surface-live-ledger prepared))
(report (tp--surface-report-value prepared (car counts) (cdr counts))))
(tp--promote-prepared-objects prepared)
(setf (tp--surface-plan surface) (tp--prepared-surface-plan prepared)
(tp--surface-objects surface)
(if (tp--prepared-surface-retained-mount-state-p prepared)
(tp--surface-objects surface)
(tp--surface-object-table
(tp--prepared-surface-objects prepared)))
(tp--surface-mounts surface) mounts
(tp--surface-index surface) mounts
(tp--surface-mount-index surface)
(tp--prepared-surface-live-mount-index prepared)
(tp--surface-ledger surface) ledger
(tp--surface-client-state surface)
(tp--prepared-surface-client-state prepared)
(tp--surface-producer surface) (tp--prepared-surface-producer prepared)
(tp--surface-revision surface) (1+ (tp--surface-revision surface))
(tp--surface-report surface) report
(tp--surface-live surface) t
(tp--surface-stale surface) nil
(tp--prepared-surface-report prepared) report)
(tp--publication-step 'client-state surface)
(when (tp--prepared-surface-initial prepared)
(tp--register-live-surface surface))))
(defun tp--publish-one-surface (prepared)
"Publish PREPARED buffer and side state."
(let* ((surface (tp--prepared-surface-surface prepared))
(observed (tp--surface-observers surface))
(started (and observed (tp--surface-observation-clock)))
(counts (if (eq (tp--surface-capability surface) 'content)
(tp--publish-buffer-content prepared)
(tp--publish-buffer-properties prepared))))
(if (tp--prepared-surface-retained-mount-state-p prepared)
(tp--retain-candidate-mount-state prepared)
(tp--create-candidate-mount-state prepared))
(when-let* ((batch (and (not (tp--prepared-surface-retained-mount-state-p
prepared))
(tp--prepared-surface-commit-batch prepared))))
(tp--commit-batch-validate-mounts
surface batch (tp--prepared-surface-live-mounts prepared)))
(tp--publication-step 'marker surface)
(tp--publication-step 'index surface)
(tp--swap-surface-state prepared counts)
(when started
(let* ((duration-ms
(* 1000.0 (- (tp--surface-observation-clock) started)))
(report (tp--surface-report surface)))
(setq report (plist-put report :provider 'tp)
report (plist-put report :stage 'publication)
report (plist-put report :duration-ms duration-ms)
report (plist-put report :timing
(list :publication (/ duration-ms 1000.0))))
(setf (tp--surface-report surface) report
(tp--prepared-surface-report prepared) report)))))
(defun tp--journal-intervals (prepared-list)
"Return buffer intervals whose properties PREPARED-LIST may mutate."
(let (entries)
(dolist (prepared prepared-list)
(let ((surface (tp--prepared-surface-surface prepared)))
(if (eq (tp--surface-capability surface) 'content)
(if (and (tp--prepared-surface-scope-objects prepared)
(not (tp--prepared-surface-scope-fallback prepared)))
(let ((base (marker-position (tp--surface-start surface))))
(dolist (patch (tp--prepared-surface-scope-patches prepared))
(push (list (tp--surface-buffer surface)
(+ base (plist-get patch :old-start))
(+ base (plist-get patch :old-end)))
entries)))
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(push (list (tp--surface-buffer surface) start end) entries)))
(dolist (operation
(tp--prepared-surface-property-operations prepared))
(push (list (tp--surface-buffer surface)
(plist-get operation :start)
(plist-get operation :end))
entries)))))
entries))
(defun tp--merge-buffer-intervals (entries)
"Merge overlapping journal ENTRIES per buffer."
(let ((by-buffer (make-hash-table :test #'eq)) result)
(dolist (entry entries)
(push (cdr entry) (gethash (car entry) by-buffer)))
(maphash
(lambda (buffer intervals)
(setq intervals (sort intervals (lambda (a b) (< (car a) (car b)))))
(let (merged)
(dolist (interval intervals)
(if (and merged (<= (car interval) (cadar merged)))
(setcar (cdar merged) (max (cadar merged) (cadr interval)))
(push (copy-sequence interval) merged)))
(dolist (interval (nreverse merged))
(push (list buffer (car interval) (cadr interval)) result))))
by-buffer)
result))
(defun tp--capture-property-journals (prepared-list)
"Capture direct property journals for PREPARED-LIST."
(mapcar
(lambda (entry)
(pcase-let ((`(,buffer ,start ,end) entry))
(list buffer start end
(with-current-buffer buffer
(save-restriction
(widen) (buffer-substring start end))))))
(tp--merge-buffer-intervals (tp--journal-intervals prepared-list))))
(defun tp--replay-string-properties (buffer start string)
"Restore STRING's direct properties into BUFFER starting at START."
(let ((length (length string)) (offset 0))
(when (> length 0)
(set-text-properties start (+ start length) nil buffer)
(while (< offset length)
(let ((next (next-property-change offset string length)))
(set-text-properties (+ start offset) (+ start next)
(text-properties-at offset string) buffer)
(setq offset next))))))
(defun tp--restore-property-journals (journals)
"Restore direct property JOURNALS after buffer change cancellation."
(dolist (journal journals)
(pcase-let ((`(,buffer ,start ,_end ,string) journal))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-restriction
(widen)
(let ((inhibit-read-only t))
(with-silent-modifications
(tp--replay-string-properties buffer start string)))))))))
(defun tp--prepared-buffers (prepared-list)
"Return distinct live buffers affected by PREPARED-LIST."
(let ((seen (make-hash-table :test #'eq)) buffers)
(dolist (prepared prepared-list)
(let ((buffer
(tp--surface-buffer (tp--prepared-surface-surface prepared))))
(unless (gethash buffer seen)
(puthash buffer t seen)
(push buffer buffers))))
(nreverse buffers)))
(defun tp--capture-view-state (buffers)
"Capture point, windows, and modified state for BUFFERS."
(let (states success)
(unwind-protect
(progn
(dolist (buffer buffers)
(with-current-buffer buffer
(let ((state
;; Point is a cursor location, not an insertion anchor:
;; content publication must not make it follow a patch
;; to the end of the inserted text.
(list buffer (copy-marker (point) nil)
(buffer-modified-p) nil)))
(push state states)
(dolist (window (get-buffer-window-list buffer nil t))
(push (cons window
(copy-marker (window-start window) nil))
(nth 3 state)))
(setf (nth 3 state) (nreverse (nth 3 state))))))
(setq states (nreverse states)
success t)
states)
(unless success (tp--dispose-view-state states)))))
(defun tp--dispose-view-state (states)
"Dispose marker storage retained by captured view STATES."
(dolist (state states)
(pcase-let ((`(,_buffer ,point-marker ,_modified ,windows) state))
(tp--dispose-marker point-marker)
(dolist (entry windows) (tp--dispose-marker (cdr entry))))))
(defun tp--restore-view-state (states &optional retain-markers)
"Restore captured point, window, and modified STATES.
When RETAIN-MARKERS is non-nil, keep snapshot markers for rollback."
(dolist (state states)
(pcase-let ((`(,buffer ,point-marker ,modified ,windows) state))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when-let* ((position (marker-position point-marker)))
(goto-char (min (point-max) (max (point-min) position))))
(set-buffer-modified-p modified))
(dolist (entry windows)
(when (window-live-p (car entry))
(when-let* ((position (marker-position (cdr entry))))
(set-window-start (car entry) position t)))))
(unless retain-markers
(tp--dispose-marker point-marker)
(dolist (entry windows) (tp--dispose-marker (cdr entry)))))))
(defun tp--dispose-content-mounts (mounts)
"Detach private markers owned by content MOUNTS."
(dolist (mount mounts)
(when (eq (tp--surface-mount-capability mount) 'content)
(tp--dispose-marker (tp--surface-mount-start mount))
(tp--dispose-marker (tp--surface-mount-end mount)))))
(defun tp--dispose-ledger (ledger)
"Detach interval markers owned by property LEDGER entries."
(dolist (entry ledger)
(tp--dispose-marker (tp--property-ledger-start entry))
(tp--dispose-marker (tp--property-ledger-end entry))))
(defun tp--unregister-surface (surface)
"Remove SURFACE from weak and buffer-local registries."
(remhash (tp--surface-id surface) tp--surfaces)
(when (buffer-live-p (tp--surface-buffer surface))
(with-current-buffer (tp--surface-buffer surface)
(setq tp--buffer-surfaces (delq surface tp--buffer-surfaces))
(unless tp--buffer-surfaces
(remove-hook 'before-change-functions #'tp--surface-before-change t)
(remove-hook 'after-change-functions #'tp--surface-after-change t)
(remove-hook 'kill-buffer-hook #'tp--surface-buffer-killed t)))))
(defun tp--restore-surface-snapshot (prepared snapshot)
"Restore PREPARED's surface from SNAPSHOT after failed publication."
(let ((surface (tp--prepared-surface-surface prepared)))
(tp--restore-retained-mount-state prepared)
(tp--dispose-content-mounts
(tp--prepared-surface-live-mounts prepared))
(tp--dispose-ledger (tp--prepared-surface-live-ledger prepared))
(if (not (buffer-live-p (tp--surface-buffer surface)))
(progn
(when (tp--surface-snapshot-objects snapshot)
(maphash (lambda (_path object)
(setf (tp--surface-object-live object) nil
(tp--surface-object-disposed object) t))
(tp--surface-snapshot-objects snapshot)))
(setf (tp--surface-live surface) nil
(tp--surface-stale surface) 'killed
(tp--surface-objects surface)
(tp--surface-snapshot-objects snapshot)
(tp--surface-mounts surface)
(tp--surface-snapshot-mounts snapshot)
(tp--surface-index surface)
(tp--surface-snapshot-index snapshot)
(tp--surface-mount-index surface)
(tp--surface-snapshot-mount-index snapshot)
(tp--surface-ledger surface)
(tp--surface-snapshot-ledger snapshot)))
(when (and (tp--surface-live surface)
(not (tp--surface-snapshot-live snapshot)))
(tp--unregister-surface surface))
(dolist (object (tp--prepared-surface-created prepared))
(setf (tp--surface-object-live object) nil
(tp--surface-object-disposed object) t))
(when (tp--surface-snapshot-objects snapshot)
(maphash (lambda (_path object)
(setf (tp--surface-object-live object)
(tp--surface-snapshot-live snapshot)
(tp--surface-object-disposed object) nil))
(tp--surface-snapshot-objects snapshot)))
(setf (tp--surface-plan surface) (tp--surface-snapshot-plan snapshot)
(tp--surface-objects surface) (tp--surface-snapshot-objects snapshot)
(tp--surface-mounts surface) (tp--surface-snapshot-mounts snapshot)
(tp--surface-index surface) (tp--surface-snapshot-index snapshot)
(tp--surface-mount-index surface)
(tp--surface-snapshot-mount-index snapshot)
(tp--surface-ledger surface) (tp--surface-snapshot-ledger snapshot)
(tp--surface-client-state surface)
(tp--surface-snapshot-client-state snapshot)
(tp--surface-producer surface)
(tp--surface-snapshot-producer snapshot)
(tp--surface-revision surface)
(tp--surface-snapshot-revision snapshot)
(tp--surface-report surface) (tp--surface-snapshot-report snapshot)
(tp--surface-live surface) (tp--surface-snapshot-live snapshot)
(tp--surface-stale surface) (tp--surface-snapshot-stale snapshot)))))
(defun tp--prepare-change-group-for-buffers (buffers)
"Prepare and activate one multi-buffer change group for BUFFERS."
(let (handle success)
(unwind-protect
(progn
(dolist (buffer buffers)
(unless (buffer-live-p buffer)
(signal 'tp-dead-surface (list buffer)))
(setq handle (nconc handle (prepare-change-group buffer))))
(activate-change-group handle)
(setq success t)
handle)
(unless success (tp--cancel-change-group-safely handle)))))
(defun tp--surface-state-snapshots (prepared-list)
"Return side-state snapshots for PREPARED-LIST."
(mapcar (lambda (prepared)
(cons prepared
(tp--surface-snapshot
(tp--prepared-surface-surface prepared))))
prepared-list))
(defun tp--shadow-object-ids (objects)
"Return stable sorted ids from OBJECTS, a list or path-indexed table."
(let (ids)
(if (hash-table-p objects)
(maphash (lambda (_path object)
(push (tp--surface-object-id object) ids))
objects)
(dolist (object objects)
(push (tp--surface-object-id object) ids)))
(sort ids #'<)))
(defun tp--shadow-ledger-spec-signature (specs)
"Return the deterministic target signature for ledger SPECS."
(mapcar
(lambda (spec)
(list :start (plist-get spec :start)
:end (plist-get spec :end)
:property (plist-get spec :property)
:baseline-present (plist-get spec :baseline-present)
:baseline-value (plist-get spec :baseline-value)
:published-present (plist-get spec :published-present)
:published-value (plist-get spec :published-value)
:anchors (plist-get spec :anchors)))
specs))
(defun tp--shadow-live-ledger-signature (ledger)
"Return the deterministic committed signature for live LEDGER entries."
(mapcar
(lambda (entry)
(list :start (tp--ledger-position (tp--property-ledger-start entry))
:end (tp--ledger-position (tp--property-ledger-end entry))
:property (tp--property-ledger-property entry)
:baseline-present (tp--property-ledger-baseline-present entry)
:baseline-value (tp--property-ledger-baseline-value entry)
:published-present (tp--property-ledger-published-present entry)
:published-value (tp--property-ledger-published-value entry)
:anchors (tp--property-ledger-anchors entry)))
ledger))
(defun tp--shadow-apply-commit-batch-to-string (string batch)
"Return STRING with BATCH patches applied without touching a buffer."
(let ((patches (tp-commit-batch-patches batch)))
(if (null patches)
(copy-sequence string)
(let ((cursor 0) pieces)
(dolist (patch patches)
(push (substring string cursor (plist-get patch :old-start)) pieces)
(push (plist-get patch :replacement) pieces)
(setq cursor (plist-get patch :old-end)))
(push (substring string cursor) pieces)
(apply #'concat (nreverse pieces))))))
(defun tp--shadow-apply-property-operations-to-string
(string base operations)
"Return STRING with absolute property OPERATIONS rebased from BASE."
(let ((result (copy-sequence string)))
(dolist (operation operations)
(let ((start (- (plist-get operation :start) base))
(end (- (plist-get operation :end) base))
(property (plist-get operation :property)))
(if (plist-get operation :present)
(put-text-property start end property
(plist-get operation :value) result)
(remove-list-of-text-properties start end (list property) result))))
result))
(defun tp--shadow-surface-output (surface)
"Return SURFACE's current text and direct properties as one snapshot."
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(with-current-buffer (tp--surface-buffer surface)
(buffer-substring start end))))
(defun tp--shadow-target-output (prepared old-output)
"Return PREPARED's expected publication output from OLD-OUTPUT."
(let ((surface (tp--prepared-surface-surface prepared)))
(if (eq (tp--surface-capability surface) 'content)
(if-let* ((batch (tp--prepared-surface-commit-batch prepared)))
(tp--shadow-apply-commit-batch-to-string old-output batch)
(copy-sequence (tp--prepared-surface-rendered prepared)))
(pcase-let ((`(,start . ,_end) (tp--surface-range surface)))
(tp--shadow-apply-property-operations-to-string
old-output start
(tp--prepared-surface-property-operations prepared))))))
(defun tp--shadow-view-point (views buffer)
"Return BUFFER's numeric point snapshot from transaction VIEWS."
(when-let* ((state (cl-find buffer views :key #'car :test #'eq)))
(marker-position (cadr state))))
(defun tp--shadow-artifact-equal-p (expected actual)
"Return non-nil when EXPECTED and ACTUAL publication artifacts are equal."
(and (eq (plist-get expected :buffer) (plist-get actual :buffer))
(= (plist-get expected :revision) (plist-get actual :revision))
(eq (plist-get expected :plan) (plist-get actual :plan))
(eq (plist-get expected :client-state)
(plist-get actual :client-state))
(equal (plist-get expected :object-ids)
(plist-get actual :object-ids))
(equal (plist-get expected :mount-ids)
(plist-get actual :mount-ids))
(equal (plist-get expected :mounts) (plist-get actual :mounts))
(equal (plist-get expected :ledger) (plist-get actual :ledger))
(= (plist-get expected :point) (plist-get actual :point))
(equal-including-properties
(plist-get expected :output) (plist-get actual :output))))
(defun tp--shadow-current-artifact (surface)
"Return a normalized read-only artifact for current SURFACE state."
(list :buffer (tp--surface-buffer surface)
:revision (tp--surface-revision surface)
:plan (tp--surface-plan surface)
:client-state (tp--surface-client-state surface)
:object-ids (tp--shadow-object-ids (tp--surface-objects surface))
:mount-ids (tp--live-mount-ids surface)
:mounts (tp--live-mount-signature surface)
:ledger (tp--shadow-live-ledger-signature (tp--surface-ledger surface))
:point (with-current-buffer (tp--surface-buffer surface) (point))
:output (tp--shadow-surface-output surface)))
(defun tp--surface-shadow-target-entry
(prepared snapshot journals views batch-id mapping-generation)
"Build a BATCH-ID target view over PREPARED and SNAPSHOT.
JOURNALS and VIEWS are exact references to the shared rollback state.
For real surface entries, the diff slot references PREPARED's exact commit
batch, or nil when publication does not use a commit batch."
(let* ((prepared (tp--assign-prepared-mount-ids prepared))
(surface (tp--prepared-surface-surface prepared))
(buffer (tp--surface-buffer surface))
(old-output (tp--shadow-surface-output surface))
(old-mounts (tp--live-mount-signature surface))
(old-ledger
(tp--shadow-live-ledger-signature
(tp--surface-snapshot-ledger snapshot)))
(context (tp--prepared-surface-context prepared))
(commit-batch (tp--prepared-surface-commit-batch prepared))
(target-mount-ids (tp--prepared-target-mount-ids prepared))
(candidate-id (tp--next-publication-candidate-id))
(authority-token (make-symbol "tp-publication-entry-authority"))
(point (tp--shadow-view-point views buffer))
(commit-expected
(list :buffer buffer
:revision (1+ (tp--surface-snapshot-revision snapshot))
:plan (tp--prepared-surface-plan prepared)
:client-state (tp--prepared-surface-client-state prepared)
:object-ids
(tp--shadow-object-ids
(tp--prepared-surface-objects prepared))
:mount-ids target-mount-ids
:mounts (tp--prepared-mount-signature prepared old-mounts)
:ledger
(tp--shadow-ledger-spec-signature
(tp--prepared-surface-ledger-specs prepared))
:point point
:output (tp--shadow-target-output prepared old-output)))
(rollback-expected
(list :buffer buffer
:revision (tp--surface-snapshot-revision snapshot)
:plan (tp--surface-snapshot-plan snapshot)
:client-state (tp--surface-snapshot-client-state snapshot)
:object-ids
(tp--shadow-object-ids
(tp--surface-snapshot-objects snapshot))
:mount-ids
(mapcar #'tp--ensure-surface-mount-id
(tp--surface-snapshot-mounts snapshot))
:mounts old-mounts
:ledger old-ledger
:point point
:output old-output))
(expected (list :commit commit-expected
:rollback rollback-expected))
(old-revision (tp--surface-snapshot-revision snapshot))
(new-revision (1+ old-revision))
(shadow-validator
(lambda (_entry phase)
(let* ((target (plist-get expected
(if (eq phase 'commit)
:commit
:rollback)))
(actual (tp--shadow-current-artifact surface)))
(list :equivalent (tp--shadow-artifact-equal-p target actual)
:surface-id (tp--surface-id surface)
:expected target :actual actual)))))
(unless
(and (tp-prepare-context-p context)
(tp--context-active context)
(eq (tp--context-surface context) surface)
(= old-revision (tp--surface-revision surface))
(tp--publication-target-entry-arguments-valid-p
tp--transaction-id batch-id candidate-id (tp--surface-id surface)
target-mount-ids buffer
old-revision new-revision authority-token shadow-validator))
(signal 'tp-publication-binding-error
(list :prepared-target prepared snapshot)))
(tp--make-publication-target-entry
:transaction-id tp--transaction-id
:batch-id batch-id
:candidate-id candidate-id
:surface-id (tp--copy-property-value (tp--surface-id surface))
:mount-ids (tp--copy-property-value target-mount-ids)
:buffer buffer
:old-revision old-revision
:new-revision new-revision
:plan (tp--prepared-surface-plan prepared)
:diff commit-batch
:ledger (tp--prepared-surface-ledger-specs prepared)
:objects (tp--prepared-surface-objects prepared)
:ranges (tp--prepared-surface-mount-specs prepared)
:client-state (tp--prepared-surface-client-state prepared)
:rollback-snapshot (vector prepared snapshot journals views)
:authority-token authority-token
:mapping-generation mapping-generation
:shadow-expected expected
:shadow-validator shadow-validator)))
(defun tp--surface-shadow-target-entries
(prepared snapshots journals views batch-id mapping-generation)
"Return BATCH-ID entries for PREPARED using SNAPSHOTS, JOURNALS, and VIEWS."
(mapcar
(lambda (candidate)
(let ((snapshot (cdr (assq candidate snapshots))))
(unless snapshot
(signal 'tp-surface-error (list :missing-shadow-snapshot candidate)))
(tp--surface-shadow-target-entry
candidate snapshot journals views batch-id mapping-generation)))
prepared))
(defun tp--surface-validate-publication-entry (candidate entry)
"Return ENTRY's exact prepared binding after validating CANDIDATE authority."
(let* ((rollback (tp-publication-target-entry-rollback-snapshot entry))
(prepared (and (vectorp rollback) (= (length rollback) 4)
(aref rollback 0)))
(snapshot (and prepared (aref rollback 1)))
(surface (and (tp--prepared-surface-p prepared)
(tp--prepared-surface-surface prepared)))
(context (and surface (tp--prepared-surface-context prepared)))
(commit-batch
(and surface (tp--prepared-surface-commit-batch prepared)))
(expected (and surface
(tp-publication-target-entry-shadow-expected entry)))
(commit-expected (and expected (plist-get expected :commit)))
(rollback-expected (and expected (plist-get expected :rollback))))
(unless
(and surface (tp--surface-snapshot-p snapshot)
(tp-prepare-context-p context)
(tp--context-active context)
(eq (tp--context-surface context) surface)
(tp--publication-target-entry-bound-p
entry
(tp-publication-batch-candidate-transaction-id candidate)
(tp-publication-batch-candidate-id candidate))
(equal (tp-publication-target-entry-surface-id entry)
(tp--surface-id surface))
(eq (tp-publication-target-entry-buffer entry)
(tp--surface-buffer surface))
(= (tp-publication-target-entry-old-revision entry)
(tp--surface-snapshot-revision snapshot))
(= (tp-publication-target-entry-old-revision entry)
(tp--surface-revision surface))
(= (tp-publication-target-entry-new-revision entry)
(1+ (tp--surface-snapshot-revision snapshot)))
(eq (tp-publication-target-entry-plan entry)
(tp--prepared-surface-plan prepared))
(eq (tp-publication-target-entry-ledger entry)
(tp--prepared-surface-ledger-specs prepared))
(eq (tp-publication-target-entry-objects entry)
(tp--prepared-surface-objects prepared))
(eq (tp-publication-target-entry-ranges entry)
(tp--prepared-surface-mount-specs prepared))
(eq (tp-publication-target-entry-client-state entry)
(tp--prepared-surface-client-state prepared))
(eq (tp-publication-target-entry-diff entry) commit-batch)
(or
(null commit-batch)
(and
(= (tp-publication-target-entry-old-revision entry)
(tp-commit-batch-base-revision commit-batch))
(= (tp-publication-target-entry-new-revision entry)
(tp-commit-batch-target-revision commit-batch))
(= (tp-commit-batch-base-extent commit-batch)
(length (plist-get rollback-expected :output)))
(= (tp-commit-batch-target-extent commit-batch)
(length (plist-get commit-expected :output)))))
(= (tp-publication-target-entry-mapping-generation entry)
tp--surface-transaction-id))
(signal 'tp-publication-binding-error
(list :prepared-entry entry prepared)))
prepared))
(defun tp--surface-stage-publication-entries (candidate)
"Publish exactly CANDIDATE's entry-bound prepared surfaces once each."
(let ((generation
(tp-publication-target-entry-mapping-generation
(car (tp-publication-batch-candidate-entries candidate)))))
(unless (= generation (1+ tp--surface-transaction-id))
(signal 'tp-publication-binding-error
(list :mapping-generation generation
tp--surface-transaction-id)))
(setq tp--surface-transaction-id generation))
(dolist (entry (tp-publication-batch-candidate-entries candidate))
(let ((prepared (tp--surface-validate-publication-entry candidate entry)))
(tp--publish-one-surface prepared)
(tp--surface-record-publication-operation-counts prepared))))
(defun tp--surface-record-publication-operation-counts (prepared)
"Record PREPARED's actual live report counts into its exact batch entry."
(when tp--transaction-publication-batch
(let* ((surface (tp--prepared-surface-surface prepared))
(entry
(cl-find
(tp--surface-id surface)
(tp-publication-batch-candidate-entries
tp--transaction-publication-batch)
:key #'tp-publication-target-entry-surface-id :test #'equal))
(report (tp--prepared-surface-report prepared)))
(unless (and entry report)
(signal 'tp-publication-binding-error
(list :missing-publication-report (tp--surface-id surface))))
(setf
(tp-publication-target-entry-operation-counts entry)
(list :surface-id (tp--surface-id surface)
:old-revision (plist-get report :old-revision)
:new-revision (plist-get report :new-revision)
:mapping-generation tp--surface-transaction-id
:text-operations (or (plist-get report :text-operations) 0)
:property-operations
(or (plist-get report :property-operations) 0)
:touched-characters (or (plist-get report :touched-characters) 0))))))
(defun tp--surface-publish-transaction ()
"Prepare journals and publish every queued changed surface atomically."
(let* ((state (tp--surface-extension-state))
(table (gethash 'prepared state))
(prepared (and table (tp--sorted-prepared-surfaces table))))
(when prepared
(let* ((buffers (tp--prepared-buffers prepared))
(journals (tp--capture-property-journals prepared))
(views (tp--capture-view-state buffers))
snapshots group
(next-generation (1+ tp--surface-transaction-id)))
(puthash 'changed prepared state)
(puthash 'journals journals state)
(puthash 'views views state)
(setq snapshots (tp--surface-state-snapshots prepared))
(puthash 'snapshots snapshots state)
(setq group (tp--prepare-change-group-for-buffers buffers))
(puthash 'change-group group state)
(puthash 'surface-transaction-id-before
tp--surface-transaction-id state)
(let* ((batch-id (tp--next-publication-batch-id))
(entries
(tp--surface-shadow-target-entries
prepared snapshots journals views batch-id
next-generation)))
(tp--transaction-begin-publication-batch
batch-id entries state 'tp--surface-stage-publication-entries)
(tp--transaction-batch-transition 'staged))
(let ((tp--surface-publishing t))
(tp--publication-batch-execute-stage
tp--transaction-publication-batch))))))
(defun tp--surface-precommit-step (step state)
"Report precommit STEP for transaction STATE."
(when tp--surface-precommit-step-function
(funcall tp--surface-precommit-step-function step state)))
(defun tp--validate-retained-batch-precommit (prepared snapshot)
"Validate PREPARED retained batch against committed SNAPSHOT identities."
(let* ((surface (tp--prepared-surface-surface prepared))
(batch (tp--prepared-surface-commit-batch prepared))
(mounts (tp--surface-mounts surface))
(mount-index (tp--surface-mount-index surface)))
(tp--validate-live-surface surface)
(unless (and batch
(eq (tp--surface-plan surface)
(tp--prepared-surface-plan prepared))
(eq (tp--surface-objects surface)
(tp--surface-snapshot-objects snapshot))
(eq mounts (tp--surface-snapshot-mounts snapshot))
(eq mount-index
(tp--surface-snapshot-mount-index snapshot))
(eq (tp--surface-index surface) mounts)
(eq (tp--prepared-surface-live-mounts prepared) mounts)
(eq (tp--prepared-surface-live-mount-index prepared)
mount-index)
(= (tp--surface-revision surface)
(tp-commit-batch-target-revision batch))
(not (tp--context-active
(tp--prepared-surface-context prepared)))
(plist-get (tp--surface-report surface) :commit-batch)
(plist-get (tp--surface-report surface)
:retained-mount-state))
(signal 'tp-surface-error
(list :invalid-retained-batch-precommit
(tp--surface-id surface))))))
(defun tp--validate-surface-precommit (state)
"Validate final published semantics and rollback ownership in STATE."
(unless (and (gethash 'change-group state)
(listp (gethash 'changed state))
(listp (gethash 'snapshots state))
(listp (gethash 'views state)))
(signal 'tp-surface-error (list :invalid-precommit-state)))
(dolist (view (gethash 'views state))
(unless (and (bufferp (nth 0 view))
(markerp (nth 1 view))
(cl-every (lambda (entry) (markerp (cdr entry)))
(nth 3 view)))
(signal 'tp-surface-error (list :invalid-view-cleanup view))))
(dolist (entry (gethash 'snapshots state))
(unless (and (tp--prepared-surface-p (car entry))
(tp--surface-snapshot-p (cdr entry)))
(signal 'tp-surface-error (list :invalid-surface-cleanup entry)))
(let ((prepared (car entry))
(snapshot (cdr entry)))
(if (tp--prepared-surface-retained-mount-state-p prepared)
(tp--validate-retained-batch-precommit prepared snapshot)
(let* ((surface (tp--prepared-surface-surface prepared))
(plan (tp--surface-plan surface))
(objects (tp--surface-objects surface))
(mounts (tp--surface-mounts surface))
(mount-index (tp--surface-mount-index surface))
(capability (tp--surface-capability surface))
(mount-set (make-hash-table :test #'eq))
(indexed-mount-set (make-hash-table :test #'eq)))
(tp--validate-live-surface surface)
(unless (eq plan (tp--prepared-surface-plan prepared))
(signal 'tp-surface-error (list :invalid-published-plan surface)))
(tp--validate-surface-plan-tree plan)
(tp--validate-plan-capability plan capability)
(tp--validate-context-tree (tp--prepared-surface-context prepared) plan)
(unless (and (hash-table-p objects) (hash-table-p mount-index)
(eq (tp--surface-index surface) mounts))
(signal 'tp-surface-error (list :invalid-published-index surface)))
(maphash
(lambda (path object)
(unless (and (tp-object-p object)
(equal path (tp--surface-object-path object))
(eq (tp--surface-object-surface object) surface)
(tp--surface-object-live object)
(not (tp--surface-object-disposed object))
(null (tp--surface-object-candidate-context object)))
(signal 'tp-surface-error
(list :invalid-published-object path object))))
objects)
(dolist (mount mounts)
(when (gethash mount mount-set)
(signal 'tp-surface-error
(list :duplicate-published-mount mount)))
(puthash mount t mount-set))
(maphash
(lambda (object entries)
(unless (and (tp-object-p object)
(eq (gethash (tp--surface-object-path object) objects)
object))
(signal 'tp-surface-error
(list :invalid-mount-index-owner object)))
(dolist (mount entries)
(unless (and (gethash mount mount-set)
(eq (tp--surface-mount-object mount) object)
(not (gethash mount indexed-mount-set)))
(signal 'tp-surface-error
(list :invalid-mount-index-entry object mount)))
(puthash mount t indexed-mount-set)))
mount-index)
(unless (= (hash-table-count mount-set)
(hash-table-count indexed-mount-set))
(signal 'tp-surface-error (list :incomplete-mount-index surface)))
(dolist (mount mounts)
(let ((object (and (tp--surface-mount-p mount)
(tp--surface-mount-object mount))))
(unless (and object
(eq (gethash (tp--surface-object-path object) objects)
object)
(eq (tp--surface-mount-capability mount) capability)
(gethash mount indexed-mount-set))
(signal 'tp-surface-error
(list :invalid-published-mount mount)))
(tp--validate-buffer-range
(tp--surface-buffer surface)
(tp--surface-mount-start mount)
(tp--surface-mount-end mount))
(if (eq capability 'properties)
(let ((anchor (tp--surface-mount-anchor mount)))
(tp--validate-anchor anchor surface)
(unless (and (eq (tp--surface-mount-start mount)
(tp--anchor-start anchor))
(eq (tp--surface-mount-end mount)
(tp--anchor-end anchor)))
(signal 'tp-surface-error
(list :invalid-published-anchor mount))))
(when (tp--surface-mount-anchor mount)
(signal 'tp-surface-error
(list :invalid-content-anchor mount)))))))))
state))
(defun tp--surface-prevalidate-cleanup (state)
"Validate marker-backed cleanup lists retained in surface STATE."
(dolist (entry (gethash 'snapshots state))
(let ((prepared (car entry)))
(unless (tp--prepared-surface-retained-mount-state-p prepared)
(let* ((snapshot (cdr entry))
(surface (tp--prepared-surface-surface prepared))
(coordinate-p
(plist-get (tp--surface-options surface) :coordinate-mounts)))
(dolist (mount (tp--surface-snapshot-mounts snapshot))
(when (eq (tp--surface-mount-capability mount) 'content)
(unless (or (and (markerp (tp--surface-mount-start mount))
(markerp (tp--surface-mount-end mount)))
(and coordinate-p
(integerp (tp--surface-mount-start mount))
(integerp (tp--surface-mount-end mount))))
(signal 'tp-surface-error
(list :invalid-mount-cleanup mount)))))
(dolist (ledger (tp--surface-snapshot-ledger snapshot))
(unless (and (markerp (tp--property-ledger-start ledger))
(markerp (tp--property-ledger-end ledger)))
(signal 'tp-surface-error
(list :invalid-ledger-cleanup ledger))))))))
state)
(defun tp--surface-final-accept ()
"Accept the active surface transaction's prepared change group."
(when-let* ((state (tp--transaction-extension tp--surface-extension-key)))
(when-let* ((group (gethash 'change-group state)))
(accept-change-group group))))
(defun tp--surface-precommit-transaction ()
"Prepare every fallible surface transition before signal commit."
(when-let* ((state (tp--transaction-extension tp--surface-extension-key)))
(when-let* ((table (gethash 'prepared state)))
(let ((all-prepared (tp--all-prepared-surfaces table))
(changed (gethash 'changed state)))
(when changed
(tp--surface-precommit-step 'validate state)
(tp--validate-surface-precommit state)
(tp--surface-precommit-step 'view state)
(tp--restore-view-state (gethash 'views state) t)
(tp--surface-precommit-step 'anchors state)
(let (anchors)
(dolist (entry (gethash 'snapshots state))
(let* ((prepared (car entry))
(snapshot (cdr entry))
(surface (tp--prepared-surface-surface prepared)))
(unless (tp--prepared-surface-retained-mount-state-p prepared)
(dolist (mounts
(list (tp--surface-snapshot-mounts snapshot)
(tp--surface-mounts surface)))
(dolist (mount mounts)
(when-let* ((anchor (tp--surface-mount-anchor mount)))
(push anchor anchors)))))))
(puthash 'anchor-undo
(tp--capture-anchor-ownership-undo (nreverse anchors))
state))
(dolist (entry (gethash 'snapshots state))
(let* ((prepared (car entry))
(snapshot (cdr entry))
(surface (tp--prepared-surface-surface prepared)))
(unless (tp--prepared-surface-retained-mount-state-p prepared)
(tp--apply-anchor-ownership
surface (tp--surface-snapshot-mounts snapshot)
(tp--surface-mounts surface))))))
(tp--surface-precommit-step 'lifecycle state)
(dolist (prepared all-prepared)
(tp--finalize-object-lifecycle prepared)
(tp--surface-precommit-step 'lifecycle-finalized state))
(when changed
(tp--surface-precommit-step 'observers state)
(dolist (prepared changed)
(tp--enqueue-surface-observers
(tp--prepared-surface-surface prepared)))
(tp--surface-precommit-step 'cleanup state)
(tp--surface-prevalidate-cleanup state)
(tp--transaction-install-final-accept
#'tp--surface-final-accept))))))
(defun tp--cancel-change-group-safely (group)
"Cancel active change GROUP without allowing quit to interrupt rollback."
(when group
(let ((inhibit-quit t)
(inhibit-modification-hooks t)
(tp--surface-publishing t))
(condition-case nil
(cancel-change-group group)
((error quit) nil)))))
(defun tp--cancel-change-group-for-rollback (group)
"Cancel active change GROUP while allowing failure aggregation."
(when group
(let ((inhibit-quit t)
(inhibit-modification-hooks t)
(tp--surface-publishing t))
(cancel-change-group group))))
(defun tp--surface-rollback-transaction ()
"Rollback buffers, side state, markers, and contexts for this transaction."
(when-let* ((state (tp--transaction-extension tp--surface-extension-key)))
(let (failures)
(cl-labels
((attempt (owner function)
(condition-case failure
(funcall function)
((error quit)
(push (list owner failure) failures)))))
(attempt 'change-group
(lambda ()
(tp--cancel-change-group-for-rollback
(gethash 'change-group state))))
(attempt 'properties
(lambda ()
(tp--restore-property-journals
(gethash 'journals state))))
(dolist (entry (gethash 'snapshots state))
(attempt
(list 'surface
(tp--surface-id
(tp--prepared-surface-surface (car entry))))
(lambda ()
(tp--restore-surface-snapshot (car entry) (cdr entry)))))
(attempt 'view
(lambda ()
(tp--restore-view-state (gethash 'views state))))
(attempt 'anchors
(lambda ()
(tp--undo-anchor-ownership
(gethash 'anchor-undo state))))
(attempt 'surface-counter
(lambda ()
(when (gethash 'surface-transaction-id-before state)
(setq tp--surface-transaction-id
(gethash 'surface-transaction-id-before state))))))
(when failures
(signal 'tp-surface-rollback-error
(list :failures (nreverse failures)))))))
(defun tp--surface-finalize-killed-rollback ()
"Dispose candidate contexts and reapply killed-buffer teardown."
(when-let* ((state (tp--transaction-extension tp--surface-extension-key)))
(let (failures)
(when-let* ((table (gethash 'prepared state)))
(dolist (prepared (tp--all-prepared-surfaces table))
(let ((surface (tp--prepared-surface-surface prepared)))
(condition-case failure
(let ((context (tp--prepared-surface-context prepared)))
(setf (tp--context-active context) t)
(tp--discard-context context)
(unless (buffer-live-p (tp--surface-buffer surface))
(tp--teardown-surface surface t)))
((error quit)
(push (list (tp--surface-id surface) failure)
failures))))))
(when failures
(signal 'tp-surface-rollback-error
(list :finalize-failures (nreverse failures)))))))
(defun tp--anchors-in-mounts (mounts)
"Return distinct range anchors referenced by MOUNTS."
(let ((seen (make-hash-table :test #'eq)) anchors)
(dolist (mount mounts)
(when-let* ((anchor (tp--surface-mount-anchor mount)))
(unless (gethash anchor seen)
(puthash anchor t seen)
(push anchor anchors))))
(nreverse anchors)))
(defun tp--capture-anchor-ownership-undo (anchors)
"Capture exact ownership undo entries for opaque ANCHORS."
(let ((seen (make-hash-table :test #'eq)) undo)
(dolist (anchor anchors)
(unless (gethash anchor seen)
(puthash anchor t seen)
(push (list anchor (copy-sequence (tp--anchor-surfaces anchor))
(tp--anchor-candidate-context anchor))
undo)))
(nreverse undo)))
(defun tp--apply-anchor-ownership (surface old-mounts new-mounts)
"Apply SURFACE ownership from OLD-MOUNTS to NEW-MOUNTS."
(let ((old (tp--anchors-in-mounts old-mounts))
(new (tp--anchors-in-mounts new-mounts))
(new-set (make-hash-table :test #'eq)))
(dolist (anchor new) (puthash anchor t new-set))
(dolist (anchor old)
(unless (gethash anchor new-set)
(setf (tp--anchor-surfaces anchor)
(delq surface (tp--anchor-surfaces anchor)))))
(dolist (anchor new)
(cl-pushnew surface (tp--anchor-surfaces anchor) :test #'eq)
(setf (tp--anchor-candidate-context anchor) nil))))
(defun tp--undo-anchor-ownership (undo)
"Restore anchors from ownership UNDO entries."
(dolist (entry undo)
(setf (tp--anchor-surfaces (nth 0 entry)) (nth 1 entry)
(tp--anchor-candidate-context (nth 0 entry)) (nth 2 entry))))
(defun tp--finalize-object-lifecycle (prepared)
"Delete omitted bindings and removed objects after PREPARED commits."
(let* ((context (tp--prepared-surface-context prepared))
(objects (tp--prepared-surface-objects prepared))
(omitted (tp--context-binding-removals context objects)))
(dolist (binding omitted)
(when (tp-binding-live-p binding) (tp-binding-dispose binding)))
(dolist (object (tp--prepared-surface-removed prepared))
(tp-binding-dispose-owner object)
(setf (tp--surface-object-live object) nil
(tp--surface-object-disposed object) t))
(setf (tp--context-active context) nil)))
(defun tp--record-observer-error (surface observer failure)
"Record OBSERVER FAILURE in SURFACE's latest report."
(let ((report (tp--copy-property-value (tp--surface-report surface))))
(setq report
(plist-put report :observer-errors
(append (plist-get report :observer-errors)
(list (list :observer observer :error failure)))))
(setf (tp--surface-report surface) report)))
(defun tp--record-cleanup-error (surface owner failure)
"Record contained cleanup OWNER FAILURE on SURFACE."
(let ((report (tp--copy-property-value (tp--surface-report surface))))
(setq report
(plist-put report :cleanup-errors
(append (plist-get report :cleanup-errors)
(list (list :owner owner :error failure)))))
(setf (tp--surface-report surface) report)))
(defun tp--run-surface-observers (surface observers report)
"Run SURFACE OBSERVERS with REPORT and record their errors."
(dolist (observer observers)
(let ((inhibit-quit t)
(quit-flag nil))
(condition-case failure
(funcall observer surface report)
((error quit)
(tp--record-observer-error surface observer failure))))))
(defun tp--enqueue-surface-observers (surface)
"Schedule SURFACE observers outside the publishing transaction."
(when-let* ((observers (tp--surface-observers surface)))
(let ((observers (copy-sequence observers))
(report (tp-surface-report surface)))
(tp--enqueue-after-commit
(lambda () (tp--run-surface-observers surface observers report))))))
(defun tp--surface-commit-transaction ()
"Run contained postaccept cleanup for every transaction surface."
(when-let* ((state (tp--transaction-extension tp--surface-extension-key)))
(let ((changed (gethash 'changed state))
(snapshots (gethash 'snapshots state)))
(cl-labels
((cleanup (surface owner function)
(let ((inhibit-quit t)
(quit-flag nil))
(condition-case failure
(progn
(when tp--surface-cleanup-step-function
(funcall tp--surface-cleanup-step-function
surface owner))
(funcall function))
((error quit)
(tp--record-cleanup-error surface owner failure))))))
(when-let* ((prepared (car changed)))
(cleanup
(tp--prepared-surface-surface prepared) 'view-markers
(lambda () (tp--dispose-view-state (gethash 'views state)))))
(dolist (entry snapshots)
(let* ((prepared (car entry))
(snapshot (cdr entry))
(surface (tp--prepared-surface-surface prepared)))
(unless (tp--prepared-surface-retained-mount-state-p prepared)
(dolist (anchor
(tp--anchors-in-mounts
(tp--surface-snapshot-mounts snapshot)))
(when (null (tp--anchor-surfaces anchor))
(cleanup
surface (list 'old-anchor (tp--anchor-id anchor))
(lambda () (tp--dispose-anchor anchor)))))
(cleanup
surface 'old-mounts
(lambda ()
(tp--dispose-content-mounts
(tp--surface-snapshot-mounts snapshot))))
(cleanup
surface 'old-ledger
(lambda ()
(tp--dispose-ledger
(tp--surface-snapshot-ledger snapshot)))))))
(when-let* ((table (gethash 'prepared state)))
(dolist (prepared (tp--all-prepared-surfaces table))
(let* ((surface (tp--prepared-surface-surface prepared))
(context (tp--prepared-surface-context prepared)))
(dolist (anchor (tp--context-created-anchors context))
(when (null (tp--anchor-surfaces anchor))
(cleanup
surface (list 'candidate-anchor (tp--anchor-id anchor))
(lambda () (tp--dispose-anchor anchor))))))))))))
(defun tp-surface-materialize-string (plan-or-producer)
"Materialize PLAN-OR-PRODUCER without creating live runtime state."
(let* ((surface (tp--make-surface
:id 0 :capability 'content :options nil
:objects (make-hash-table :test #'equal)))
(context (tp--make-context surface t))
(success nil)
rendered)
(unwind-protect
(let* ((normalized
(tp--prepare-input surface plan-or-producer nil context))
(plan (car normalized))
(owned-p (nth 2 normalized)))
(tp--validate-plan-capability plan 'content)
(tp--validate-context-tree context plan)
(setq rendered (car (tp--render-plan plan context owned-p))
success t)
rendered)
(setf (tp--context-active context) t)
(tp--discard-context context)
(unless success (setq rendered nil)))))
(defun tp--rebase-ledger-entry (surface entry)
"Return current-property segments rebased from ledger ENTRY on SURFACE."
(let* ((buffer (tp--surface-buffer surface))
(start (tp--ledger-position (tp--property-ledger-start entry)))
(end (tp--ledger-position (tp--property-ledger-end entry)))
(property (tp--property-ledger-property entry))
segments)
(while (< start end)
(let* ((next (with-current-buffer buffer
(next-single-property-change start property buffer end)))
(state (tp--property-state-at buffer start property)))
(push (tp--make-property-ledger
:start (with-current-buffer buffer (copy-marker start nil))
:end (with-current-buffer buffer (copy-marker next t))
:property property :baseline-present (car state)
:baseline-value (cdr state) :published-present (car state)
:published-value (cdr state)
:anchors (tp--property-ledger-anchors entry))
segments)
(setq start next)))
(tp--dispose-marker (tp--property-ledger-start entry))
(tp--dispose-marker (tp--property-ledger-end entry))
(nreverse segments)))
(defun tp-range-rebase (anchor)
"Accept current host properties as the new baseline for ANCHOR."
(unless (tp-range-anchor-live-p anchor)
(signal 'tp-invalid-range-anchor (list anchor)))
(dolist (surface (copy-sequence (tp--anchor-surfaces anchor)))
(when (tp-surface-live-p surface)
(let (ledger)
(dolist (entry (tp--surface-ledger surface))
(setq ledger
(nconc ledger
(if (memq anchor (tp--property-ledger-anchors entry))
(tp--rebase-ledger-entry surface entry)
(list entry)))))
(setf (tp--surface-ledger surface) ledger))))
(setf (tp--anchor-stale anchor) nil)
anchor)
(defun tp--unmount-ledger-segments (surface entry)
"Return SURFACE restoration operations and conflicts for ledger ENTRY."
(let* ((buffer (tp--surface-buffer surface))
(start (tp--ledger-position (tp--property-ledger-start entry)))
(end (tp--ledger-position (tp--property-ledger-end entry)))
(property (tp--property-ledger-property entry))
(published (tp--ledger-published-state entry))
operations conflicts)
(while (< start end)
(let* ((next (with-current-buffer buffer
(next-single-property-change start property buffer end)))
(current (tp--property-state-at buffer start property)))
(if (tp--property-state-equal-p current published)
(push (list :start start :end next :property property
:present (tp--property-ledger-baseline-present entry)
:value (tp--property-ledger-baseline-value entry))
operations)
(push (list :start start :end next :property property
:current current :published published)
conflicts))
(setq start next)))
(cons (nreverse operations) (nreverse conflicts))))
(defun tp--unmount-property-operations (surface)
"Return restoration operations and conflicts for SURFACE."
(let (operations conflicts)
(dolist (entry (tp--surface-ledger surface))
(pcase-let ((`(,entry-operations . ,entry-conflicts)
(tp--unmount-ledger-segments surface entry)))
(setq operations (nconc operations entry-operations)
conflicts (nconc conflicts entry-conflicts))))
(cons operations conflicts)))
(defun tp--unmount-journals (surface operations)
"Capture property rollback journals for SURFACE and OPERATIONS."
(let ((entries
(if (eq (tp--surface-capability surface) 'content)
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(list (list (tp--surface-buffer surface) start end)))
(mapcar (lambda (operation)
(list (tp--surface-buffer surface)
(plist-get operation :start)
(plist-get operation :end)))
operations))))
(mapcar
(lambda (entry)
(pcase-let ((`(,buffer ,start ,end) entry))
(list buffer start end
(with-current-buffer buffer (buffer-substring start end)))))
(tp--merge-buffer-intervals entries))))
(defun tp--publish-unmount (surface operations)
"Remove SURFACE content or apply property restoration OPERATIONS."
(let ((buffer (tp--surface-buffer surface))
(tp--surface-publishing t))
(with-current-buffer buffer
(save-restriction
(widen)
(let ((inhibit-read-only
(plist-get (tp--surface-options surface) :inhibit-read-only)))
(if (eq (tp--surface-capability surface) 'content)
(pcase-let ((`(,start . ,end) (tp--surface-range surface)))
(delete-region start end))
(with-silent-modifications
(dolist (operation operations)
(tp--apply-property-operation buffer operation)))))))))
(defun tp--remove-surface-anchor-ownership (surface)
"Detach SURFACE from every range anchor it owns."
(dolist (anchor (tp--anchors-in-mounts (tp--surface-mounts surface)))
(setf (tp--anchor-surfaces anchor)
(delq surface (tp--anchor-surfaces anchor)))
(unless (tp--anchor-surfaces anchor) (tp--dispose-anchor anchor))))
(defun tp--teardown-surface (surface killed)
"Release SURFACE runtime state; KILLED means its buffer is gone."
(when (tp-surface-p surface)
(tp--unregister-surface surface)
(tp-binding-dispose-owner surface)
(when (tp--surface-objects surface)
(maphash
(lambda (_path object)
(tp-binding-dispose-owner object)
(setf (tp--surface-object-live object) nil
(tp--surface-object-disposed object) t))
(tp--surface-objects surface)))
(tp--remove-surface-anchor-ownership surface)
(tp--dispose-content-mounts (tp--surface-mounts surface))
(tp--dispose-ledger (tp--surface-ledger surface))
(tp--dispose-marker (tp--surface-start surface))
(tp--dispose-marker (tp--surface-end surface))
(setf (tp--surface-live surface) nil
(tp--surface-stale surface) (and killed 'killed)
(tp--surface-objects surface) (make-hash-table :test #'equal)
(tp--surface-mounts surface) nil (tp--surface-index surface) nil
(tp--surface-mount-index surface) (make-hash-table :test #'eq)
(tp--surface-ledger surface) nil
(tp--surface-client-state surface) nil
(tp--surface-observers surface) nil)))
;;;###autoload
(defun tp-surface-unmount (surface)
"Unmount SURFACE and return a generic report.
Properties-only mounts restore their baseline only when TP's last value still
owns the property; conflicting host values are preserved and reported."
(tp--validate-live-surface surface)
(when tp--transaction-active
(signal 'tp-surface-error (list :unmount-during-transaction)))
(let* ((property-result
(if (eq (tp--surface-capability surface) 'properties)
(tp--unmount-property-operations surface)
(cons nil nil)))
(operations (car property-result))
(conflicts (cdr property-result))
(journals (tp--unmount-journals surface operations))
(views (tp--capture-view-state (list (tp--surface-buffer surface))))
(group (tp--prepare-change-group-for-buffers
(list (tp--surface-buffer surface))))
success)
(unwind-protect
(progn
(tp--publish-unmount surface operations)
(accept-change-group group)
(setq success t))
(unless success
(tp--cancel-change-group-safely group)
(tp--restore-property-journals journals))
(tp--restore-view-state views))
(let ((report (list :transaction-id (cl-incf tp--surface-transaction-id)
:surface-id (tp--surface-id surface)
:old-revision (tp--surface-revision surface)
:new-revision nil :text-operations
(if (eq (tp--surface-capability surface) 'content) 1 0)
:property-operations (length operations)
:property-conflicts conflicts :unmounted t)))
(tp--teardown-surface surface nil)
report)))
(defun tp-surface-at-point (&optional position buffer)
"Return live objects mounted at POSITION in BUFFER from side indexes."
(let ((target (if buffer (get-buffer buffer) (current-buffer)))
objects)
(unless (buffer-live-p target)
(signal 'tp-unsupported-buffer (list buffer)))
(with-current-buffer target
(let ((point (or position (point))))
(dolist (surface tp--buffer-surfaces)
(dolist (mount (tp--surface-index surface))
(let ((start (tp--mount-position (tp--surface-mount-start mount)))
(end (tp--mount-position (tp--surface-mount-end mount))))
(when (and start end (<= start point) (< point end))
(cl-pushnew (tp--surface-mount-object mount) objects
:test #'eq)))))))
(sort objects
(lambda (left right)
(< (length (tp--surface-object-path left))
(length (tp--surface-object-path right)))))))
(defun tp-surface-report (surface)
"Return a defensive copy of SURFACE's latest generic commit report."
(unless (tp-surface-p surface)
(signal 'wrong-type-argument (list 'tp-surface-p surface)))
(tp--copy-property-value (tp--surface-report surface)))
(defun tp-surface-report-summary (surface)
"Return scalar commit metrics for SURFACE without copying its full report.
The returned plist contains only numbers, symbols, booleans, or nil. Use
`tp-surface-report' when callers need arbitrary report payloads or a defensive
snapshot of nested values."
(unless (tp-surface-p surface)
(signal 'wrong-type-argument (list 'tp-surface-p surface)))
(let ((report (tp--surface-report surface)))
(list :transaction-id (plist-get report :transaction-id)
:text-operations (plist-get report :text-operations)
:property-operations (plist-get report :property-operations)
:full-root (plist-get report :full-root)
:scope-count (plist-get report :scope-count)
:scope-range-count (plist-get report :scope-range-count)
:scope-fallback (plist-get report :scope-fallback)
:reconciled-objects (plist-get report :reconciled-objects)
:created-objects (plist-get report :created-objects)
:removed-objects (plist-get report :removed-objects)
:moved-objects (plist-get report :moved-objects))))
(defun tp-surface-inspect (surface)
"Return read-only retained diagnostics for SURFACE."
(tp--validate-live-surface surface)
(list :surface surface :id (tp--surface-id surface)
:buffer (tp--surface-buffer surface)
:capability (tp--surface-capability surface)
:revision (tp--surface-revision surface)
:object-count (hash-table-count (tp--surface-objects surface))
:mount-count (length (tp--surface-mounts surface))
:client-state (tp--surface-client-state surface)
:report (tp-surface-report surface)))
(defun tp--watch-producer (anchor compute)
"Return a properties producer backed by ANCHOR and COMPUTE."
(lambda (context)
(let* ((object (tp-object-ensure
context nil 'watch 'text-properties))
(binding
(tp-bind object '(tp/watch . declarations)
(lambda ()
(tp--project-text-declarations (funcall compute))))))
(tp-object-attach-range context object anchor)
(tp-surface-plan-create
:key 'watch :kind 'text-properties
:props (tp-binding-read binding) :capability 'properties))))
;;;###autoload
(defun tp-watch (buffer start end compute)
"Reactively apply declarations from COMPUTE to BUFFER from START to END.
COMPUTE is a zero-argument function returning native text declarations.
Return the underlying properties-only surface for inspection or unmounting."
(unless (functionp compute)
(signal 'wrong-type-argument (list 'functionp compute)))
(let ((anchor (tp-range-anchor-create buffer start end)) surface)
(unwind-protect
(setq surface
(tp-surface-mount
buffer (tp--watch-producer anchor compute)
'(:capability properties :inhibit-read-only t)))
(unless surface (tp--dispose-anchor anchor)))
surface))
(add-hook 'tp--binding-changed-functions #'tp--surface-binding-changed)
(add-hook 'tp--transaction-publish-functions #'tp--surface-publish-transaction)
(tp--transaction-register-precommit-function
'tp--surface-precommit-transaction)
(add-hook 'tp--transaction-rollback-functions #'tp--surface-rollback-transaction)
(add-hook 'tp--transaction-rollback-final-functions
#'tp--surface-finalize-killed-rollback)
(add-hook 'tp--transaction-committed-functions #'tp--surface-commit-transaction)
(provide 'tp-surface)
;;; tp-surface.el ends here