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:
1 parent
8ce5e5cdad
commit
7eef6e8be6
30 files changed
+1484
-174
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user