diff --git a/DasherCore b/DasherCore index 2f10d5c..b711753 160000 --- a/DasherCore +++ b/DasherCore @@ -1 +1 @@ -Subproject commit 2f10d5c3f94c0169938ced8858e8e94a8e880002 +Subproject commit b7117534c8206fef89a5515ca86d25291b877c79 diff --git a/shared-resources b/shared-resources index 95a7a94..d93cb86 160000 --- a/shared-resources +++ b/shared-resources @@ -1 +1 @@ -Subproject commit 95a7a94decf3872a4386290101773b4f5838c944 +Subproject commit d93cb86465f3444fa16be39f8ed23a68092e7cd1 diff --git a/src/Dasher.Windows/Resources/Strings.af.resx b/src/Dasher.Windows/Resources/Strings.af.resx index eb04d05..6c9b038 100644 --- a/src/Dasher.Windows/Resources/Strings.af.resx +++ b/src/Dasher.Windows/Resources/Strings.af.resx @@ -314,6 +314,9 @@ Jy kan dit enige tyd verander in Instellings > Privaatheid. Nuwe Boodskap + + Nuwe sessie (maak teks en konteks skoon) + Geen Dasher 5-data op hierdie Mac gevind nie. diff --git a/src/Dasher.Windows/Resources/Strings.ar.resx b/src/Dasher.Windows/Resources/Strings.ar.resx index 741d4cb..db7b479 100644 --- a/src/Dasher.Windows/Resources/Strings.ar.resx +++ b/src/Dasher.Windows/Resources/Strings.ar.resx @@ -317,6 +317,9 @@ رسالة جديدة + + جلسة جديدة (مسح النص والسياق) + لم يتم العثور على بيانات Dasher 5 على هذا الجهاز Mac. diff --git a/src/Dasher.Windows/Resources/Strings.bn.resx b/src/Dasher.Windows/Resources/Strings.bn.resx index 9ab0b06..5d034bb 100644 --- a/src/Dasher.Windows/Resources/Strings.bn.resx +++ b/src/Dasher.Windows/Resources/Strings.bn.resx @@ -317,6 +317,9 @@ নতুন বার্তা + + নতুন সেশন (টেক্সট ও প্রসঙ্গ মুছুন) + এই ম্যাক-এ কোনো ড্যাশার ৫ ডেটা পাওয়া যায়নি। diff --git a/src/Dasher.Windows/Resources/Strings.cs.resx b/src/Dasher.Windows/Resources/Strings.cs.resx index 7c56060..7c2a6fb 100644 --- a/src/Dasher.Windows/Resources/Strings.cs.resx +++ b/src/Dasher.Windows/Resources/Strings.cs.resx @@ -308,6 +308,9 @@ Toto nastavení můžete kdykoli změnit v Nastavení > Soukromí. Nová zpráva + + Nová relace (vymazat text a kontext) + Na tomto Macu nebyla nalezena žádná data pro Dasher 5. diff --git a/src/Dasher.Windows/Resources/Strings.da.resx b/src/Dasher.Windows/Resources/Strings.da.resx index 09d0e0c..7c0e449 100644 --- a/src/Dasher.Windows/Resources/Strings.da.resx +++ b/src/Dasher.Windows/Resources/Strings.da.resx @@ -308,6 +308,9 @@ Du kan til enhver tid ændre dette i Indstillinger > Privatliv. Ny besked + + Ny session (ryd tekst og kontekst) + Ingen Dasher 5-data fundet på denne Mac. diff --git a/src/Dasher.Windows/Resources/Strings.de.resx b/src/Dasher.Windows/Resources/Strings.de.resx index 3f9b43e..7521ea0 100644 --- a/src/Dasher.Windows/Resources/Strings.de.resx +++ b/src/Dasher.Windows/Resources/Strings.de.resx @@ -302,6 +302,9 @@ Sie können dies jederzeit unter Einstellungen > Datenschutz ändern. Neue Nachricht + + Neue Sitzung (Text und Kontext löschen) + Auf diesem Mac wurden keine Dasher 5-Daten gefunden. diff --git a/src/Dasher.Windows/Resources/Strings.el.resx b/src/Dasher.Windows/Resources/Strings.el.resx index 8d7139f..d9e386b 100644 --- a/src/Dasher.Windows/Resources/Strings.el.resx +++ b/src/Dasher.Windows/Resources/Strings.el.resx @@ -317,6 +317,9 @@ Νέο μήνυμα + + Νέα συνεδρία (εκκαθάριση κειμένου και συμφραζομένων) + Δεν βρέθηκαν δεδομένα Dasher 5 σε αυτόν τον Mac. diff --git a/src/Dasher.Windows/Resources/Strings.es.resx b/src/Dasher.Windows/Resources/Strings.es.resx index 6318e1f..1f13de2 100644 --- a/src/Dasher.Windows/Resources/Strings.es.resx +++ b/src/Dasher.Windows/Resources/Strings.es.resx @@ -308,6 +308,9 @@ Puedes cambiar esta configuración en cualquier momento en Ajustes > Privacid Nuevo mensaje + + Nueva sesión (borrar texto y contexto) + No se encontraron datos de Dasher 5 en esta Mac. diff --git a/src/Dasher.Windows/Resources/Strings.fa.resx b/src/Dasher.Windows/Resources/Strings.fa.resx index 4c53cde..0ed9d83 100644 --- a/src/Dasher.Windows/Resources/Strings.fa.resx +++ b/src/Dasher.Windows/Resources/Strings.fa.resx @@ -317,6 +317,9 @@ پیام جدید + + نشست جدید (پاک کردن متن و زمینه) + هیچ داده‌ای از Dasher 5 در این مک یافت نشد. diff --git a/src/Dasher.Windows/Resources/Strings.fi.resx b/src/Dasher.Windows/Resources/Strings.fi.resx index 67861ac..99cdbde 100644 --- a/src/Dasher.Windows/Resources/Strings.fi.resx +++ b/src/Dasher.Windows/Resources/Strings.fi.resx @@ -314,6 +314,9 @@ Voit muuttaa tätä milloin tahansa kohdassa Asetukset > Tietosuoja. Uusi viesti + + Uusi istunto (tyhjennä teksti ja konteksti) + Tältä Macilta ei löytynyt Dasher 5 -tietoja. diff --git a/src/Dasher.Windows/Resources/Strings.fr.resx b/src/Dasher.Windows/Resources/Strings.fr.resx index 233356f..4f6268c 100644 --- a/src/Dasher.Windows/Resources/Strings.fr.resx +++ b/src/Dasher.Windows/Resources/Strings.fr.resx @@ -294,6 +294,9 @@ Vous pouvez modifier cela à tout moment dans Paramètres > Confidentialité. Nouveau message + + Nouvelle session (effacer le texte et le contexte) + Aucune donnée Dasher 5 trouvée sur ce Mac. diff --git a/src/Dasher.Windows/Resources/Strings.gu.resx b/src/Dasher.Windows/Resources/Strings.gu.resx index f2b6e1b..bdd8a52 100644 --- a/src/Dasher.Windows/Resources/Strings.gu.resx +++ b/src/Dasher.Windows/Resources/Strings.gu.resx @@ -320,6 +320,9 @@ નવો સંદેશ + + નવો સત્ર (ટેક્સ્ટ અને સંદર્ભ સાફ કરો) + આ Mac પર કોઈ Dasher 5 ડેટા મળ્યો નથી. diff --git a/src/Dasher.Windows/Resources/Strings.hi.resx b/src/Dasher.Windows/Resources/Strings.hi.resx index 4e50a09..6a7e00e 100644 --- a/src/Dasher.Windows/Resources/Strings.hi.resx +++ b/src/Dasher.Windows/Resources/Strings.hi.resx @@ -317,6 +317,9 @@ नया सन्देश + + नया सत्र (पाठ और संदर्भ साफ़ करें) + इस मैक पर डैशर 5 का कोई डेटा नहीं मिला। diff --git a/src/Dasher.Windows/Resources/Strings.hu.resx b/src/Dasher.Windows/Resources/Strings.hu.resx index 3663aec..c1e1424 100644 --- a/src/Dasher.Windows/Resources/Strings.hu.resx +++ b/src/Dasher.Windows/Resources/Strings.hu.resx @@ -308,6 +308,9 @@ Ezt bármikor módosíthatja a Beállítások > Adatvédelem menüpontban. Új üzenet + + Új munkamenet (szöveg és kontextus törlése) + Nem találhatók Dasher 5 adatok ezen a Mac gépen. diff --git a/src/Dasher.Windows/Resources/Strings.it.resx b/src/Dasher.Windows/Resources/Strings.it.resx index 89a05f1..14b3d63 100644 --- a/src/Dasher.Windows/Resources/Strings.it.resx +++ b/src/Dasher.Windows/Resources/Strings.it.resx @@ -311,6 +311,9 @@ Puoi modificare questa impostazione in qualsiasi momento in Impostazioni > Pr Nuovo messaggio + + Nuova sessione (cancella testo e contesto) + Nessun dato Dasher 5 trovato su questo Mac. diff --git a/src/Dasher.Windows/Resources/Strings.kn.resx b/src/Dasher.Windows/Resources/Strings.kn.resx index 5045fd5..f760c78 100644 --- a/src/Dasher.Windows/Resources/Strings.kn.resx +++ b/src/Dasher.Windows/Resources/Strings.kn.resx @@ -314,6 +314,9 @@ ಹೊಸ ಸಂದೇಶ + + ಹೊಸ ಅಧಿವೇಶನ (ಪಠ್ಯ ಮತ್ತು ಸಂದರ್ಭ ಅಳಿಸಿ) + ಈ Mac ನಲ್ಲಿ ಯಾವುದೇ Dasher 5 ಡೇಟಾ ಕಂಡುಬಂದಿಲ್ಲ. diff --git a/src/Dasher.Windows/Resources/Strings.ml.resx b/src/Dasher.Windows/Resources/Strings.ml.resx index a906bcd..052166d 100644 --- a/src/Dasher.Windows/Resources/Strings.ml.resx +++ b/src/Dasher.Windows/Resources/Strings.ml.resx @@ -320,6 +320,9 @@ പുതിയ സന്ദേശം + + പുതിയ സെഷൻ (വാചകവും സന്ദർഭവും മായ്ക്കുക) + ഈ മാക്കിൽ ഡാഷർ 5 ഡാറ്റയൊന്നും കണ്ടെത്തിയില്ല. diff --git a/src/Dasher.Windows/Resources/Strings.mr.resx b/src/Dasher.Windows/Resources/Strings.mr.resx index 0345249..c150b47 100644 --- a/src/Dasher.Windows/Resources/Strings.mr.resx +++ b/src/Dasher.Windows/Resources/Strings.mr.resx @@ -317,6 +317,9 @@ नवीन संदेश + + नवीन सत्र (मजकूर आणि संदर्भ पुसा) + या मॅकवर डॅशर ५ चा कोणताही डेटा आढळला नाही. diff --git a/src/Dasher.Windows/Resources/Strings.nl.resx b/src/Dasher.Windows/Resources/Strings.nl.resx index e125718..6a00032 100644 --- a/src/Dasher.Windows/Resources/Strings.nl.resx +++ b/src/Dasher.Windows/Resources/Strings.nl.resx @@ -308,6 +308,9 @@ U kunt dit op elk moment wijzigen in Instellingen > Privacy. Nieuw bericht + + Nieuwe sessie (tekst en context wissen) + Er zijn geen Dasher 5-gegevens gevonden op deze Mac. diff --git a/src/Dasher.Windows/Resources/Strings.pa.resx b/src/Dasher.Windows/Resources/Strings.pa.resx index 5d91d70..3419a14 100644 --- a/src/Dasher.Windows/Resources/Strings.pa.resx +++ b/src/Dasher.Windows/Resources/Strings.pa.resx @@ -320,6 +320,9 @@ ਨਵਾਂ ਸੁਨੇਹਾ + + ਨਵੀਂ ਸੈਸ਼ਨ (ਟੈਕਸਟ ਅਤੇ ਪ੍ਰਸੰਗ ਸਾਫ਼ ਕਰੋ) + ਇਸ ਮੈਕ 'ਤੇ ਕੋਈ ਡੈਸ਼ਰ 5 ਡਾਟਾ ਨਹੀਂ ਮਿਲਿਆ। diff --git a/src/Dasher.Windows/Resources/Strings.pl.resx b/src/Dasher.Windows/Resources/Strings.pl.resx index 542564c..b4e9944 100644 --- a/src/Dasher.Windows/Resources/Strings.pl.resx +++ b/src/Dasher.Windows/Resources/Strings.pl.resx @@ -314,6 +314,9 @@ Możesz to zmienić w dowolnym momencie w Ustawieniach > Prywatność. Nowa wiadomość + + Nowa sesja (wyczyść tekst i kontekst) + Nie znaleziono danych Dasher 5 na tym Macu. diff --git a/src/Dasher.Windows/Resources/Strings.pt-PT.resx b/src/Dasher.Windows/Resources/Strings.pt-PT.resx index e0e0482..cb239a8 100644 --- a/src/Dasher.Windows/Resources/Strings.pt-PT.resx +++ b/src/Dasher.Windows/Resources/Strings.pt-PT.resx @@ -314,6 +314,9 @@ Você pode alterar isso a qualquer momento em Configurações > Privacidade.< Nova mensagem + + Nova sessão (limpar texto e contexto) + Nenhum dado do Dasher 5 foi encontrado neste Mac. diff --git a/src/Dasher.Windows/Resources/Strings.pt.resx b/src/Dasher.Windows/Resources/Strings.pt.resx index 1546ce4..ba1fafb 100644 --- a/src/Dasher.Windows/Resources/Strings.pt.resx +++ b/src/Dasher.Windows/Resources/Strings.pt.resx @@ -314,6 +314,9 @@ Você pode alterar isso a qualquer momento em Configurações > Privacidade.< Nova mensagem + + Nova sessão (limpar texto e contexto) + Nenhum dado do Dasher 5 foi encontrado neste Mac. diff --git a/src/Dasher.Windows/Resources/Strings.resx b/src/Dasher.Windows/Resources/Strings.resx index 6e17e3e..e9ced4f 100644 --- a/src/Dasher.Windows/Resources/Strings.resx +++ b/src/Dasher.Windows/Resources/Strings.resx @@ -119,6 +119,24 @@ Copy commands + + Could not load the file into the engine + + + Could not open the selected file. + + + Could not read the selected file. + + + Could not sync your edit with the engine — it may be overwritten + + + Could not write the export file. + + + Could not write the training data. + Current Calibration @@ -212,6 +230,9 @@ You can change this anytime in Settings > Privacy. Fast + + File truncated at 4 MB — only the first part is loaded + Flatpak or AppImage build can't provide. @@ -250,6 +271,12 @@ You can change this anytime in Settings > Privacy. Import Training Text + + Imported %1 — applies fully on next launch + + + Imported %1 of training text + Increase Speed @@ -346,6 +373,9 @@ You can change this anytime in Settings > Privacy. Light + + Loaded %1 characters + Language @@ -364,6 +394,9 @@ You can change this anytime in Settings > Privacy. New Message + + New session (clear text and context) + No Dasher 5 data found on this Mac. @@ -373,6 +406,12 @@ You can change this anytime in Settings > Privacy. No speech engine available. Build with the system TTS feature, or set up a cloud voice in Preferences. + + No training data to export yet + + + No user training data yet + Nothing to save @@ -598,12 +637,24 @@ You can change this anytime in Settings > Privacy. Text files + + That file already is your training data. + the app you are pointing at (needs ydotool) The debug overlay draws a green panel top-left of the canvas showing live touch event counts and the last reported (x, y). When you pinch and hold, you should see touch.began then touch.moved with changing coordinates as you look around. + + The file is too large (over 10 MB) to import as training text. + + + The selected file is empty. + + + This deletes your accumulated training data for every alphabet. Predictions keep the influence of very recent typing. + This deletes your accumulated training data. The language model will return to built-in defaults on next launch. This cannot be undone. @@ -649,9 +700,15 @@ You can change this anytime in Settings > Privacy. Training Data + + Training data deleted — restart Dasher to fully apply + Import adds text to the language model and your accumulated training data (kept across restarts). Export saves your training data for backup or transfer. Reset deletes all user training data — the model returns to its built-in defaults on next launch. + + Training data exported. + Export Training Data @@ -670,6 +727,9 @@ You can change this anytime in Settings > Privacy. Imported {0} KB — applies fully on next launch (live model returned {1}) + + Training is not available yet — try again once the canvas has rendered. + No user training data yet diff --git a/src/Dasher.Windows/Resources/Strings.ru.resx b/src/Dasher.Windows/Resources/Strings.ru.resx index 90c184b..f7ae7b8 100644 --- a/src/Dasher.Windows/Resources/Strings.ru.resx +++ b/src/Dasher.Windows/Resources/Strings.ru.resx @@ -311,6 +311,9 @@ Новое сообщение + + Новый сеанс (очистить текст и контекст) + Данные Dasher 5 на этом Mac не найдены. diff --git a/src/Dasher.Windows/Resources/Strings.sv.resx b/src/Dasher.Windows/Resources/Strings.sv.resx index 964a959..5cc0649 100644 --- a/src/Dasher.Windows/Resources/Strings.sv.resx +++ b/src/Dasher.Windows/Resources/Strings.sv.resx @@ -311,6 +311,9 @@ Du kan ändra detta när som helst i Inställningar > Sekretess. Nytt meddelande + + Ny session (rensa text och kontext) + Inga Dasher 5-data hittades på den här Mac-datorn. diff --git a/src/Dasher.Windows/Resources/Strings.sw.resx b/src/Dasher.Windows/Resources/Strings.sw.resx index 8c7d68a..46b040e 100644 --- a/src/Dasher.Windows/Resources/Strings.sw.resx +++ b/src/Dasher.Windows/Resources/Strings.sw.resx @@ -314,6 +314,9 @@ Unaweza kubadilisha hili wakati wowote katika Mipangilio > Faragha. Ujumbe Mpya + + Kipya kikao (futa maandishi na muktadha) + Hakuna data ya Dasher 5 iliyopatikana kwenye Mac hii. diff --git a/src/Dasher.Windows/Resources/Strings.ta.resx b/src/Dasher.Windows/Resources/Strings.ta.resx index 1aec4b5..e624333 100644 --- a/src/Dasher.Windows/Resources/Strings.ta.resx +++ b/src/Dasher.Windows/Resources/Strings.ta.resx @@ -317,6 +317,9 @@ புதிய செய்தி + + புதிய அமர்வு (உரை மற்றும் சூழலை அழி) + இந்த மேக்கில் டேஷர் 5 தரவு எதுவும் காணப்படவில்லை. diff --git a/src/Dasher.Windows/Resources/Strings.te.resx b/src/Dasher.Windows/Resources/Strings.te.resx index f5bbef5..8229ae7 100644 --- a/src/Dasher.Windows/Resources/Strings.te.resx +++ b/src/Dasher.Windows/Resources/Strings.te.resx @@ -317,6 +317,9 @@ కొత్త సందేశం + + కొత్త సెషన్ (పాఠ్యం మరియు సందర్భాన్ని క్లియర్ చేయండి) + ఈ Macలో డాషర్ 5 డేటా కనుగొనబడలేదు. diff --git a/src/Dasher.Windows/Resources/Strings.th.resx b/src/Dasher.Windows/Resources/Strings.th.resx index 17f2551..98c2594 100644 --- a/src/Dasher.Windows/Resources/Strings.th.resx +++ b/src/Dasher.Windows/Resources/Strings.th.resx @@ -317,6 +317,9 @@ ข้อความใหม่ + + เซสชันใหม่ (ล้างข้อความและบริบท) + ไม่พบข้อมูล Dasher 5 บนเครื่อง Mac นี้ diff --git a/src/Dasher.Windows/Resources/Strings.ur.resx b/src/Dasher.Windows/Resources/Strings.ur.resx index 79ddb92..683d93d 100644 --- a/src/Dasher.Windows/Resources/Strings.ur.resx +++ b/src/Dasher.Windows/Resources/Strings.ur.resx @@ -317,6 +317,9 @@ نیا پیغام + + نئی سیشن (متن اور سیاق صاف کریں) + اس میک پر کوئی Dasher 5 ڈیٹا نہیں ملا۔ diff --git a/src/Dasher.Windows/Resources/Strings.zh-CN.resx b/src/Dasher.Windows/Resources/Strings.zh-CN.resx index 21fa285..d9dd1b4 100644 --- a/src/Dasher.Windows/Resources/Strings.zh-CN.resx +++ b/src/Dasher.Windows/Resources/Strings.zh-CN.resx @@ -314,6 +314,9 @@ 新消息 + + 新建会话(清除文本和上下文) + 在此 Mac 上找不到 Dasher 5 数据。 diff --git a/src/Dasher.Windows/Resources/Strings.zu.resx b/src/Dasher.Windows/Resources/Strings.zu.resx index 4f69c36..ae32335 100644 --- a/src/Dasher.Windows/Resources/Strings.zu.resx +++ b/src/Dasher.Windows/Resources/Strings.zu.resx @@ -317,6 +317,9 @@ Ungashintsha lokhu noma nini kuzilungiselelo > Ubumfihlo. Umlayezo Omusha + + Iseshini esisha (sula umbhalo nomongo) + Akukho datha ye-Dasher 5 etholakele kule Mac. diff --git a/src/Dasher.Windows/Services/EditorSeedPolicy.cs b/src/Dasher.Windows/Services/EditorSeedPolicy.cs new file mode 100644 index 0000000..ddd2b22 --- /dev/null +++ b/src/Dasher.Windows/Services/EditorSeedPolicy.cs @@ -0,0 +1,34 @@ +using System; + +namespace Dasher.Windows.Services; + +/// +/// RFC 0019 clause 7 — long-document seed cap. Seeding replaces the engine +/// buffer and rebuilds the model anchored at the caret; at extreme document +/// sizes that cost stops being acceptable per keystroke, so only a trailing +/// window of the text is seeded (predictions continue from the caret — the +/// head of a 500k-char document carries no more local signal than the tail). +/// Pure function: unit-testable without an engine. +/// +public static class EditorSeedPolicy +{ + /// Trailing UTF-16 window seeded for over-long documents. + public const int MaxSeedUtf16 = 100_000; + + /// The text to seed, the caret offset within it (UTF-16), and + /// whether truncation was applied (for diagnostics). + public static (string Text, int CaretUtf16, bool Truncated) Clamp(string text, int caretUtf16) + { + if (string.IsNullOrEmpty(text) || text.Length <= MaxSeedUtf16) + return (text, Math.Clamp(caretUtf16, 0, text?.Length ?? 0), false); + + var start = text.Length - MaxSeedUtf16; + // A fixed UTF-16 cutoff can split a supplementary character (emoji, + // CJK extensions): landing on the LOW half would seed an unpaired + // surrogate through the UTF-8 bridge. Slide one unit forward. + if (char.IsLowSurrogate(text[start])) start++; + var seed = text[start..]; + var caret = Math.Clamp(caretUtf16 - start, 0, seed.Length); + return (seed, caret, true); + } +} diff --git a/src/Dasher.Windows/Views/MainWindow.axaml b/src/Dasher.Windows/Views/MainWindow.axaml index 89fcf84..861c592 100644 --- a/src/Dasher.Windows/Views/MainWindow.axaml +++ b/src/Dasher.Windows/Views/MainWindow.axaml @@ -412,10 +412,10 @@ - - diff --git a/src/Dasher.Windows/Views/MainWindow.axaml.cs b/src/Dasher.Windows/Views/MainWindow.axaml.cs index 8885505..0b510f3 100644 --- a/src/Dasher.Windows/Views/MainWindow.axaml.cs +++ b/src/Dasher.Windows/Views/MainWindow.axaml.cs @@ -49,6 +49,16 @@ public partial class MainWindow : Window [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); + // IME composition probe (RFC 0019 clause 2 — defer seeding until commit). + [DllImport("imm32.dll")] + private static extern IntPtr ImmGetContext(IntPtr hWnd); + + [DllImport("imm32.dll")] + private static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hImc); + + [DllImport("imm32.dll")] + private static extern int ImmGetCompositionStringW(IntPtr hImc, int dwIndex, IntPtr lpBuf, int dwBufLen); + [DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); @@ -609,6 +619,13 @@ private async Task CompleteStartupAsync(V5MigrationResult? migrationResult) // RFC 0019 (the editor contract): editable pane + caret re-anchor. WireEditorSync(); + // RFC 0019 clause 5 — New buttons, localized from the shared + // catalogue (new + new_session_tooltip; dasher-shared-resources#3). + var newTip = Loc.Tr("new_session_tooltip", "New session (clear text and context)"); + ToolTip.SetTip(BtnNewSession, newTip); + ToolTip.SetTip(KbNewSession, newTip); + BtnNewSessionLabel.Text = Loc.Tr("new", "New"); + // Wire speech volume slider to SpeechService var volSlider = this.FindControl("SpeakVolumeSlider"); if (volSlider != null) @@ -752,9 +769,20 @@ private void WireEditorSync() ? Marshal.PtrToStringUTF8(enginePtr) ?? "" : ""; if (paneText == engineText) return; - var caretBytes = NativeBridge.dasher_byte_offset_from_utf16(paneText, box.CaretIndex); - NativeBridge.dasher_seed_buffer(vm.Handle, paneText, caretBytes); - KbLog($"Editor sync: seeded {paneText.Length} chars, caret u16={box.CaretIndex} → byte={caretBytes}"); + // RFC 0019 clause 2 — IME composition deferral: mid-composition + // TextChanged events carry the evolving composition string; + // seeding each intermediate state is wasted rebuilds. Defer until + // composition ends (the recheck timer completes the seed). On + // IMMs where the probe can't see composition this degrades to + // today's immediate seeding — always correct, merely noisier. + if (ImeCompositionActive()) + { + _imeSeedPending = true; + EnsureImeRecheckTimer().Start(); + return; + } + + SeedPaneText(vm, box, paneText); }; MessageArea.PropertyChanged += (s, args) => @@ -779,6 +807,69 @@ private void WireEditorSync() }; } + /// True while an IME composition is in progress on our window. + private bool ImeCompositionActive() + { + try + { + var hwnd = OurHwnd(); + if (hwnd == IntPtr.Zero) return false; + var himc = ImmGetContext(hwnd); + if (himc == IntPtr.Zero) return false; + try + { + // GCS_COMPSTR: a non-empty composition string means in-progress. + const int GCS_COMPSTR = 0x0008; + return ImmGetCompositionStringW(himc, GCS_COMPSTR, IntPtr.Zero, 0) > 0; + } + finally { ImmReleaseContext(hwnd, himc); } + } + catch { return false; } + } + + private bool _imeSeedPending; + private Avalonia.Threading.DispatcherTimer? _imeRecheckTimer; + + private Avalonia.Threading.DispatcherTimer EnsureImeRecheckTimer() + { + if (_imeRecheckTimer != null) return _imeRecheckTimer; + _imeRecheckTimer = new Avalonia.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; + _imeRecheckTimer.Tick += (_, _) => + { + if (_imeSeedPending && !ImeCompositionActive()) + { + _imeSeedPending = false; + _imeRecheckTimer!.Stop(); + var box = MessageArea; + if (_vm != null && _vm.Handle != IntPtr.Zero && box != null && !_suppressEditorSync) + SeedPaneText(_vm, box, box.Text ?? ""); + } + else if (!_imeSeedPending) + { + _imeRecheckTimer!.Stop(); + } + }; + return _imeRecheckTimer; + } + + /// + /// Seed the engine from the pane text — FULL text, no cap: the pane IS + /// the document and the engine buffer mirrors it back through the VM + /// binding, so a capped seed would truncate the user's document on the + /// next engine-origin push (the buffer-as-document invariant). The RFC + /// 0019 clause-7 cap applies to the TARGET-field path only, where the + /// target's app owns the full text. IME commit note: the immediate seed + /// here supersedes any pending deferred seed. + /// + private void SeedPaneText(MainWindowViewModel vm, TextBox box, string paneText) + { + _imeSeedPending = false; // this seed supersedes the deferred one + _imeRecheckTimer?.Stop(); + var caretBytes = NativeBridge.dasher_byte_offset_from_utf16(paneText, box.CaretIndex); + NativeBridge.dasher_seed_buffer(vm.Handle, paneText, caretBytes); + KbLog($"Editor sync: seeded {paneText.Length} chars, caret u16={box.CaretIndex} → byte={caretBytes}"); + } + /// /// RFC 0019 clause 5 — New = full reset (v5's SetBuffer(0)): buffer AND /// model context AND the rate window. dasher_reset_output_text alone @@ -932,18 +1023,24 @@ private async Task SeedContextFromTargetAsync(string reason) return; } + // RFC 0019 clause 7 — long-document seed cap (a 500k-char target + // document seeds only its trailing window; the caret moves with it). + var (seedText, seedCaretUtf16, truncated) = + EditorSeedPolicy.Clamp(context.Text, context.CaretUtf16); + // UIA carets are UTF-16 units; convert to the engine's UTF-8 bytes. - var byteOffset = NativeBridge.dasher_byte_offset_from_utf16(context.Text, context.CaretUtf16); - - // Shadow-compare skip (#58): if the target text EQUALS the engine - // buffer, the read carried no new information — either an echo of - // our own injection or an unchanged re-read (focus churn). A full - // re-seed would force a model rebuild = the visible canvas reset; - // at most the caret moved within unchanged text, which is exactly - // the cheap set_offset re-anchor (no rebuild). + var byteOffset = NativeBridge.dasher_byte_offset_from_utf16(seedText, seedCaretUtf16); + + // Shadow-compare skip (#58), cap-aware: the engine buffer holds the + // CLAMPED text, so the comparison must use the same clamp — comparing + // the full document against a capped buffer could never match and + // every caret move would re-seed (the #58 reset loop reborn for huge + // documents; review P1). Unchanged target → either an echo of our own + // injection or an unchanged re-read; at most the caret moved, which is + // exactly the cheap set_offset re-anchor (no rebuild). var enginePtr = NativeBridge.dasher_get_output_text(_vm.Handle); var engineText = enginePtr != IntPtr.Zero ? Marshal.PtrToStringUTF8(enginePtr) ?? "" : ""; - if (context.Text == engineText) + if (seedText == engineText) { if (byteOffset >= 0 && byteOffset != NativeBridge.dasher_get_offset(_vm.Handle)) { @@ -957,8 +1054,8 @@ private async Task SeedContextFromTargetAsync(string reason) return; } - KbLog($"Context seed ({reason}): {context.Text.Length} chars, caret u16={context.CaretUtf16} → byte={byteOffset}"); - NativeBridge.dasher_seed_buffer(_vm.Handle, context.Text, byteOffset); + KbLog($"Context seed ({reason}): {seedText.Length} chars{(truncated ? $" (capped from {context.Text.Length})" : "")}, caret u16={seedCaretUtf16} → byte={byteOffset}"); + NativeBridge.dasher_seed_buffer(_vm.Handle, seedText, byteOffset); } private int _contextSeedGeneration; diff --git a/tests/Dasher.Windows.Tests/EditorSeedPolicyTests.cs b/tests/Dasher.Windows.Tests/EditorSeedPolicyTests.cs new file mode 100644 index 0000000..e8eb15e --- /dev/null +++ b/tests/Dasher.Windows.Tests/EditorSeedPolicyTests.cs @@ -0,0 +1,86 @@ +using Dasher.Windows.Services; + +namespace Dasher.Windows.Tests; + +/// +/// RFC 0019 clause 7 — long-document seed cap (#57). Over-long pane/target +/// text seeds only a trailing window; the caret moves with the window. +/// +public class EditorSeedPolicyTests +{ + [Fact] + public void Under_cap_passes_through_untouched() + { + var (text, caret, truncated) = EditorSeedPolicy.Clamp("hello world", 5); + Assert.Equal("hello world", text); + Assert.Equal(5, caret); + Assert.False(truncated); + } + + [Fact] + public void Empty_text_passes_through() + { + var (text, caret, truncated) = EditorSeedPolicy.Clamp("", 0); + Assert.Equal("", text); + Assert.Equal(0, caret); + Assert.False(truncated); + } + + [Fact] + public void Over_cap_seeds_trailing_window_with_caret_adjusted() + { + // 150k chars; marker 'X' at the very start, 'Z' at the very end. + var big = new string('a', EditorSeedPolicy.MaxSeedUtf16 + 50_000); + var text = "X" + big[1..^1] + "Z"; + + var (seed, caret, truncated) = EditorSeedPolicy.Clamp(text, text.Length - 1); // caret on 'Z' + + Assert.True(truncated); + Assert.Equal(EditorSeedPolicy.MaxSeedUtf16, seed.Length); + Assert.EndsWith("Z", seed); + Assert.DoesNotContain("X", seed); // head dropped + Assert.Equal(seed.Length - 1, caret); // caret still on 'Z' within the window + } + + [Fact] + public void Caret_in_dropped_head_clamps_to_window_start() + { + var text = new string('a', EditorSeedPolicy.MaxSeedUtf16 + 10_000); + var (seed, caret, truncated) = EditorSeedPolicy.Clamp(text, 5); // caret in the dropped head + + Assert.True(truncated); + Assert.Equal(0, caret); + } + + [Fact] + public void Boundary_never_splits_a_surrogate_pair() + { + // 'a' * (Max+1) then an emoji (surrogate PAIR) straddling the cutoff: + // the window's first unit would be the emoji's LOW surrogate. + var emoji = "\U0001F600"; // U+1F600 = high + low surrogate + var text = new string('a', EditorSeedPolicy.MaxSeedUtf16 - 1) + emoji + new string('b', 10); + + var (seed, caret, truncated) = EditorSeedPolicy.Clamp(text, text.Length); + + Assert.True(truncated); + Assert.False(char.IsLowSurrogate(seed[0])); // never starts on a low half + Assert.Equal(seed.Length, caret); + + // And the seed is what the UTF-8 bridge can encode losslessly: + Assert.DoesNotContain('\uD800', seed.Select(c => c).Where(char.IsSurrogate).ToArray()); + var roundTrip = new string(seed.SkipWhile(char.IsLowSurrogate).ToArray()); + Assert.Equal(seed, roundTrip); + } + + [Fact] + public void Out_of_range_caret_clamps() + { + var (text, caret, _) = EditorSeedPolicy.Clamp("abc", 99); + Assert.Equal("abc", text); + Assert.Equal(3, caret); + + var (t2, c2, _) = EditorSeedPolicy.Clamp("abc", -1); + Assert.Equal("abc", t2); + Assert.Equal(0, c2); + } +}