Skip to content

Commit dab3995

Browse files
ozgesolidkeyclaude
andcommitted
Add search configs feature with persistent multi-pattern search and highlighting
- New bottom slide-up panel (Ctrl+8) for defining multiple search patterns with individual colors - Each config has pattern, regex/case/wholeWord toggles, color picker, and global/local scope - Configs persist to ~/.logan/search-configs.json, batch search runs all enabled patterns in one IPC call - Results merged and sorted by line number, click to navigate, right-click chips for edit/export/delete - Patterns highlighted inline in the viewer alongside existing search and highlight systems - Colored minimap markers for each enabled config - Mutual exclusion with notes drawer and search results panel (shared bottom slot) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3f06a71 commit dab3995

7 files changed

Lines changed: 1243 additions & 7 deletions

File tree

src/main/index.ts

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as os from 'os';
55
import { spawn } from 'child_process';
66
import * as pty from 'node-pty';
77
import { FileHandler, filterLineToVisibleColumns, ColumnConfig } from './fileHandler';
8-
import { IPC, SearchOptions, Bookmark, Highlight, HighlightGroup, ActivityEntry, LocalFileData } from '../shared/types';
8+
import { IPC, SearchOptions, Bookmark, Highlight, HighlightGroup, SearchConfig, ActivityEntry, LocalFileData } from '../shared/types';
99
import * as Diff from 'diff';
1010
import { analyzerRegistry, AnalyzerOptions } from './analyzers';
1111
import { loadDatadogConfig, saveDatadogConfig, clearDatadogConfig, fetchDatadogLogs, DatadogConfig, DatadogFetchParams } from './datadogClient';
@@ -1570,6 +1570,181 @@ ipcMain.handle('highlight-group-delete', async (_, groupId: string) => {
15701570
return { success: true };
15711571
});
15721572

1573+
// === Search Configs ===
1574+
1575+
const getSearchConfigsPath = () => path.join(getConfigDir(), 'search-configs.json');
1576+
const GLOBAL_SEARCH_CONFIGS_KEY = '_global';
1577+
1578+
interface SearchConfigsStore {
1579+
[key: string]: SearchConfig[];
1580+
}
1581+
1582+
function loadSearchConfigsStore(): SearchConfigsStore {
1583+
try {
1584+
ensureConfigDir();
1585+
const configPath = getSearchConfigsPath();
1586+
if (fs.existsSync(configPath)) {
1587+
const data = fs.readFileSync(configPath, 'utf-8');
1588+
return JSON.parse(data);
1589+
}
1590+
} catch (error) {
1591+
console.error('Failed to load search configs:', error);
1592+
}
1593+
return {};
1594+
}
1595+
1596+
function saveSearchConfigsStore(store: SearchConfigsStore): void {
1597+
try {
1598+
ensureConfigDir();
1599+
const configPath = getSearchConfigsPath();
1600+
const cleanStore: SearchConfigsStore = {};
1601+
for (const [key, value] of Object.entries(store)) {
1602+
if (value.length > 0) {
1603+
cleanStore[key] = value;
1604+
}
1605+
}
1606+
fs.writeFileSync(configPath, JSON.stringify(cleanStore, null, 2), 'utf-8');
1607+
} catch (error) {
1608+
console.error('Failed to save search configs:', error);
1609+
}
1610+
}
1611+
1612+
function loadSearchConfigsForFile(filePath: string): SearchConfig[] {
1613+
const store = loadSearchConfigsStore();
1614+
const configs: SearchConfig[] = [];
1615+
1616+
// Load global configs
1617+
const globalConfigs = store[GLOBAL_SEARCH_CONFIGS_KEY] || [];
1618+
for (const c of globalConfigs) {
1619+
configs.push({ ...c, isGlobal: true });
1620+
}
1621+
1622+
// Load file-specific configs
1623+
if (filePath) {
1624+
const fileConfigs = store[filePath] || [];
1625+
for (const c of fileConfigs) {
1626+
configs.push({ ...c, isGlobal: false });
1627+
}
1628+
}
1629+
1630+
return configs;
1631+
}
1632+
1633+
function saveSearchConfig(config: SearchConfig): void {
1634+
const store = loadSearchConfigsStore();
1635+
1636+
// Remove from all keys first
1637+
for (const k of Object.keys(store)) {
1638+
store[k] = store[k].filter(c => c.id !== config.id);
1639+
}
1640+
1641+
const key = config.isGlobal ? GLOBAL_SEARCH_CONFIGS_KEY : (currentFilePath || GLOBAL_SEARCH_CONFIGS_KEY);
1642+
if (!store[key]) store[key] = [];
1643+
store[key].push(config);
1644+
saveSearchConfigsStore(store);
1645+
}
1646+
1647+
function removeSearchConfigFromStore(id: string): void {
1648+
const store = loadSearchConfigsStore();
1649+
let changed = false;
1650+
for (const k of Object.keys(store)) {
1651+
const before = store[k].length;
1652+
store[k] = store[k].filter(c => c.id !== id);
1653+
if (store[k].length !== before) changed = true;
1654+
}
1655+
if (changed) saveSearchConfigsStore(store);
1656+
}
1657+
1658+
ipcMain.handle(IPC.SEARCH_CONFIG_SAVE, async (_, config: SearchConfig) => {
1659+
saveSearchConfig(config);
1660+
return { success: true };
1661+
});
1662+
1663+
ipcMain.handle(IPC.SEARCH_CONFIG_LOAD, async () => {
1664+
const configs = loadSearchConfigsForFile(currentFilePath || '');
1665+
return { success: true, configs };
1666+
});
1667+
1668+
ipcMain.handle(IPC.SEARCH_CONFIG_DELETE, async (_, id: string) => {
1669+
removeSearchConfigFromStore(id);
1670+
return { success: true };
1671+
});
1672+
1673+
ipcMain.handle(IPC.SEARCH_CONFIG_BATCH, async (_, configs: Array<{ id: string; pattern: string; isRegex: boolean; matchCase: boolean; wholeWord: boolean }>) => {
1674+
const handler = getFileHandler();
1675+
if (!handler) return { success: false, error: 'No file open' };
1676+
1677+
const results: Record<string, Array<{ lineNumber: number; column: number; length: number; lineText: string }>> = {};
1678+
const totalConfigs = configs.length;
1679+
1680+
for (let i = 0; i < configs.length; i++) {
1681+
const cfg = configs[i];
1682+
try {
1683+
const searchOpts: SearchOptions = {
1684+
pattern: cfg.pattern,
1685+
isRegex: cfg.isRegex,
1686+
isWildcard: false,
1687+
matchCase: cfg.matchCase,
1688+
wholeWord: cfg.wholeWord,
1689+
};
1690+
1691+
// Add filtered lines if filter is active
1692+
const filteredIndices = getFilteredLines();
1693+
if (filteredIndices) {
1694+
searchOpts.filteredLineIndices = filteredIndices;
1695+
}
1696+
1697+
const matches = await handler.search(searchOpts, (percent) => {
1698+
const overallPercent = Math.round(((i + percent / 100) / totalConfigs) * 100);
1699+
mainWindow?.webContents.send(IPC.SEARCH_CONFIG_BATCH_PROGRESS, { percent: overallPercent, configId: cfg.id });
1700+
}, { cancelled: false });
1701+
1702+
// If filter is active, remap line numbers
1703+
if (filteredIndices && filteredIndices.length > 0) {
1704+
const filteredSet = new Set(filteredIndices);
1705+
const lineToFilteredIndex = new Map<number, number>();
1706+
filteredIndices.forEach((lineNum, idx) => lineToFilteredIndex.set(lineNum, idx));
1707+
1708+
results[cfg.id] = matches
1709+
.filter(m => filteredSet.has(m.lineNumber))
1710+
.map(m => ({
1711+
lineNumber: lineToFilteredIndex.get(m.lineNumber) ?? m.lineNumber,
1712+
column: m.column,
1713+
length: m.length,
1714+
lineText: m.lineText,
1715+
}));
1716+
} else {
1717+
results[cfg.id] = matches.map(m => ({
1718+
lineNumber: m.lineNumber,
1719+
column: m.column,
1720+
length: m.length,
1721+
lineText: m.lineText,
1722+
}));
1723+
}
1724+
} catch (error) {
1725+
console.error(`Search config batch error for ${cfg.id}:`, error);
1726+
results[cfg.id] = [];
1727+
}
1728+
}
1729+
1730+
return { success: true, results };
1731+
});
1732+
1733+
ipcMain.handle(IPC.SEARCH_CONFIG_EXPORT, async (_, configId: string, lines: string[]) => {
1734+
if (!currentFilePath) return { success: false, error: 'No file open' };
1735+
1736+
try {
1737+
const baseName = path.basename(currentFilePath, path.extname(currentFilePath));
1738+
const date = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
1739+
const exportName = `${baseName}_search_${configId.substring(0, 8)}_${date}.txt`;
1740+
const exportPath = path.join(path.dirname(currentFilePath), exportName);
1741+
fs.writeFileSync(exportPath, lines.join('\n'), 'utf-8');
1742+
return { success: true, filePath: exportPath };
1743+
} catch (error) {
1744+
return { success: false, error: String(error) };
1745+
}
1746+
});
1747+
15731748
// === Utility ===
15741749

15751750
ipcMain.handle('get-file-info', async () => {

src/preload/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const IPC = {
2525
LOAD_ACTIVITY_HISTORY: 'load-activity-history',
2626
CLEAR_ACTIVITY_HISTORY: 'clear-activity-history',
2727
GET_LOCAL_FILE_STATUS: 'get-local-file-status',
28+
SEARCH_CONFIG_SAVE: 'search-config-save',
29+
SEARCH_CONFIG_LOAD: 'search-config-load',
30+
SEARCH_CONFIG_DELETE: 'search-config-delete',
31+
SEARCH_CONFIG_BATCH: 'search-config-batch',
32+
SEARCH_CONFIG_BATCH_PROGRESS: 'search-config-batch-progress',
33+
SEARCH_CONFIG_EXPORT: 'search-config-export',
2834
} as const;
2935

3036
// API exposed to renderer
@@ -318,6 +324,28 @@ const api = {
318324
readFileContent: (filePath: string): Promise<{ success: boolean; content?: string; sizeMB?: number; error?: string }> =>
319325
ipcRenderer.invoke('read-file-content', filePath),
320326

327+
// Search configs
328+
searchConfigSave: (config: any): Promise<{ success: boolean }> =>
329+
ipcRenderer.invoke(IPC.SEARCH_CONFIG_SAVE, config),
330+
331+
searchConfigLoad: (): Promise<{ success: boolean; configs?: any[] }> =>
332+
ipcRenderer.invoke(IPC.SEARCH_CONFIG_LOAD),
333+
334+
searchConfigDelete: (id: string): Promise<{ success: boolean }> =>
335+
ipcRenderer.invoke(IPC.SEARCH_CONFIG_DELETE, id),
336+
337+
searchConfigBatch: (configs: any[]): Promise<{ success: boolean; results?: Record<string, any[]>; error?: string }> =>
338+
ipcRenderer.invoke(IPC.SEARCH_CONFIG_BATCH, configs),
339+
340+
onSearchConfigBatchProgress: (callback: (data: { percent: number; configId: string }) => void): (() => void) => {
341+
const handler = (_: any, data: { percent: number; configId: string }) => callback(data);
342+
ipcRenderer.on(IPC.SEARCH_CONFIG_BATCH_PROGRESS, handler);
343+
return () => ipcRenderer.removeListener(IPC.SEARCH_CONFIG_BATCH_PROGRESS, handler);
344+
},
345+
346+
searchConfigExport: (configId: string, lines: string[]): Promise<{ success: boolean; filePath?: string; error?: string }> =>
347+
ipcRenderer.invoke(IPC.SEARCH_CONFIG_EXPORT, configId, lines),
348+
321349
// Window controls
322350
windowMinimize: (): Promise<void> => ipcRenderer.invoke('window-minimize'),
323351
windowMaximize: (): Promise<void> => ipcRenderer.invoke('window-maximize'),

src/renderer/index.html

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@
122122
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 8 14"></polyline><path d="M2.05 12A10 10 0 0 1 4.63 5.36"></path></svg>
123123
<span class="activity-badge" id="badge-history"></span>
124124
</button>
125+
<button class="activity-bar-btn" id="btn-search-configs" title="Search Configs (Ctrl+8)">
126+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line><line x1="11" y1="8" x2="11" y2="14"></line></svg>
127+
<span class="activity-badge" id="badge-search-configs"></span>
128+
</button>
125129
</div>
126130
<div class="activity-bar-bottom">
127131
<button class="activity-bar-btn" id="btn-activity-settings" title="Settings">
@@ -360,6 +364,34 @@ <h2>LOGAN</h2>
360364
</div>
361365
</div>
362366

367+
<!-- Search Configs Panel (slide-up from bottom) -->
368+
<div id="search-configs-overlay" class="search-configs-overlay hidden">
369+
<div id="search-configs-panel" class="search-configs-panel">
370+
<div id="search-configs-resize-handle" class="search-configs-resize-handle-top"></div>
371+
<div class="search-configs-header">
372+
<span class="search-configs-title">Search Configs</span>
373+
<span id="search-configs-summary" class="search-configs-summary"></span>
374+
<button id="btn-search-configs-close" class="search-configs-close-btn" title="Close (Esc)">&times;</button>
375+
</div>
376+
<div id="search-configs-chips" class="search-configs-chips">
377+
<button id="btn-add-search-config" class="search-config-add-btn" title="Add search config">+ Add</button>
378+
</div>
379+
<div id="search-configs-form" class="search-configs-form hidden">
380+
<input type="text" id="sc-pattern-input" class="sc-pattern-input" placeholder="Search pattern...">
381+
<div class="sc-form-toggles">
382+
<label class="sc-toggle-label"><input type="checkbox" id="sc-regex"> Regex</label>
383+
<label class="sc-toggle-label"><input type="checkbox" id="sc-match-case"> Case</label>
384+
<label class="sc-toggle-label"><input type="checkbox" id="sc-whole-word"> Word</label>
385+
<label class="sc-toggle-label" title="Global configs apply to all files"><input type="checkbox" id="sc-global"> Global</label>
386+
</div>
387+
<input type="color" id="sc-color-input" class="sc-color-input" value="#ffff00">
388+
<button id="btn-sc-save" class="sc-form-btn primary">Save</button>
389+
<button id="btn-sc-cancel" class="sc-form-btn">Cancel</button>
390+
</div>
391+
<div id="search-configs-results" class="search-configs-results"></div>
392+
</div>
393+
</div>
394+
363395
<!-- Status Bar -->
364396
<footer class="status-bar">
365397
<div class="status-left">
@@ -604,7 +636,8 @@ <h4>Terminal & Notes</h4>
604636
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>`</kbd><span>Toggle terminal panel</span></div>
605637
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd><span>Toggle notes drawer</span></div>
606638
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd><span>Toggle search results panel</span></div>
607-
<div class="shortcut-item"><kbd>Esc</kbd><span>Close terminal/notes/search results (when focused)</span></div>
639+
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>8</kbd><span>Toggle search configs panel</span></div>
640+
<div class="shortcut-item"><kbd>Esc</kbd><span>Close terminal/notes/search results/search configs (when focused)</span></div>
608641
</div>
609642
</div>
610643
<div class="help-section">

0 commit comments

Comments
 (0)