areibman / areibman/bottleneck
Feature: Enable find-in-page functionality (Cmd/Ctrl+F) for searching within the current view
- Dominant language
- TypeScript
- Stars
- 156
- Forks
- 21
- PR merge metrics
- No merged PRs in 30d
Description
## Feature Request
Enable the standard find-in-page functionality (Cmd/Ctrl+F) in the Electron application to allow users to search within the current page/view, as this is not enabled by default in Electron.
## Description
Electron applications don't have find-in-page functionality enabled by default. Users expect to be able to use Cmd+F (macOS) or Ctrl+F (Windows/Linux) to search within the current page, especially when viewing PRs, code, or long comment threads.
## Implementation Details
### 1. Main Process Implementation
#### Enable Find-in-Page in Main Process
```typescript
// main/main.ts
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
class FindInPage {
private currentWindow: BrowserWindow | null = null;
private searchVisible: boolean = false;
initialize(window: BrowserWindow) {
this.currentWindow = window;
this.registerShortcuts();
this.setupIPC();
}
private registerShortcuts() {
// Register global shortcuts for the window
this.currentWindow?.webContents.on('before-input-event', (event, input) => {
// Cmd/Ctrl + F to open search
if ((input.control || input.meta) && input.key === 'f' && input.type === 'keyDown') {
event.preventDefault();
this.toggleSearch();
}
// Escape to close search
if (input.key === 'Escape' && input.type === 'keyDown' && this.searchVisible) {
event.preventDefault();
this.closeSearch();
}
// Cmd/Ctrl + G for find next
if ((input.control || input.meta) && input.key === 'g' && input.type === 'keyDown') {
event.preventDefault();
if (input.shift) {
this.findPrevious();
} else {
this.findNext();
}
}
// F3 for find next (Windows/Linux)
if (input.key === 'F3' && input.type === 'keyDown') {
event.preventDefault();
if (input.shift) {
this.findPrevious();
} else {
this.findNext();
}
}
});
}
private toggleSearch() {
if (this.searchVisible) {
this.closeSearch();
} else {
this.openSearch();
}
}
private openSearch() {
this.searchVisible = true;
this.currentWindow?.webContents.send('toggle-search', true);
}
private closeSearch() {
this.searchVisible = false;
this.currentWindow?.webContents.stopFindInPage('clearSelection');
this.currentWindow?.webContents.send('toggle-search', false);
}
private findNext() {
this.currentWindow?.webContents.findInPage(this.lastSearchTerm, {
forward: true,
findNext: true
});
}
private findPrevious() {
this.currentWindow?.webContents.findInPage(this.lastSearchTerm, {
forward: false,
findNext: true
});
}
private lastSearchTerm: string = '';
private setupIPC() {
// Handle search requests from renderer
ipcMain.on('find-in-page', (event, searchTerm: string, options?: Electron.FindInPageOptions) => {
if (!this.currentWindow) return;
this.lastSearchTerm = searchTerm;
if (searchTerm === '') {
this.currentWindow.webContents.stopFindInPage('clearSelection');
} else {
this.currentWindow.webContents.findInPage(searchTerm, options || {});
}
});
// Handle search result updates
this.currentWindow?.webContents.on('found-in-page', (event, result) => {
this.currentWindow?.webContents.send('found-in-page-result', result);
});
// Handle close search
ipcMain.on('close-find-in-page', () => {
this.closeSearch();
});
}
}
// Initialize when creating window
function createWindow() {
const mainWindow = new BrowserWindow({
// ... window options
});
const findInPage = new FindInPage();
findInPage.initialize(mainWindow);
}
```
### 2. Renderer Process Search UI
#### Search Bar Component
```tsx
// renderer/components/FindInPage.tsx
import React, { useState, useEffect, useRef } from 'react';
import { ipcRenderer } from 'electron';
interface FindInPageResult {
requestId: number;
activeMatchOrdinal: number;
matches: number;
finalUpdate: boolean;
}
export function FindInPageBar() {
const [visible, setVisible] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [result, setResult] = useState(null);
const [caseSensitive, setCaseSensitive] = useState(false);
const [wholeWord, setWholeWord] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
// Listen for toggle events from main process
const handleToggle = (event: any, show: boolean) => {
setVisible(show);
if (show) {
setTimeout(() => inputRef.current?.focus(), 0);
// Select all text when opening
setTimeout(() => inputRef.current?.select(), 0);
}
};
// Listen for search results
const handleResult = (event: any, result: FindInPageResult) => {
setResult(result);
};
ipcRenderer.on('toggle-search', handleToggle);
ipcRenderer.on('found-in-page-result', handleResult);
return () => {
ipcRenderer.removeListener('toggle-search', handleToggle);
ipcRenderer.removeListener('found-in-page-result', handleResult);
};
}, []);
const handleSearch = (term: string) => {
setSearchTerm(term);
if (term) {
ipcRenderer.send('find-in-page', term, {
forward: true,
findNext: false,
matchCase: caseSensitive,
wordStart: wholeWord
});
} else {
ipcRenderer.send('find-in-page', '');
setResult(null);
}
};
const findNext = () => {
if (searchTerm) {
ipcRenderer.send('find-in-page', searchTerm, {
forward: true,
findNext: true,
matchCase: caseSensitive,
wordStart: wholeWord
});
}
};
const findPrevious = () => {
if (searchTerm) {
ipcRenderer.send('find-in-page', searchTerm, {
forward: false,
findNext: true,
matchCase: caseSensitive,
wordStart: wholeWord
});
}
};
const close = () => {
ipcRenderer.send('close-find-in-page');
setSearchTerm('');
setResult(null);
};
if (!visible) return null;
return (
handleSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) {
findPrevious();
} else {
findNext();
}
} else if (e.key === 'Escape') {
close();
}
}}
placeholder="Find in page..."
className="search-input"
/>
{result && result.matches > 0 ? (
{result.activeMatchOrdinal} of {result.matches}
) : searchTerm ? (
No results
) : null}
↑
↓
setCaseSensitive(!caseSensitive)}
className={`option-button ${caseSensitive ? 'active' : ''}`}
title="Match Case"
>
Aa
setWholeWord(!wholeWord)}
className={`option-button ${wholeWord ? 'active' : ''}`}
title="Whole Word"
>
W
✕
);
}
```
### 3. Styling
#### Search Bar Styles
```css
.find-in-page-bar {
position: fixed;
top: 0;
right: 20px;
z-index: 9999;
background: var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 8px;
display: flex;
align-items: center;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.search-container {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
width: 200px;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 3px;
font-size: 13px;
outline: none;
}
.search-input:focus {
border-color: var(--focus-color);
box-shadow: 0 0 0 2px var(--focus-shadow);
}
.search-results {
color: var(--text-secondary);
font-size: 12px;
min-width: 60px;
}
.no-results {
color: var(--text-danger);
}
.search-controls {
display: flex;
align-items: center;
gap: 4px;
}
.nav-button,
.option-button,
.close-button {
padding: 4px 8px;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.nav-button:hover:not(:disabled),
.option-button:hover,
.close-button:hover {
background: var(--hover-bg);
}
.nav-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.option-button.active {
background: var(--primary-color);
color: white;
}
.search-options {
display: flex;
gap: 2px;
margin: 0 4px;
padding: 0 4px;
border-left: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
}
/* Highlight styles for found text */
::selection {
background-color: var(--selection-bg);
}
.find-highlight {
background-color: yellow !important;
color: black !important;
}
.find-highlight-current {
background-color: orange !important;
color: black !important;
}
```
### 4. Menu Bar Integration
#### Add to Application Menu
```typescript
// main/menu.ts
import { Menu, MenuItemConstructorOptions } from 'electron';
function createApplicationMenu(window: BrowserWindow): Menu {
const template: MenuItemConstructorOptions[] = [
// ... other menu items
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ type: 'separator' },
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
window.webContents.send('toggle-search', true);
}
},
{
label: 'Find Next',
accelerator: 'CmdOrCtrl+G',
click: () => {
window.webContents.send('find-next');
}
},
{
label: 'Find Previous',
accelerator: 'CmdOrCtrl+Shift+G',
click: () => {
window.webContents.send('find-previous');
}
}
]
}
// ... other menu items
];
return Menu.buildFromTemplate(template);
}
```
### 5. Advanced Features
#### Context-Aware Search
```typescript
class ContextAwareSearch {
// Search within specific areas
searchInComments() {
this.setSearchScope('.comment-body');
}
searchInCode() {
this.setSearchScope('.code-block, .diff-view');
}
searchInFiles() {
this.setSearchScope('.file-content');
}
// Regex support
enableRegexSearch(pattern: string) {
try {
const regex = new RegExp(pattern, 'gi');
this.performRegexSearch(regex);
} catch (e) {
console.error('Invalid regex:', e);
}
}
// Search history
private searchHistory: string[] = [];
addToHistory(term: string) {
if (!this.searchHistory.includes(term)) {
this.searchHistory.unshift(term);
this.searchHistory = this.searchHistory.slice(0, 20);
}
}
}
```
### 6. Keyboard Shortcuts Summary
```typescript
const shortcuts = {
'CmdOrCtrl+F': 'Open find bar',
'Escape': 'Close find bar',
'Enter': 'Find next',
'Shift+Enter': 'Find previous',
'CmdOrCtrl+G': 'Find next',
'CmdOrCtrl+Shift+G': 'Find previous',
'F3': 'Find next (Windows/Linux)',
'Shift+F3': 'Find previous (Windows/Linux)',
'Alt+C': 'Toggle case sensitive',
'Alt+W': 'Toggle whole word'
};
```
## Benefits
- **Standard UX**: Users expect Cmd/Ctrl+F to work
- **Improved Navigation**: Quickly find content in long pages
- **Better Accessibility**: Helps users locate specific information
- **Productivity**: Faster than manual scrolling
- **Familiar Interface**: Similar to browser find functionality
## Acceptance Criteria
- [ ] Cmd/Ctrl+F opens find bar
- [ ] Search highlights all matches on page
- [ ] Current match is highlighted differently
- [ ] Next/Previous navigation works
- [ ] Match counter shows X of Y results
- [ ] Escape closes find bar
- [ ] Case sensitive option works
- [ ] Whole word option works
- [ ] Search persists when navigating next/previous
- [ ] Found text is scrolled into view
- [ ] Search bar has smooth animation
- [ ] Keyboard shortcuts work as expected
- [ ] Menu bar items trigger search
- [ ] Search works across all views (PRs, issues, code)
- [ ] Performance is good with large documents
## Resources
- [Electron findInPage API](https://www.electronjs.org/docs/latest/api/web-contents#contentsfindinpagesearchstring-options)
- [Electron Menu API](https://www.electronjs.org/docs/latest/api/menu)
- [Electron Accelerator Docs](https://www.electronjs.org/docs/latest/api/accelerator)
🤖 Generated with [Claude Code](https://claude.ai/code)
Contributor guide
No contributing guide indexed for this repository
Research direction
The request proposes changes in main/main.ts, renderer/components/FindInPage.tsx, and main/menu.ts; first inspect whether these entry points exist and how the Electron main and renderer processes communicate. Verify the existing window and menu setup before scoping the work. Done means Cmd/Ctrl+F opens a search UI for the current view, navigation and Escape work, and results are displayed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- desktop, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100