Skip to content

iOS: search results show the line where the body match was found - #28

Open
cjbest wants to merge 6 commits into
mainfrom
claude/ios-search-snippet
Open

iOS: search results show the line where the body match was found#28
cjbest wants to merge 6 commits into
mainfrom
claude/ios-search-snippet

Conversation

@cjbest

@cjbest cjbest commented May 5, 2026

Copy link
Copy Markdown
Owner

When you type a query that matches a note's body, the little context line in each result row now shows the actual line of the document where the match is — not the generic second-line preview.

  • Title-only matches keep the regular preview (the title itself is the match indicator).
  • Body matches show the trimmed matching line, capped at 140 chars with "…" when longer.
  • Original casing is preserved in the snippet; matching itself stays case-insensitive.

Implementation:

  • `search(_:)` returns `[SearchHit]` (note + optional snippet) instead of `[Note]`.
  • `bodyCache` switched from lower-cased to original-case so snippets read naturally.
  • 3 new unit tests, plus existing tests adapted: 27 unit + 4 UI = 31 green.

🤖 Generated with Claude Code

cjbest and others added 6 commits May 4, 2026 23:54
When the search query matches a note's body (rather than its title),
the row's context line now shows the actual line of the document where
the match was found — not the static second-line preview. Title-only
matches keep using the regular preview, since the title itself is the
match indicator.

- search(_:) now returns [SearchHit] (note + optional snippet) instead
  of [Note]. Snippet is the trimmed line containing the first body
  match, capped at 140 chars with an ellipsis if longer.
- bodyCache stores original-case strings so snippets render with the
  source's casing (matching is still case-insensitive via range(of:
  options:.caseInsensitive)).
- NoteRow takes a `searchSnippet:` override that replaces note.preview
  when present.
- 3 new tests cover snippet line extraction, case preservation, and
  long-line truncation. Existing search tests updated for the new
  return type. 27 unit + 4 UI = 31 green.
Two follow-ups on PR #28:

- Pre-window the snippet so a match deep in a long line is visible:
  if the match is more than ~18 chars from the start of the trimmed
  line, drop everything before that and prepend a "…". The row's
  lineLimit(1) then truncates the trailing end (with a "…" if we
  capped it at 200 chars). Two new tests pin both branches.

- Dismiss search when selectedNote changes via .onChange. Without it,
  the .searchable bar persisted into the editor's chrome and the
  editor's .toolbar(.hidden) didn't take effect. Initially tried a
  simultaneousGesture on the NavigationLink — that broke navigation
  in UI tests because the gesture stole the tap. .onChange reacts
  after the selection lands, no gesture conflict.

29 unit + 4 UI = 33 green.
- NoteRow builds an AttributedString from the snippet and bolds the
  span matching the query (with primary color, so it stands out a bit
  more against the .secondary surrounding text).
- Switch .searchable to the isPresented: form. Setting it to false in
  .onChange(of: selectedNote) actually collapses the search bar — the
  previous dismissSearch() call only removed focus, leaving the bar
  visible in the navigation chrome of the pushed editor.
- Fold the SearchableNoteList back into NoteListView's `list` since
  we no longer need the dismissSearch environment access.
Two related fixes addressing the same root cause: SwiftUI's
.toolbar(.hidden, for: .navigationBar) doesn't fully take effect when
the parent view has .searchable applied — the back chevron leaks
through, leaving a bar with a back button visible on the editor.

- EditorChrome.viewWillAppear now calls setNavigationBarHidden(true)
  imperatively on the underlying UINavigationController. This is the
  reliable hammer that overrides the parent searchable's chrome.
- Drop the previous isPresented: $isSearchPresented hack on .searchable
  along with the onChange that flipped it to false on selection.
  That worked around the wrong problem and broke the back-trip: the
  search bar collapsed but the query stayed populated, so the user
  saw filtered results with no way to clear or edit. Now the search
  bar persists across navigation in the standard way — go forward,
  read a note, swipe back, search bar still showing your query.
The webview kept restoring whatever file was last open by reading
localStorage on mount, which was the right behaviour for HMR and page
reloads but wrong for a real app launch.

Use sessionStorage as the discriminator: it survives reloads and HMR
but gets wiped when the webview is destroyed (app quit), so its
absence on mount is a reliable signal that this is a cold launch.
When that happens, drop any stale registration for this window label
and start blank.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new section under Shortcuts showing the notes list and editor
side-by-side, captured from iPhone 17 Pro with the device bezel.

The screenshots are reproducible via testCaptureReadmeScreenshots in
DriftUITests, which seeds four fake notes, navigates to each state,
and synchronises with a host-side screencapture orchestrator via
marker files in /tmp. XCUIScreen.screenshot() gives only the device
display; the bezel comes from screencapturing the Simulator window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant