Skip to content

Move finding search results to default dispatcher#1046

Merged
Crustack merged 3 commits into
mainfrom
fix/in-note-search-performance
May 31, 2026
Merged

Move finding search results to default dispatcher#1046
Crustack merged 3 commits into
mainfrom
fix/in-note-search-performance

Conversation

@Crustack
Copy link
Copy Markdown
Owner

@Crustack Crustack commented May 31, 2026

Fixes #1033

Summary by CodeRabbit

  • Bug Fixes

    • Search highlighting now runs asynchronously, cancels prior in-flight highlights, and avoids redundant retriggers for smoother, more responsive searching.
  • New Features

    • Jump-to-match selection reliably scrolls and positions the cursor when navigating results.
    • Highlights are capped to a safe limit to prevent UI overload and improve performance on large notes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4416617-0c7f-4025-97e0-808a4523f463

📥 Commits

Reviewing files that changed from the base of the PR and between 9c94d13 and da4e74e.

📒 Files selected for processing (3)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/adapter/ListItemVH.kt
  • app/src/main/java/com/philkes/notallyx/utils/SortedListExtensions.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/adapter/ListItemVH.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/utils/SortedListExtensions.kt

📝 Walkthrough

Walkthrough

Highlighting moved off the main thread: highlightSearchResults is suspend, occurrence detection runs on Dispatchers.Default, lifecycleScope.launch orchestrates calls, highlight APIs switched to bulk highlight/select, and findAllOccurrences was rewritten to an indexOf loop with a result cap.

Changes

Search highlighting background execution

Layer / File(s) Summary
Base contract & lifecycle orchestration
app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
Make highlightSearchResults suspend; updateSearchResults now calls it from lifecycleScope.launch; doAfterTextChanged avoids re-trigger when text equals current query.
Suspend indexed mapping utilities
app/src/main/java/com/philkes/notallyx/utils/SortedListExtensions.kt
Add suspend fun <R,C> SortedList<R>.mapIndexedSuspended(...) and suspend fun <R,C> List<R>.mapIndexedSuspended(...) to support suspending indexed transforms.
EditListActivity coroutine highlighting
app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt
Convert SortedList/List highlightSearch helpers and highlightSearchResults override to suspend; switch to mapIndexedSuspended and compute occurrences on Dispatchers.Default via withContext.
EditNoteActivity coroutine highlighting
app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt
Make highlightSearchResults a suspend override, compute occurrences on Dispatchers.Default using withContext, assign searchResultIndices, and use binding.EnterBody.select(start,end) for selection navigation.
EditTextPlainActivity coroutine highlighting
app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditTextPlainActivity.kt
Change highlightSearchResults to suspend, compute findAllOccurrences on Dispatchers.Default, apply binding.EnterBody.highlight(occurrences, -1) in bulk, set searchResultIndices, and use select for navigation.
Highlightable view bulk API
app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt
Add SEARCH_HIGHLIGHT_LIMIT, new highlight(newOccurrences: List<Pair<Int,Int>>, selectedIndex: Int = -1), and select(startIdx,endIdx): Int?; remove old single-range highlight(start,end,selected) and update unselectHighlight.
List item binding changes
app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/adapter/ListItemVH.kt
Refactor bind() to build start/end pairs, call highlight(pairs, -1) once, and separately select the currently selected range; clear highlights when null.
findAllOccurrences implementation
app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt
Replace regex-based matching with an indexOf loop, return early for empty search, respect case sensitivity, and cap results at 20,000 to limit work on huge texts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Crustack/NotallyX#975: Related changes to HighlightableEditText span removal behavior that intersect with this PR's highlight/span pipeline.

Poem

🐰 I hopped through code to make search light and kind,
Off the main thread now, no freeze left behind.
Suspended hops, highlights in tidy rows,
A thousand little sparks where the cursor goes.
Quiet thumps, small paws—smooth typing you'll find.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: moving expensive search result computation to the default dispatcher to prevent main-thread blocking.
Linked Issues check ✅ Passed The PR addresses issue #1033 by moving regex/matching operations off the main thread via suspend functions and default dispatcher, preventing ANR freezes during large note processing.
Out of Scope Changes check ✅ Passed All changes are in-scope: converting search highlighting to async operations, moving computation to default dispatcher, updating highlight API, and optimizing string matching to address the ANR issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/in-note-search-performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt (1)

167-184: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep checked-list refresh state separate from the main list.

Lines 172-184 reuse one alreadyNotifiedItemPos set for both adapters and then send the checked-section fallback refresh through adapter. If index 0 is highlighted in the main list, checked item 0 is treated as already refreshed too, and any non-highlighted checked rows are notified on the wrong adapter. That leaves stale highlights in the checked section.

Proposed fix
     override suspend fun highlightSearchResults(search: String): Int {
         val resultPosCounter = AtomicInteger(0)
-        val alreadyNotifiedItemPos = mutableSetOf<Int>()
+        val alreadyNotifiedMainItemPos = mutableSetOf<Int>()
+        val alreadyNotifiedCheckedItemPos = mutableSetOf<Int>()
         adapter?.clearHighlights()
         adapterChecked?.clearHighlights()
         val amount =
-            items.highlightSearch(search, adapter, resultPosCounter, alreadyNotifiedItemPos) +
+            items.highlightSearch(search, adapter, resultPosCounter, alreadyNotifiedMainItemPos) +
                 (itemsChecked?.highlightSearch(
                     search,
                     adapterChecked,
                     resultPosCounter,
-                    alreadyNotifiedItemPos,
+                    alreadyNotifiedCheckedItemPos,
                 ) ?: 0)
         items.indices
-            .filter { !alreadyNotifiedItemPos.contains(it) }
+            .filter { !alreadyNotifiedMainItemPos.contains(it) }
             .forEach { adapter?.notifyItemChanged(it) }
         itemsChecked
             ?.indices
-            ?.filter { !alreadyNotifiedItemPos.contains(it) }
-            ?.forEach { adapter?.notifyItemChanged(it) }
+            ?.filter { !alreadyNotifiedCheckedItemPos.contains(it) }
+            ?.forEach { adapterChecked?.notifyItemChanged(it) }
         return amount
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt`
around lines 167 - 184, The shared alreadyNotifiedItemPos set is used for both
main and checked lists causing index collisions and the final notify loop for
the checked section calls the wrong adapter; split the state and notifications
so each list tracks its own refreshed indexes: create a separate
alreadyNotifiedItemPosChecked (or similar) and pass it into the
itemsChecked.highlightSearch call, then notify changes for the main list with
adapter.notifyItemChanged(...) and for the checked list with
adapterChecked.notifyItemChanged(...), ensuring highlightSearch is invoked with
the correct adapter/sets (references: highlightSearch, items, itemsChecked,
adapter, adapterChecked, resultPosCounter).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt`:
- Around line 424-438: The updateSearchResults function currently launches a new
lifecycleScope.launch for each invocation causing concurrent searches; fix this
by introducing a cancellable Job field (e.g., a private var searchJob: Job?) at
the class level, cancel the existing searchJob if active before starting a new
one, then assign the new lifecycleScope.launch to searchJob and run
highlightSearchResults inside it (keeping the existing Dispatchers.Default usage
inside highlightSearchResults). Ensure you still update search.results.value and
search.resultPos.value only from the latest job so canceled jobs do not
overwrite newer results.

In `@app/src/main/java/com/philkes/notallyx/utils/SortedListExtensions.kt`:
- Around line 13-19: The mapIndexedSuspended extensions iterate live collections
and may resume after the underlying data changes; snapshot first, then suspend
over the snapshot. In SortedList<R>.mapIndexedSuspended, create an immutable
list of elements (and their order) from the current SortedList before any
suspending calls, then iterate that snapshot with indices to invoke transform.
In List<R>.mapIndexedSuspended, create an immutable copy of the list and iterate
that copy with indices to invoke transform. Ensure no access uses this[...] or
size() from the live collection during iteration; rely solely on the snapshot.

---

Outside diff comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt`:
- Around line 167-184: The shared alreadyNotifiedItemPos set is used for both
main and checked lists causing index collisions and the final notify loop for
the checked section calls the wrong adapter; split the state and notifications
so each list tracks its own refreshed indexes: create a separate
alreadyNotifiedItemPosChecked (or similar) and pass it into the
itemsChecked.highlightSearch call, then notify changes for the main list with
adapter.notifyItemChanged(...) and for the checked list with
adapterChecked.notifyItemChanged(...), ensuring highlightSearch is invoked with
the correct adapter/sets (references: highlightSearch, items, itemsChecked,
adapter, adapterChecked, resultPosCounter).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73bd2e29-694b-4b38-b1a3-fda97d8febb3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d42f90 and da58ffd.

📒 Files selected for processing (5)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditTextPlainActivity.kt
  • app/src/main/java/com/philkes/notallyx/utils/SortedListExtensions.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt (1)

139-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

unselectHighlight() now wipes every other highlight.

The new highlight(newOccurrences, ...) first clears highlightedSpans and removes all existing spans (Lines 68-70) before rebuilding only from the passed list. Since unselect() calls highlight(listOf(Pair(previousHighlightedStartIdx, previousHighlightedEndIdx)), -1), deselecting a result (e.g. EditNoteActivity.selectSearchResult(resultPos < 0)) removes all other search highlights, leaving just the single previously-selected range. This is a regression from the old single-range highlight semantics.

Revert just this span to a dimmed highlight instead of going through the bulk API:

🐛 Proposed fix
     private fun CharacterStyle.unselect() {
-        val (previousHighlightedStartIdx, previousHighlightedEndIdx) = getSpanRange(this)
-        if (previousHighlightedStartIdx != -1) {
-            removeSpan(this)
-            highlight(listOf(Pair(previousHighlightedStartIdx, previousHighlightedEndIdx)), -1)
-        }
+        val (start, end) = getSpanRange(this)
+        if (start != -1) {
+            val editable = text ?: return
+            editable.removeSpan(this)
+            highlightedSpans.remove(this)
+            if (this == selectedHighlightedSpan) selectedHighlightedSpan = null
+            val dimmedSpan = HighlightSpan(highlightColor.withAlpha(0.1f))
+            editable.setSpan(dimmedSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            highlightedSpans.add(dimmedSpan)
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt`
around lines 139 - 145, CharacterStyle.unselect() currently calls
highlight(listOf(Pair(previousHighlightedStartIdx, previousHighlightedEndIdx)),
-1) which resets highlightedSpans and removes all other spans; instead, after
removeSpan(this) re-apply only a dimmed span for the same range so other
highlights remain. Concretely: inside CharacterStyle.unselect() use
getSpanRange(this) as before, call removeSpan(this), then add a dimmed variant
for (previousHighlightedStartIdx, previousHighlightedEndIdx) (e.g., by
creating/setting the appropriate CharacterStyle or calling the existing
add/set-span helper instead of highlight(...)), and update highlightedSpans to
include that single dimmed range without touching other entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt`:
- Line 12: The constant SEARCH_HIGHLIGHT_LIMIT (1_000) used in
HighlightableEditText.highlight() diverges from the 20_000 cap used by
findAllOccurrences, causing the UI to only render 1,000 highlights while
searchResultIndices can report up to 20,000 matches; update the code so the two
limits are consistent by either increasing SEARCH_HIGHLIGHT_LIMIT to match
findAllOccurrences' cap (20_000) or reduce/centralize the cap so both
findAllOccurrences and highlight() reference the same shared limit constant, and
ensure any UI code that relies on searchResultIndices (e.g., select or
navigation code) handles the unified cap accordingly.

---

Outside diff comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt`:
- Around line 139-145: CharacterStyle.unselect() currently calls
highlight(listOf(Pair(previousHighlightedStartIdx, previousHighlightedEndIdx)),
-1) which resets highlightedSpans and removes all other spans; instead, after
removeSpan(this) re-apply only a dimmed span for the same range so other
highlights remain. Concretely: inside CharacterStyle.unselect() use
getSpanRange(this) as before, call removeSpan(this), then add a dimmed variant
for (previousHighlightedStartIdx, previousHighlightedEndIdx) (e.g., by
creating/setting the appropriate CharacterStyle or calling the existing
add/set-span helper instead of highlight(...)), and update highlightedSpans to
include that single dimmed range without touching other entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b542e83e-ee05-452d-9440-d5b94e525727

📥 Commits

Reviewing files that changed from the base of the PR and between da58ffd and 9c94d13.

📒 Files selected for processing (6)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditTextPlainActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/misc/highlightableview/HighlightableEditText.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/note/listitem/adapter/ListItemVH.kt
  • app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditTextPlainActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt

@Crustack Crustack merged commit 76e1572 into main May 31, 2026
1 check passed
@Crustack Crustack deleted the fix/in-note-search-performance branch May 31, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App freezes on launch after large import

1 participant