areibman / areibman/bottleneck

Feature: Enable find-in-page functionality (Cmd/Ctrl+F) for searching within the current view

Open
#31 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.