-
Notifications
You must be signed in to change notification settings - Fork 22
Open
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
// 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
// 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<FindInPageResult | null>(null);
const [caseSensitive, setCaseSensitive] = useState(false);
const [wholeWord, setWholeWord] = useState(false);
const inputRef = useRef<HTMLInputElement>(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 (
<div className="find-in-page-bar">
<div className="search-container">
<input
ref={inputRef}
type="text"
value={searchTerm}
onChange={(e) => 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"
/>
<div className="search-results">
{result && result.matches > 0 ? (
<span className="result-count">
{result.activeMatchOrdinal} of {result.matches}
</span>
) : searchTerm ? (
<span className="no-results">No results</span>
) : null}
</div>
<div className="search-controls">
<button
onClick={findPrevious}
disabled={!searchTerm || !result?.matches}
className="nav-button"
title="Previous (Shift+Enter)"
>
↑
</button>
<button
onClick={findNext}
disabled={!searchTerm || !result?.matches}
className="nav-button"
title="Next (Enter)"
>
↓
</button>
<div className="search-options">
<button
onClick={() => setCaseSensitive(!caseSensitive)}
className={`option-button ${caseSensitive ? 'active' : ''}`}
title="Match Case"
>
Aa
</button>
<button
onClick={() => setWholeWord(!wholeWord)}
className={`option-button ${wholeWord ? 'active' : ''}`}
title="Whole Word"
>
W
</button>
</div>
<button
onClick={close}
className="close-button"
title="Close (Esc)"
>
✕
</button>
</div>
</div>
</div>
);
}3. Styling
Search Bar Styles
.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
// 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
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
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
🤖 Generated with Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels