Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import com.ichi2.anki.preferences.SharedPreferencesProvider
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.settings.PrefsRepository
import com.ichi2.anki.utils.ext.currentCardBrowse
import com.ichi2.anki.utils.ext.normalizeForSearch
import com.ichi2.anki.utils.ext.normalizeForSearchPreservingTags
import com.ichi2.anki.utils.ext.setUserFlagForCards
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -1178,7 +1178,7 @@ class CardBrowserViewModel(
}
flowOfSearchTerms.value =
if (shouldIgnoreAccents) {
searchQuery.normalizeForSearch()
searchQuery.normalizeForSearchPreservingTags()
} else {
searchQuery
}
Expand Down
23 changes: 23 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/String.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import java.util.regex.Pattern

private val DIACRITICAL_MARKS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+")

/** Matches `tag:` + quoted or unquoted value. Left verbatim so backend matches tags exactly. */
private val TAG_SEGMENT_PATTERN = Pattern.compile("tag:(?:\"[^\"]*\"|[^\\s\\)]+)")

/**
* Normalizes the given string by removing diacritical marks (accents) to enable accent-insensitive searches.
*
Expand All @@ -42,3 +45,23 @@ fun String.normalizeForSearch(): String {
val normalized = Normalizer.normalize(this, Normalizer.Form.NFD)
return DIACRITICAL_MARKS_PATTERN.matcher(normalized).replaceAll("")
}

/**
* Normalizes query for accent-insensitive search but keeps `tag:...` segments unchanged
* (backend matches tags literally). Uses [normalizeForSearch] only outside tag segments.
*
* @receiver Raw search query (e.g. `café ("tag:être")`).
* @return Query with accents removed except inside `tag:...`.
*/
fun String.normalizeForSearchPreservingTags(): String {
val matcher = TAG_SEGMENT_PATTERN.matcher(this)
val sb = StringBuilder(length)
var cursor = 0
while (matcher.find()) {
sb.append(substring(cursor, matcher.start()).normalizeForSearch())
sb.append(matcher.group())
cursor = matcher.end()
}
sb.append(substring(cursor).normalizeForSearch())
return sb.toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package com.ichi2.anki.browser
import androidx.core.content.edit
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import anki.config.ConfigKey
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.CardBrowser
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.Flag
import com.ichi2.anki.NoteEditorActivity
import com.ichi2.anki.NoteEditorFragment
Expand Down Expand Up @@ -70,6 +72,7 @@ import com.ichi2.anki.libanki.QueueType
import com.ichi2.anki.libanki.QueueType.ManuallyBuried
import com.ichi2.anki.libanki.QueueType.New
import com.ichi2.anki.libanki.testutils.AnkiTest
import com.ichi2.anki.model.CardStateFilter
import com.ichi2.anki.model.CardsOrNotes
import com.ichi2.anki.model.SelectableDeck
import com.ichi2.anki.model.SortType
Expand Down Expand Up @@ -1360,6 +1363,21 @@ class CardBrowserViewModelTest : JvmTest() {
}
}

@Test
fun `accented tags are searchable if ignoring accents - Issue 20409`() =
runTest {
addBasicNote().update { tags = mutableListOf("être") }
withCol { config.setBool(ConfigKey.Bool.IGNORE_ACCENTS_IN_SEARCH, true) }
runViewModelTest(notes = 0) {
flowOfSearchState.test {
filterByTags(listOf("être"), CardStateFilter.ALL_CARDS)
assertThat(awaitItem(), equalTo(CardBrowserViewModel.SearchState.Searching))
assertThat(awaitItem(), equalTo(CardBrowserViewModel.SearchState.Completed))
assertThat(rowCount, equalTo(1))
}
}
}

private fun assertDate(str: String?) {
// 2025-01-09 @ 18:06
assertNotNull(str)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.ichi2.anki.browser

import com.ichi2.anki.utils.ext.normalizeForSearch
import com.ichi2.anki.utils.ext.normalizeForSearchPreservingTags
import org.junit.Assert.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
Expand All @@ -37,4 +38,18 @@ class StringNormalizationTest {
) {
assertEquals(expected, input.normalizeForSearch())
}

@ParameterizedTest
@CsvSource(
"café tag:foo, cafe tag:foo",
"tag:être, tag:être",
"(\"tag:être\" or \"tag:foo\"), (\"tag:être\" or \"tag:foo\")",
"résumé tag:naïve, resume tag:naïve",
)
fun `test normalizeForSearchPreservingTags preserves tag segments`(
input: String,
expected: String,
) {
assertEquals(expected, input.normalizeForSearchPreservingTags())
}
}
Loading