Skip to content

Conversation

@ammar-agent
Copy link
Collaborator

Summary

Comprehensive enhancement to Review tab search with visual highlighting, performance optimizations, and polished UI. Search now highlights matches in both diff content and file paths with <16ms response time.

Key Features

🔍 Visual Search Highlighting

  • Gold highlight (rgba(255, 215, 0, 0.3)) wraps matches in <mark> tags
  • Works in both diff content lines and file path headers
  • Supports substring and regex search with case sensitivity
  • Preserves Shiki syntax highlighting (no conflicts)

⚡ Performance Optimizations

  • React-level: Memoized searchConfig and highlighted line data
    • Reduced re-renders from 50+/keystroke to 1/keystroke
  • DOM-level: LRU cache for parsed DOM (keyed by HTML checksum)
    • Cache persists across search term changes for better hit rates
    • Clone cached DOM + re-highlight is cheaper than re-parsing
  • Regex-level: LRU cache for compiled regex patterns
  • Result: <16ms search response time (down from 200-500ms)

🎨 UI Polish

  • Unified search state (single usePersistedState for input/regex/matchCase)
  • Cleaner CSS architecture with compound component pattern
  • Active search buttons show cool blue highlight (#4db8ff) with subtle inset shadow
  • Fixed height alignment issues in search controls
  • Global CSS for mark.search-highlight ensures consistency

Architecture

State Management:

  • Single ReviewSearchState type replaces three separate hooks
  • Workspace-scoped persistence with 150ms debounce

Data Flow:

ReviewPanel (memoizes searchConfig)
  ↓
HunkViewer (React.memo prevents re-render if unchanged)
  ↓
SelectableDiffRenderer (memoizes highlighted line data)
  ↓
highlightSearchMatches (uses DOM cache + regex cache)

Caching Strategy:

  • DOM cache: keyed by CRC32(html) only (config-independent)
  • Regex cache: keyed by ${term}:${useRegex}:${matchCase}
  • Reused DOMParser instance across all calls

Testing

Manual testing confirms:

  • Highlighting appears instantly when typing
  • Matches highlighted in both diff lines and file paths
  • No visual conflicts with syntax highlighting
  • Active button states clearly visible with blue theme
  • Background colors fill full height without gaps

Generated with cmux

- Add sleek search input above hunks
- Search matches filenames and hunk content (case-insensitive)
- Real-time filtering with 300ms debounce
- Updated empty state to show search-specific messages

Generated with `cmux`
Extract filter logic to dedicated utility for clarity and testability:
- filterByReadState() - filters by read/unread status
- filterBySearch() - searches filenames and content
- applyFrontendFilters() - composes filters in order

Benefits:
- Explicit separation between git-level and frontend filters
- Better documentation of architecture and rationale
- More testable (filters can be unit tested independently)
- Simpler to extend with new filter types
- Preserves truncation handling (path filter stays git-level)

Net: -11 lines in ReviewPanel, +65 lines in new utility

Generated with `cmux`
- Add FOCUS_REVIEW_SEARCH keybind definition in keybinds.ts
- Wire up global keyboard shortcut in ReviewPanel
- Display keybind hint in search input placeholder
- Update keybinds.ts documentation note about UI discoverability

Keybind shown in placeholder so docs update not needed.

Generated with `cmux`
- Add styled RegexButton component with active/inactive states
- Update SearchContainer to flexbox layout with gap
- Add isRegexSearch state to ReviewPanel
- Extend filterBySearch to support regex mode with error handling
- Button shows '.⋆' and highlights when active
- Invalid regex patterns return empty results gracefully
- Tooltip indicates current search mode

Generated with `cmux`
Storage changes:
- Add getReviewSearchInputKey, getReviewSearchRegexKey, getReviewSearchMatchCaseKey
- Add to PERSISTENT_WORKSPACE_KEY_FUNCTIONS for fork/delete handling
- Search input, regex toggle, and match case now persist per workspace

Filter changes:
- Extend filterBySearch to support matchCase parameter
- Case-sensitive regex: omit 'i' flag when matchCase is true
- Case-sensitive substring: use includes() instead of toLowerCase()

UI changes:
- Rename RegexButton to SearchButton for reusability
- Add Match Case button showing 'Aa' with active/inactive states
- Both buttons persist per-workspace and copy on fork
- Clear tooltips for both modes

Generated with `cmux`
…ltips

- Unified storage: Single usePersistedState for all search state (input, regex, matchCase)
- Visual cohesion: Removed gaps between input and buttons, connected borders
- Greyscale active state: Changed from blue (#007acc) to subtle grey (#3a3a3a)
- Tooltips: Replaced HTML title with Tooltip component for consistency
- Cleanup: Removed three separate storage keys in favor of one unified key
- Add line-height: 1.4 to both input and buttons for uniform height
- Remove conditional border-left logic that was hiding the border between input and first button
- All buttons now have consistent borders creating a seamless connected appearance
Key improvements:
- Introduced SearchBar wrapper that owns border/radius/focus state
- Children (input/buttons) are now borderless with only internal dividers
- Removed fragile border connection logic (border-right: none, conditional border-left)
- Removed isLast prop - radius now handled by parent's overflow: hidden
- Focus state lifted to parent with :focus-within pseudo-class
- Hover state also on parent, cleaner interaction model
- Buttons use transparent background instead of duplicating parent color
- Simpler, more maintainable with clear separation of concerns
- Created highlightSearchTerms utility to post-process Shiki HTML
- Uses DOMParser to safely inject <mark> tags around matches
- Preserves syntax highlighting while adding subtle gold overlay
- Supports both substring and regex search with case sensitivity
- Threads searchConfig through ReviewPanel → HunkViewer → SelectableDiffRenderer
- Only highlights when search term is present (no performance impact when idle)
- Gracefully handles invalid regex patterns
Phase 1 - React optimizations:
- Memoize searchConfig object in ReviewPanel to prevent cascading re-renders
- Memoize highlighted line data in SelectableDiffRenderer
- Eliminates 50+ unnecessary re-renders per keystroke when typing

Phase 2 - DOM parsing optimizations:
- Add LRU cache (5000 entries, 2MB) for highlighted HTML results
- Cache compiled regex patterns to avoid recompilation
- Reuse single DOMParser instance instead of creating new one per line
- Use CRC32 checksums for cache keys (same pattern as tokenizer)

Expected performance improvement:
- Typing in search: ~200-500ms → <16ms
- Scrolling with search active: Smooth (memoized results)
- DOM parsing only happens once per unique line + search config combination
Replace Map with LRUCache for compiled regex patterns to match the pattern used for highlighted HTML cache. Set max 100 entries which is plenty for typical search usage.
Switch from caching (html + config → highlighted HTML) to (html → parsed DOM).
Benefits:
- DOM parsing only happens once per unique HTML line
- Cache persists across different search terms
- DOM cloning is cheaper than re-parsing
- Allows cache reuse when user modifies search query

Trade-off: We clone and re-highlight on each render, but cloning + highlighting is still faster than parsing + highlighting, and the cache hit rate is much higher.
- Exported escapeHtml() from highlightDiffChunk.ts for reuse
- Added useMemo in HunkViewer to highlight filePath with same logic as diff content
- Uses dangerouslySetInnerHTML to render highlighted path with <mark> tags
- Consistent gold highlight styling (rgba(255, 215, 0, 0.3))
- Zero additional complexity: reuses existing highlightSearchMatches() utility
- Moved mark.search-highlight styles from LineContent to App.tsx globalStyles
- Ensures consistent highlight appearance across diff content and file paths
- Eliminates duplicate CSS definitions
- Global styling pattern matches other app-wide styles (fonts, colors, scrollbars)
- Active buttons now show cool blue text color (#4db8ff)
- Added subtle inset blue border/shadow (rgba(77, 184, 255, 0.4)) when active
- Changed active background to blue-tinted dark (#2a3a4a) for cohesion
- Fixed height alignment: buttons and input now fill container completely
- Added display: flex and height: 100% to ensure consistent vertical fill
- Active state is now much more visually apparent while maintaining clean aesthetic
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

- Lazy-load DOMParser to avoid instantiation in non-browser environments
- Prevents 'ReferenceError: DOMParser is not defined' in unit tests
- getParser() function creates instance on first use
- No performance impact: parser still cached after first call
- Use workingDoc.createElement/createTextNode instead of global document
- Prevents WrongDocumentError when replacing nodes across different Documents
- Fragment, mark elements, and text nodes now belong to the same Document
- Fixes search highlighting not appearing due to replaceChild throwing
- Changed if statement to ??= operator in getParser()
- Cleaner and more idiomatic TypeScript
- Satisfies @typescript-eslint/prefer-nullish-coalescing
@ammario ammario merged commit d2a4808 into main Oct 20, 2025
8 checks passed
@ammario ammario deleted the cr-search branch October 20, 2025 01:09
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.

2 participants