Make web document navigation survive live-updating pages

Recreated-node caret recovery, tolerant caret validation, per-root retained document trees and cursors across tab switches, document warm-up, coalesced structural moves, and hung-provider fast-fail with an 'Application is not responding' announcement.
This commit is contained in:
hjozwiak committed 2026-11-03 10:34:55 -05:00
1 parent 8ce5e5cdad
commit 7eef6e8be6
30 files changed
+1484 -174

No files matched your search

+39 -13
View File
@@ -72,14 +72,24 @@ func (document DocumentSnapshot) SubtreeEnd(index int) int {
}
func document_entry_error(entry treeEntry) error {
return document_entry_error_tolerating(entry, false)
}
// document_entry_error_tolerating optionally ignores text, geometry, and
// child-order staleness. Caret movement reads text and line bounds live
// from the provider and generation-checks every endpoint, so captured
// staleness of those cannot make its result wrong: at worst a just-inserted
// sibling is skipped until the next capture. Identity and semantics are
// checked either way.
func document_entry_error_tolerating(entry treeEntry, tolerateText bool) error {
switch {
case entry.snapshotInvalid:
return ErrStaleSnapshot
case entry.childrenInvalid:
case entry.childrenInvalid && !tolerateText:
return ErrIncompleteTree
case entry.textInvalid:
case entry.textInvalid && !tolerateText:
return ErrStaleText
case entry.geometryInvalid:
case entry.geometryInvalid && !tolerateText:
return ErrStaleGeometry
}
return nil
@@ -95,19 +105,32 @@ func (tree *AccessibleTree) ValidateDocumentRange(ctx context.Context, document
return tree.validate_document_range_locked(ctx, document, from, to)
}
// ValidateDocumentCaretRange is ValidateDocumentRange for caret movement,
// whose text and line bounds are read live from the provider during the
// move. Text, geometry, and child-order staleness along the path therefore
// does not force a recapture first; on constantly ticking pages some
// traversed counter is always mid-update, and strict validation could
// never commit a move no matter how often it recaptured. Endpoints remain
// generation-checked, and structural navigation keeps strict validation.
func (tree *AccessibleTree) ValidateDocumentCaretRange(ctx context.Context, document DocumentSnapshot, from, to int) error {
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.validate_document_path_locked(ctx, document, from, to, from != to, true)
}
// ValidateDocumentOrder also checks ancestor child order when a structural
// search or a document-boundary jump will leave its current object.
func (tree *AccessibleTree) ValidateDocumentOrder(ctx context.Context, document DocumentSnapshot, index int) error {
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.validate_document_path_locked(ctx, document, index, index, true)
return tree.validate_document_path_locked(ctx, document, index, index, true, false)
}
func (tree *AccessibleTree) validate_document_range_locked(ctx context.Context, document DocumentSnapshot, from, to int) error {
return tree.validate_document_path_locked(ctx, document, from, to, from != to)
return tree.validate_document_path_locked(ctx, document, from, to, from != to, false)
}
func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, document DocumentSnapshot, from, to int, requireOrder bool) error {
func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, document DocumentSnapshot, from, to int, requireOrder, tolerateText bool) error {
if err := ctx.Err(); err != nil {
return err
}
@@ -126,10 +149,10 @@ func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, d
if err != nil {
return err
}
if err := document_entry_error(entry); err != nil {
if err := document_entry_error_tolerating(entry, tolerateText); err != nil {
return err
}
if err := tree.validate_document_row_locked(document, node.Reference.ID()); err != nil {
if err := tree.validate_document_row_locked(document, node.Reference.ID(), tolerateText); err != nil {
return err
}
for entry.hasParent {
@@ -137,20 +160,23 @@ func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, d
if entry.snapshotInvalid {
return ErrStaleSnapshot
}
if entry.geometryInvalid {
if entry.geometryInvalid && !tolerateText {
return ErrStaleGeometry
}
if requireOrder && entry.childrenInvalid {
if requireOrder && !tolerateText && entry.childrenInvalid {
return ErrIncompleteTree
}
// Hypertext ranges interleave parent-owned text with embedded
// children. An insertion can change those ranges without changing
// the accessible child list. Refresh before crossing objects, while
// allowing movement within an unchanged child to remain local.
// Even caret movement keeps this check: captured embedded-object
// offsets decide which ranges exist, so tolerating a hypertext
// insertion would silently skip the inserted text.
if requireOrder && entry.textInvalid && len(entry.snapshot.TextChildren) != 0 {
return ErrStaleText
}
if err := tree.validate_document_row_locked(document, entry.snapshot.ID()); err != nil {
if err := tree.validate_document_row_locked(document, entry.snapshot.ID(), tolerateText); err != nil {
return err
}
}
@@ -158,13 +184,13 @@ func (tree *AccessibleTree) validate_document_path_locked(ctx context.Context, d
return nil
}
func (tree *AccessibleTree) validate_document_row_locked(document DocumentSnapshot, id ObjectID) error {
func (tree *AccessibleTree) validate_document_row_locked(document DocumentSnapshot, id ObjectID, tolerateText bool) error {
for _, reference := range document.data.rows[id] {
entry, err := tree.resolveLocked(reference)
if err != nil {
return err
}
if err := document_entry_error(entry); err != nil {
if err := document_entry_error_tolerating(entry, tolerateText); err != nil {
return err
}
}