// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #include */ import "C" import ( "context" "strings" "time" "unicode/utf8" ) func (adapter *Adapter) readNativeDocumentLine(ctx context.Context, id ObjectID, snapshot Snapshot, text *C.AtspiText, offset, count int) (DocumentCaretText, error) { source := nativeReviewText{text: text} line, err := source.line(ctx, offset) if err != nil { return DocumentCaretText{}, err } result := DocumentCaretText{Start: line.Start, End: line.End, Text: line.Text, CharacterCount: count} if snapshot.Editable || snapshot.Role == "password text" || snapshot.HTMLTag == "pre" || snapshot.HTMLTag == "code" || count > maxReviewLineCharacters || len(strings.Fields(line.Text)) > 1 || line.Start == 0 && line.End == count { return result, nil } if cached, ok := adapter.documentLineCache[id]; ok && cached.CharacterCount == count { if cached.End > 0 { return cached, nil } return result, nil } adapter.treeMutex.Lock() tree := adapter.documentTree adapter.treeMutex.Unlock() if tree != nil { if ref, err := tree.Reference(id); err == nil { if owner, err := tree.DocumentContext(ref); err == nil { for _, ancestor := range owner.Ancestors { if ancestor.HTMLTag == "pre" || ancestor.HTMLTag == "code" || strings.Contains(" "+ancestor.XMLRoles+" ", " code ") { return result, nil } } } } } var nativeErr *C.GError value := C.atspi_text_get_text(text, 0, C.gint(count), &nativeErr) if value != nil { defer C.g_free(C.gpointer(value)) } if nativeErr != nil { C.g_error_free(nativeErr) return result, nil } content := strings.ToValidUTF8(C.GoString(value), "�") if utf8.RuneCountInString(content) != count { return result, nil } probe, cancel := context.WithTimeout(ctx, 100*time.Millisecond) repair, probeErr := documentLinesAreTokens(probe, source, content) cancel() if probeErr != nil { return result, ctx.Err() } if adapter.documentLineCache == nil || len(adapter.documentLineCache) >= 64 { adapter.documentLineCache = make(map[ObjectID]DocumentCaretText) } cached := DocumentCaretText{CharacterCount: count} if repair { cached.End, cached.Text = count, content result = cached } adapter.documentLineCache[id] = cached return result, nil } // Repair only owned web text, on the GLib owner. Native selections use the // same logical line as virtual browsing, including braille cursor placement. func (adapter *Adapter) repairNativeCaretLine(source *C.AtspiAccessible, snapshot *Snapshot, offset int) { if !snapshot.TextCaretKnown || snapshot.Editable || snapshot.Role == "password text" || offset < 0 { return } adapter.treeMutex.Lock() tree := adapter.documentTree adapter.treeMutex.Unlock() if tree == nil { return } ref, err := tree.Reference(snapshot.ID()) if err != nil { return } if _, err = tree.DocumentContext(ref); err != nil { return } text := C.atspi_accessible_get_text_iface(source) if text == nil { return } defer C.g_object_unref(C.gpointer(text)) count := int(C.atspi_text_get_character_count(text, nil)) if count <= 0 || offset > count { return } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() line, err := adapter.readNativeDocumentLine(ctx, snapshot.ID(), *snapshot, text, min(offset, count-1), count) if err != nil || line.Start < 0 || line.End <= line.Start { return } snapshot.Text = sanitizeSnapshotField(line.Text) snapshot.BrailleLine, snapshot.BrailleLineStart, snapshot.BrailleCaret = line.Text, line.Start, offset snapshot.CharacterCount = count }