Skip to content

fix(instant-editor): keep symbols around clozes - #22864

Open
GuTS805 wants to merge 1 commit into
ankidroid:mainfrom
GuTS805:fix/instant-editor-cloze-drops-text
Open

GuTS805 wants to merge 1 commit into
ankidroid:mainfrom
GuTS805:fix/instant-editor-cloze-drops-text

Conversation

@GuTS805

@GuTS805 GuTS805 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Note

Assisted-by: Claude Opus 5.5

Purpose / Description

Tapping a chip in the Instant Card dropped any text outside the regex match, so C++ became {{c1::C}}, 20°C became {{c1::20}}, and an emoji became null{{c1::null}}null.

Fixes

Approach

How Has This Been Tested?

  • New unit tests, which fail without the fix. All 47 instant editor tests pass, ktlint + lint are clean
  • Emulator (API 35), sharing I like C++ and $100 at 20°C café 🙂 e=mc² Zürich:
    • clozed every chip, switched edit modes, undid them: the text came back exactly
    • saved the note, checked in the Card Browser: I like {{c1::C}}++ and ${{c2::100}} at {{c3::20}}°C café 🙂 e=mc² {{c4::Zürich}}
Before After
instant-cloze-null-text instant-cloze-fixed-saved-note

Checklist

  • You have a descriptive commit message with a short title (first line, max. 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@GuTS805

GuTS805 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review..

david-allison

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Oct 29, 2026
@GuTS805
GuTS805 force-pushed the fix/instant-editor-cloze-drops-text branch from 13c01dd to f5023b2 Compare October 29, 2026 21:24
@david-allison
david-allison self-requested a review October 29, 2026 22:01
@david-allison david-allison removed the Needs Author Reply Waiting for a reply from the original author label Oct 29, 2026

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assisted-by: grok-4.7

Thanks for tackling the instant-editor cloze chip bugs — the degree sign, emoji, and renumber cases look solid in the tests you added.

Locally I checked out the PR tip, rebased the single commit onto current main, and did not push. Lint, unit tests, and the emulator suite all passed on emulator-5556 / API35_GAPI.

EditorChips.setupChipGroup loads getWordsFromFieldText, sets each chip label with getCleanClozeWords, and on click calls buildClozeText then updateChipData, which joins every chip with one space and setClozeFieldText. Long-press calls updateClozeNumber on that chip's raw word. Switching back from advanced edit calls setupChipGroup again on the text box.

buildClozeText still returns processClozeUndo when clozePattern.find hits. Otherwise it takes the first clozeBuilderPattern match. A miss returns the input and does not increment currentClozeNumber or append intClozeList. Groups 1, 2, and 4 are written back with replaceRange(matcher.range, …), so 20°C becomes {{c1::20}}°C and €100 becomes €{{c1::100}}. Undo uses the same replaceRange, so the degree sign and the euro stay. getCleanClozeWords is clozePattern.replace, so the chip label for {{c1::20}}°C is 20°C.

getWordsFromFieldText splits on \s+ when the space's first index is outside a clozePattern range, then splitBetweenClozes cuts each piece at every {{c<digits>:: after the first. combineWordsWithPunctuation still appends a punctuation-only token only when the previous word clozePattern.matches entirely, which {{c1::20}}°C does not.

Requesting changes: getWordsFromFieldText still builds cloze ranges with clozePattern, whose trailing punctuation class includes {. On text like {{c1::born in}}-{{c2::New York}}, findAll matches through {{c1::born in}}-{{ and never sees the start of the second cloze. The space inside New York falls outside that range, so the word splitter breaks the markup into chips {{c1::born in}}-, {{c2::New, and York}}. The same cut happens when two clozes abut with only punctuation between them. Cloze ranges need to end at }} without swallowing the next {{. Please add a regression test that fails on today's chip split for a later cloze that contains a space (for example {{c1::born in}}-{{c2::New York}}).

some acceptable findings, no action required, just leaving for posterity: the comment above the clozeBuilderPattern groups still says "first, second, and third regex groups" while the code uses groups 1, 2, and 4.

@mikehardy mikehardy added Needs Author Reply Waiting for a reply from the original author and removed Needs Review labels Nov 1, 2026
@GuTS805
GuTS805 force-pushed the fix/instant-editor-cloze-drops-text branch from f5023b2 to 6953a23 Compare November 1, 2026 21:52
@GuTS805
GuTS805 requested a review from mikehardy November 1, 2026 21:53
@mikehardy mikehardy removed the Needs Author Reply Waiting for a reply from the original author label Nov 1, 2026
@GuTS805

GuTS805 commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Fixed: cloze ranges now end at }} so the next cloze's {{ isn't swallowed. Added a test with your {{c1::born in}}-{{c2::New York}} example.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
david-allison

This comment was marked as resolved.

Tapping `20°C` produced `{{c1::20}}` and an emoji produced
`null{{c1::null}}null`, as text outside the regex match was dropped.

Text touching a cloze now also stays in the same chip, so re-reading
the field doesn't split `{{c1::20}}°C` into two chips and add a space.
A chip still holds at most one cloze, so renumbering it only changes
that cloze. A nested cloze like `{{c1::H2{{c2::O}}}}` stays one chip.

Fixes 22860

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
Assisted-by: Claude Opus 5.5
@GuTS805
GuTS805 force-pushed the fix/instant-editor-cloze-drops-text branch from 6953a23 to a6f0a35 Compare November 2, 2026 22:40
@david-allison david-allison added Needs Review and removed Needs Author Reply Waiting for a reply from the original author labels Nov 2, 2026

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assisted-by: Grok 4.7

Thanks for the outerClozeRanges chip split — the born-in/New York and nested formula cases look right on the word path.

Locally I rebased onto main without force-pushing (prep stayed at the PR tip). I ran lint (lintAll, ktlintCheck, lint-rules:test), unit tests (jacocoUnitTestReport), and release emulator tests (TEST_RELEASE_BUILD=true via run_emulator_tests.py on emulator-5556 / API35_GAPI); all passed.

Requesting changes: setClozeFieldText still seeds intClozeList from getClozeOrdinals, which scans with clozePattern (not outerClozeRanges). On {{c1::born in}}-{{c2::New York}} extra, the pattern only sees c1, so the next chip you add becomes c2 again beside the existing c2. The same happens on {{c1::H2{{c2::O}}}} water — the inner c2 is not counted separately, so clozeing water also lands on c2. Please enumerate every {{c<n>:: (including nested) for the next-number path and add a regression that expects c3 after clozeing the plain word in both strings.

some acceptable findings, no action required, just leaving for posterity: the nested-formula test asserts the appended chip text rather than the next ordinal explicitly.

@mikehardy mikehardy added Needs Author Reply Waiting for a reply from the original author and removed Needs Review labels Nov 3, 2026

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse of an existing cloze ordinal

diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorClozeNumberingRegressionTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorClozeNumberingRegressionTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7a9facbc18e77bace502868a11f1a6ded0dca098
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorClozeNumberingRegressionTest.kt
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.instanteditor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.RobolectricTest
+import com.ichi2.anki.instanteditor.InstantEditorViewModelTest.Companion.runInstantEditorViewModelTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class InstantEditorClozeNumberingRegressionTest : RobolectricTest() {
+    @Test
+    fun `a new chip does not reuse a touching cloze ordinal`() =
+        runInstantEditorViewModelTest {
+            setClozeFieldText("{{c1::born in}}-{{c2::New York}} extra")
+            val words = getWordsFromFieldText().toMutableList()
+            val extraIndex = words.indexOf("extra")
+
+            words[extraIndex] = buildClozeText(words[extraIndex])
+            setClozeFieldText(words.joinToString(" "))
+
+            assertEquals("{{c3::extra}}", words[extraIndex])
+        }
+
+    @Test
+    fun `a new chip does not reuse a nested cloze ordinal`() =
+        runInstantEditorViewModelTest {
+            setClozeFieldText("{{c1::H2{{c2::O}}}} water")
+            val words = getWordsFromFieldText().toMutableList()
+            val waterIndex = words.indexOf("water")
+
+            words[waterIndex] = buildClozeText(words[waterIndex])
+            setClozeFieldText(words.joinToString(" "))
+
+            assertEquals("{{c1::H2{{c2::O}}}} {{c3::water}}", actualClozeFieldText.value)
+        }
+}

Renumbering of a nested sibling

diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorNestedClozeRenumberingRegressionTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorNestedClozeRenumberingRegressionTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dddff8c4d94799d7f4384dadd31d052541fc71c0
--- /dev/null
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/instanteditor/InstantEditorNestedClozeRenumberingRegressionTest.kt
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package com.ichi2.anki.instanteditor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.ichi2.anki.RobolectricTest
+import com.ichi2.anki.instanteditor.InstantEditorViewModelTest.Companion.runInstantEditorViewModelTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class InstantEditorNestedClozeRenumberingRegressionTest : RobolectricTest() {
+    @Test
+    fun `renumbering an outer chip preserves every nested cloze ordinal`() =
+        runInstantEditorViewModelTest {
+            setClozeFieldText("{{c1::Water contains {{c2::hydrogen}} and {{c3::oxygen}}.}}")
+            val words = getWordsFromFieldText().toMutableList()
+
+            // This is the update performed after long-pressing the outer chip.
+            words[0] = updateClozeNumber(words[0], 4)
+            setClozeFieldText(words.joinToString(" "))
+
+            assertEquals(
+                "{{c4::Water contains {{c2::hydrogen}} and {{c3::oxygen}}.}}",
+                actualClozeFieldText.value,
+            )
+        }
+}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Author Reply Waiting for a reply from the original author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Instant Card: tapping a word deletes symbols from it, and an emoji becomes null{{c1::null}}null

3 participants