Replace the legacy managed layer renderer with one independent retained/reactive text runtime. TP now owns exact dependencies, stable objects, marker-backed mounts, property contribution composition, atomic publication, rollback, and direct text-property facades without ECSS or Ebox dependencies.\n\nBREAKING CHANGE: remove tp-render, tp-stack, scan-driven managed layers, inline runtime metadata, TP-owned CSS cascade APIs, and dollar-variable declarations.\n\nVerified: 290/290 ERT, shuffled 290/290 (seed 20260806), 8/8 doctests, WERROR compile-all, checkdoc, package-lint, diff-check, and isolated TP-only load.
2457 lines
110 KiB
EmacsLisp
2457 lines
110 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-producer-buffer-mutation
|
|
"TP producer mutated a live surface buffer during prepare"
|
|
'tp-surface-error)
|
|
(define-error 'tp-scope-mismatch "Scoped TP update changed outside its objects"
|
|
'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)))
|
|
"A producer result carrying PLAN and opaque CLIENT-STATE."
|
|
plan client-state)
|
|
|
|
(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
|
|
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))
|
|
object start end tags capability anchor)
|
|
|
|
(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)
|
|
|
|
(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--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-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--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--copy-surface-plan (plan)
|
|
"Return a validated defensive copy of PLAN."
|
|
(unless (tp-surface-plan-p plan)
|
|
(signal 'wrong-type-argument (list 'tp-surface-plan-p plan)))
|
|
(let* ((children (mapcar #'tp--copy-surface-plan
|
|
(tp-surface-plan-children plan)))
|
|
(kind (tp--copy-property-value (tp-surface-plan-kind plan)))
|
|
(text (tp--copy-property-value (tp-surface-plan-text plan)))
|
|
(props (tp--copy-property-value (tp-surface-plan-props plan)))
|
|
(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))
|
|
:kind kind :text text :props props :children children
|
|
:tags (tp--copy-property-value (tp-surface-plan-tags plan))
|
|
: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)))
|
|
|
|
(defun tp-surface-result-create (plan &optional client-state)
|
|
"Return a producer result containing PLAN and opaque CLIENT-STATE."
|
|
(tp--make-surface-result (tp--copy-surface-plan plan) client-state))
|
|
|
|
(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-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-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 (marker-position (tp--surface-mount-start mount))
|
|
:end (marker-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)
|
|
: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))
|
|
(gethash object (tp--context-touched 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-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--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)
|
|
"Return a fresh plist where CHILD values override PARENT values."
|
|
(let ((result (tp--copy-property-value parent)))
|
|
(cl-loop for (property value) on child by #'cddr
|
|
do (setq result
|
|
(plist-put result property
|
|
(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)
|
|
"Return PLAN's propertized string and side records in CONTEXT."
|
|
(let (records)
|
|
(cl-labels
|
|
((walk (node path inherited offset)
|
|
(let* ((props (tp--plist-overlay
|
|
inherited (tp-surface-plan-props node)))
|
|
(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))
|
|
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 (object _present)
|
|
(puthash (tp--surface-object-path object) object actual))
|
|
(tp--context-touched 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)
|
|
"Normalize producer VALUE for SURFACE using OPTIONS."
|
|
(cond
|
|
((tp-surface-result-p value)
|
|
(cons (tp--copy-surface-plan (tp-surface-result-plan value))
|
|
(tp-surface-result-client-state value)))
|
|
((tp-surface-plan-p value)
|
|
(cons (tp--copy-surface-plan value)
|
|
(if (plist-member options :client-state)
|
|
(plist-get options :client-state)
|
|
(tp--surface-client-state surface))))
|
|
(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 objects."
|
|
(let (objects)
|
|
(maphash (lambda (object _present) (push object objects))
|
|
(tp--context-touched context))
|
|
objects))
|
|
|
|
(defun tp--content-mount-spec (object record &optional tags)
|
|
"Return one content mount spec for OBJECT over RECORD with TAGS."
|
|
(list :object object :start (plist-get record :start)
|
|
:end (plist-get record :end)
|
|
:tags (tp--copy-property-value
|
|
(if tags tags (plist-get record :tags)))))
|
|
|
|
(defun tp--content-mount-specs (records context)
|
|
"Return direct and logical-fragment mount specs for RECORDS in CONTEXT."
|
|
(let (specs)
|
|
(dolist (record records)
|
|
(let* ((fragment (plist-get record :object))
|
|
(attachments
|
|
(nreverse
|
|
(copy-sequence
|
|
(gethash fragment
|
|
(tp--context-fragment-attachments context))))))
|
|
(push (tp--content-mount-spec fragment record) specs)
|
|
(dolist (attachment attachments)
|
|
(push (tp--content-mount-spec
|
|
(car attachment) record (cdr attachment))
|
|
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 (marker-position (tp--surface-mount-start mount)))
|
|
(end (marker-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)
|
|
(let ((plan (tp--copy-surface-plan input)))
|
|
(tp--ensure-plan-objects context plan)
|
|
(tp--producer-result plan surface options)))))
|
|
|
|
(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--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 (marker-position (tp--surface-mount-start mount)))
|
|
(end (marker-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--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 (equal-including-properties
|
|
(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)))
|
|
(when (equal-including-properties
|
|
(tp--substring-ranges old old-outside)
|
|
(tp--substring-ranges new new-outside))
|
|
(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-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))))
|
|
(plan (car normalized))
|
|
(client-state (cdr 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))
|
|
(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 (- (length objects) (length 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))
|
|
|
|
(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 (copy-sequence (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, :client-state, and :observers. 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-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'. Its default, `error', signals
|
|
`tp-scope-mismatch'; `root' permits an explicit full-surface fallback."
|
|
(tp--validate-live-surface surface)
|
|
(setq objects (tp--normalize-surface-scopes surface objects))
|
|
(let ((on-mismatch (or (plist-get options :on-mismatch) 'error)))
|
|
(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)))))
|
|
(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)
|
|
"Return PREPARED's stable mount attachment signature."
|
|
(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)
|
|
(- (marker-position (tp--surface-mount-start mount)) base)
|
|
(- (marker-position (tp--surface-mount-end mount)) base)
|
|
(tp--surface-mount-tags mount)))
|
|
(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--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--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))
|
|
(insert (substring new prefix (- (length new) suffix))))
|
|
(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."
|
|
(cl-loop for offset from from below to
|
|
thereis
|
|
(not (tp--plan-props-equal-p
|
|
(text-properties-at (+ start offset) buffer)
|
|
(text-properties-at offset rendered)))))
|
|
|
|
(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)))))
|
|
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 marker-backed content mounts for PREPARED."
|
|
(let* ((surface (tp--prepared-surface-surface prepared))
|
|
(buffer (tp--surface-buffer surface))
|
|
(base (marker-position (tp--surface-start surface))))
|
|
(mapcar
|
|
(lambda (spec)
|
|
(tp--make-surface-mount
|
|
:object (plist-get spec :object)
|
|
:start (with-current-buffer buffer
|
|
(copy-marker (+ base (plist-get spec :start)) nil))
|
|
: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
|
|
: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--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 (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
|
|
: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
|
|
: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))
|
|
(plain (substring-no-properties replacement)))
|
|
(unless (equal (buffer-substring-no-properties start end) plain)
|
|
(delete-region start end)
|
|
(goto-char start)
|
|
(insert plain)
|
|
(cl-incf count))))
|
|
(set-marker (tp--surface-start surface) base)
|
|
(set-marker (tp--surface-end surface) (+ base (length rendered)))
|
|
count))
|
|
|
|
(defun tp--publish-buffer-content (prepared)
|
|
"Publish PREPARED's text and properties, returning operation counts."
|
|
(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)
|
|
(with-current-buffer buffer
|
|
(save-restriction
|
|
(widen)
|
|
(let ((inhibit-read-only
|
|
(plist-get (tp--surface-options surface) :inhibit-read-only)))
|
|
(setq text-operations
|
|
(if (and scoped (not fallback))
|
|
(tp--publish-scoped-content-text
|
|
surface rendered patches)
|
|
(tp--content-text-operation surface rendered)))
|
|
(tp--publication-step 'text surface)
|
|
(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))))
|
|
(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)
|
|
(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* ((started (float-time))
|
|
(surface (tp--prepared-surface-surface prepared))
|
|
(counts (if (eq (tp--surface-capability surface) 'content)
|
|
(tp--publish-buffer-content prepared)
|
|
(tp--publish-buffer-properties prepared))))
|
|
(tp--create-candidate-mount-state prepared)
|
|
(tp--publication-step 'marker surface)
|
|
(tp--publication-step 'index surface)
|
|
(tp--swap-surface-state prepared counts)
|
|
(let ((report (plist-put (tp--surface-report surface) :timing
|
|
(list :publication (- (float-time) started)))))
|
|
(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."
|
|
(delete-dups
|
|
(mapcar (lambda (prepared)
|
|
(tp--surface-buffer (tp--prepared-surface-surface prepared)))
|
|
prepared-list)))
|
|
|
|
(defun tp--capture-view-state (buffers)
|
|
"Capture point, windows, and modified state for BUFFERS."
|
|
(mapcar
|
|
(lambda (buffer)
|
|
(with-current-buffer buffer
|
|
(list buffer (copy-marker (point) t) (buffer-modified-p)
|
|
(mapcar (lambda (window)
|
|
(cons window (copy-marker (window-start window) nil)))
|
|
(get-buffer-window-list buffer nil t)))))
|
|
buffers))
|
|
|
|
(defun tp--restore-view-state (states)
|
|
"Restore captured point, window, and modified STATES."
|
|
(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)))))
|
|
(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--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)
|
|
(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)
|
|
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--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 (tp--surface-state-snapshots prepared))
|
|
(group (tp--prepare-change-group-for-buffers buffers)))
|
|
(puthash 'changed prepared state)
|
|
(puthash 'journals journals state)
|
|
(puthash 'views views state)
|
|
(puthash 'snapshots snapshots state)
|
|
(puthash 'change-group group state)
|
|
(cl-incf tp--surface-transaction-id)
|
|
(let ((tp--surface-publishing t))
|
|
(dolist (candidate prepared)
|
|
(tp--publish-one-surface candidate)))))))
|
|
|
|
(defun tp--cancel-change-group-safely (group)
|
|
"Cancel active change GROUP without allowing quit to interrupt rollback."
|
|
(when group
|
|
(let ((inhibit-quit t))
|
|
(condition-case nil
|
|
(cancel-change-group group)
|
|
(error nil)))))
|
|
|
|
(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)))
|
|
(tp--cancel-change-group-safely (gethash 'change-group state))
|
|
(tp--restore-property-journals (gethash 'journals state))
|
|
(dolist (entry (gethash 'snapshots state))
|
|
(tp--restore-surface-snapshot (car entry) (cdr entry)))
|
|
(tp--restore-view-state (gethash 'views state))
|
|
(when-let ((table (gethash 'prepared state)))
|
|
(maphash (lambda (_surface prepared)
|
|
(let ((context (tp--prepared-surface-context prepared)))
|
|
(setf (tp--context-active context) t)
|
|
(tp--discard-context context)))
|
|
table))))
|
|
|
|
(defun tp--surface-finalize-killed-rollback ()
|
|
"Reapply authoritative teardown after graph rollback for killed buffers."
|
|
(when-let ((state (tp--transaction-extension tp--surface-extension-key)))
|
|
(when-let ((table (gethash 'prepared state)))
|
|
(maphash
|
|
(lambda (surface _prepared)
|
|
(unless (buffer-live-p (tp--surface-buffer surface))
|
|
(tp--teardown-surface surface t)))
|
|
table))))
|
|
|
|
(defun tp--anchors-in-mounts (mounts)
|
|
"Return distinct range anchors referenced by MOUNTS."
|
|
(delete-dups
|
|
(delq nil (mapcar #'tp--surface-mount-anchor mounts))))
|
|
|
|
(defun tp--update-anchor-ownership (surface old-mounts new-mounts)
|
|
"Update anchor ownership for SURFACE from OLD-MOUNTS to NEW-MOUNTS."
|
|
(let ((old (tp--anchors-in-mounts old-mounts))
|
|
(new (tp--anchors-in-mounts new-mounts)))
|
|
(dolist (anchor old)
|
|
(unless (memq anchor new)
|
|
(setf (tp--anchor-surfaces anchor)
|
|
(delq surface (tp--anchor-surfaces anchor)))
|
|
(unless (tp--anchor-surfaces anchor) (tp--dispose-anchor anchor))))
|
|
(dolist (anchor new)
|
|
(cl-pushnew surface (tp--anchor-surfaces anchor) :test #'eq)
|
|
(setf (tp--anchor-candidate-context anchor) nil))))
|
|
|
|
(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--finalize-context (prepared published)
|
|
"Finalize PREPARED after commit; PUBLISHED means side state was swapped."
|
|
(let* ((surface (tp--prepared-surface-surface prepared))
|
|
(context (tp--prepared-surface-context prepared)))
|
|
(if published
|
|
(progn
|
|
(tp--finalize-object-lifecycle prepared)
|
|
(dolist (anchor (tp--context-created-anchors context))
|
|
(unless (tp--anchor-surfaces anchor) (tp--dispose-anchor anchor))))
|
|
(tp--finalize-object-lifecycle prepared)
|
|
(dolist (object (tp--context-new-objects context))
|
|
(unless (tp--surface-object-live object)
|
|
(setf (tp--surface-object-disposed object) t)))
|
|
(dolist (anchor (tp--context-created-anchors context))
|
|
(unless (memq surface (tp--anchor-surfaces anchor))
|
|
(tp--dispose-anchor anchor))))))
|
|
|
|
(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--run-surface-observers (surface observers report)
|
|
"Run SURFACE OBSERVERS with REPORT and record their errors."
|
|
(dolist (observer observers)
|
|
(condition-case failure
|
|
(funcall observer surface report)
|
|
(error (tp--record-observer-error surface observer failure)))))
|
|
|
|
(defun tp--enqueue-surface-observers (surface)
|
|
"Schedule SURFACE observers outside the publishing transaction."
|
|
(let ((observers (copy-sequence (tp--surface-observers surface)))
|
|
(report (tp-surface-report surface)))
|
|
(tp--enqueue-after-commit
|
|
(lambda () (tp--run-surface-observers surface observers report)))))
|
|
|
|
(defun tp--surface-commit-transaction ()
|
|
"Commit buffer edits and finalize every transaction surface."
|
|
(when-let ((state (tp--transaction-extension tp--surface-extension-key)))
|
|
(when-let ((group (gethash 'change-group state)))
|
|
(accept-change-group group))
|
|
(tp--restore-view-state (gethash 'views state))
|
|
(let ((changed (gethash 'changed state))
|
|
(snapshots (gethash 'snapshots state)))
|
|
(dolist (entry snapshots)
|
|
(let* ((prepared (car entry))
|
|
(snapshot (cdr entry))
|
|
(surface (tp--prepared-surface-surface prepared)))
|
|
(tp--update-anchor-ownership
|
|
surface (tp--surface-snapshot-mounts snapshot)
|
|
(tp--surface-mounts surface))
|
|
(tp--dispose-content-mounts (tp--surface-snapshot-mounts snapshot))
|
|
(tp--dispose-ledger (tp--surface-snapshot-ledger snapshot))))
|
|
(when-let ((table (gethash 'prepared state)))
|
|
(maphash (lambda (_surface prepared)
|
|
(tp--finalize-context prepared (memq prepared changed)))
|
|
table))
|
|
(dolist (prepared changed)
|
|
(tp--enqueue-surface-observers
|
|
(tp--prepared-surface-surface prepared))))))
|
|
|
|
(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)))
|
|
(tp--validate-plan-capability plan 'content)
|
|
(tp--validate-context-tree context plan)
|
|
(setq rendered (car (tp--render-plan plan context))
|
|
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)))
|
|
|
|
;;;###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 (marker-position (tp--surface-mount-start mount)))
|
|
(end (marker-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-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)
|
|
(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
|