From b12750a4a5e58eda9b7880fe94d3c9c0560e6631 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Mon, 25 May 2026 18:40:03 +0530 Subject: [PATCH 01/52] test(main-processor): add test case for folder wide search helpers --- .../tests/folder-search.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 apps/main-processor/tests/folder-search.test.ts diff --git a/apps/main-processor/tests/folder-search.test.ts b/apps/main-processor/tests/folder-search.test.ts new file mode 100644 index 0000000..46d3381 --- /dev/null +++ b/apps/main-processor/tests/folder-search.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { searchFolder, buildExcerpt, countMatches } from '../src/folder-search'; + +const DIR = join(tmpdir(), 'md-search-test'); + +beforeAll(() => { + mkdirSync(DIR, { recursive: true }); + writeFileSync(join(DIR, 'README.md'), '# Project\nThis project uses React.\nReact is great.'); + writeFileSync(join(DIR, 'CHANGELOG.md'), '# Changes\nVersion 1.0 released.'); + writeFileSync(join(DIR, 'notes.txt'), 'not a markdown file'); +}); +afterAll(() => { + rmSync(DIR, { recursive: true, force: true }); +}); + +describe('search folder', () => { + it('should find matches in .md files and returns results', async () => { + const results = await searchFolder(DIR, 'React'); + expect(results.length).toBeGreaterThan(0); + expect(results.some((r) => r.file.endsWith('README.md'))).toBe(true); + }); + + it('does not search non .md files', async () => { + const results = await searchFolder(DIR, 'not a markdown'); + expect(results.every((r) => r.file.endsWith('.md'))).toBe(true); + }); + + it('should return empty array when query matches nothing', async () => { + const results = await searchFolder(DIR, 'xyznotfound'); + expect(results).toHaveLength(0); + }); + + it('has search case insensitive', async () => { + const lower = await searchFolder(DIR, 'react'); + const upper = await searchFolder(DIR, 'REACT'); + expect(lower.length).toBe(upper.length); + }); +}); + +describe('build excerpt', () => { + it('returns the matched line with surrounding context', () => { + const lines = ['line1', 'line2 has React', 'line3']; + const exc = buildExcerpt(lines, 1); + expect(exc).toContain('React'); + }); +}); + +describe('count matches', () => { + it('should count occurrences of query in a line', () => { + expect(countMatches('React and react', 'react')).toBe(2); + }); +}); From 38c118b813410ab2cda7c826c354a729d72b1e03 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 26 May 2026 15:09:40 +0530 Subject: [PATCH 02/52] fix(main-processor): validate Os file path --- apps/main-processor/src/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/main-processor/src/index.ts b/apps/main-processor/src/index.ts index e330de4..422f9c3 100644 --- a/apps/main-processor/src/index.ts +++ b/apps/main-processor/src/index.ts @@ -6,16 +6,18 @@ import { PATHS } from './utils/constants/path-constants'; import { registerMenu } from './register-menu'; import { parseFilePathFromArgv } from './cli'; import { setupAutoUpdater } from './updater'; +import { resolveMarkdownFilePath } from './utils/helper/ipc-path-resolver'; let mainWindow: BrowserWindow | null = null; let pendingFilePath: string | null = null; -function sendFilePathToRenderer(filePath: string): void { +async function sendFilePathToRenderer(filePath: string): Promise { + const safeFilePath = await resolveMarkdownFilePath(filePath); if (!mainWindow) { - pendingFilePath = filePath; + pendingFilePath = safeFilePath; return; } - mainWindow.webContents.send('open-file-path', filePath); + mainWindow.webContents.send('open-file-path', safeFilePath); } // register all IPC before window is created @@ -71,7 +73,9 @@ function createWindow(): void { app.on('open-file', (event, filePath) => { event.preventDefault(); - sendFilePathToRenderer(filePath); + void sendFilePathToRenderer(filePath).catch((error) => { + console.error('Failed to open file from Os:-', error); + }); }); const hasSingleInstanceLock = app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) { From bc7cd6fc6d2fb0eded43459c75cc78f744b1f526 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 26 May 2026 15:28:39 +0530 Subject: [PATCH 03/52] feat(watcher): add unlink and error handler in file watcher --- apps/main-processor/src/file.ts | 19 ++++++++++++++++--- .../src/types/watch-file-types.ts | 5 +++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 apps/main-processor/src/types/watch-file-types.ts diff --git a/apps/main-processor/src/file.ts b/apps/main-processor/src/file.ts index 1778dbb..560fb02 100644 --- a/apps/main-processor/src/file.ts +++ b/apps/main-processor/src/file.ts @@ -1,5 +1,6 @@ import { readFile as fsReadFile } from 'node:fs/promises'; import chokidar, { type FSWatcher } from 'chokidar'; +import { WatchFileOptions } from './types/watch-file-types'; //file read logic export async function readFile(filePath: string): Promise { @@ -13,10 +14,15 @@ export async function readFile(filePath: string): Promise { const currentWatchers = new Map(); //file watching logic -export async function watchFile(filePath: string, onChange: () => void): Promise { +export async function watchFile( + filePath: string, + options: WatchFileOptions | (() => void) +): Promise { + const { onChange, onDeleted, onError } = + typeof options === 'function' ? { onChange: options } : options; + if (currentWatchers.has(filePath)) { - await currentWatchers.get(filePath)!.close(); - currentWatchers.delete(filePath); + await unWatchFile(filePath); } const watcher = chokidar.watch(filePath, { @@ -31,6 +37,13 @@ export async function watchFile(filePath: string, onChange: () => void): Promise watcher.on('ready', resolve); }); watcher.on('change', onChange); + watcher.on('unlink', () => { + void unWatchFile(filePath).then(() => onDeleted?.()); + }); + watcher.on('error', (error) => { + const watcherError = error instanceof Error ? error : new Error(String(error)); + void unWatchFile(filePath).then(() => onError?.(watcherError)); + }); currentWatchers.set(filePath, watcher); } diff --git a/apps/main-processor/src/types/watch-file-types.ts b/apps/main-processor/src/types/watch-file-types.ts new file mode 100644 index 0000000..cc19e8a --- /dev/null +++ b/apps/main-processor/src/types/watch-file-types.ts @@ -0,0 +1,5 @@ +export type WatchFileOptions = { + onChange: () => void; + onDeleted?: () => void; + onError?: (error: Error) => void; +}; From fe62280ede58f8941009d296c08ccc76b2a1ecf2 Mon Sep 17 00:00:00 2001 From: Ashminita Date: Tue, 26 May 2026 18:14:27 +0530 Subject: [PATCH 04/52] feat(main-processor): add debounce timer and its test case --- apps/main-processor/src/file.ts | 30 +++++++++++++++++++++++++- apps/main-processor/tests/file.test.ts | 11 ++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/main-processor/src/file.ts b/apps/main-processor/src/file.ts index 560fb02..6f2670d 100644 --- a/apps/main-processor/src/file.ts +++ b/apps/main-processor/src/file.ts @@ -13,6 +13,7 @@ export async function readFile(filePath: string): Promise { } const currentWatchers = new Map(); +const debounceTimers = new Map(); //file watching logic export async function watchFile( filePath: string, @@ -36,7 +37,22 @@ export async function watchFile( await new Promise((resolve) => { watcher.on('ready', resolve); }); - watcher.on('change', onChange); + watcher.on('change', () => { + const existingTimer = debounceTimers.get(filePath); + if (existingTimer) { + clearTimeout(existingTimer); + } + + const timer = setTimeout(() => { + if (!currentWatchers.has(filePath)) { + debounceTimers.delete(filePath); + return; + } + debounceTimers.delete(filePath); + onChange(); + }, 100); + debounceTimers.set(filePath, timer); + }); watcher.on('unlink', () => { void unWatchFile(filePath).then(() => onDeleted?.()); }); @@ -49,7 +65,19 @@ export async function watchFile( //unwatch file export async function unWatchFile(filePath: string): Promise { + const timer = debounceTimers.get(filePath); + if (timer) { + clearTimeout(timer); + debounceTimers.delete(filePath); + } if (!currentWatchers.has(filePath)) return; await currentWatchers.get(filePath)!.close(); currentWatchers.delete(filePath); } + +export function getWatcherDiagnostics(): { watchers: number; debounceTimers: number } { + return { + watchers: currentWatchers.size, + debounceTimers: debounceTimers.size, + }; +} diff --git a/apps/main-processor/tests/file.test.ts b/apps/main-processor/tests/file.test.ts index f4a8498..454ea65 100644 --- a/apps/main-processor/tests/file.test.ts +++ b/apps/main-processor/tests/file.test.ts @@ -2,6 +2,7 @@ import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest'; import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; +import { getWatcherDiagnostics } from '../src/file'; import { readFile, watchFile, unWatchFile } from '../src/file'; @@ -47,6 +48,7 @@ describe('File watcher', () => { writeFileSync(TEST_FILE, 'change after unwatch'); await new Promise((r) => setTimeout(r, 200)); expect(cb).not.toHaveBeenCalled(); + expect(getWatcherDiagnostics().debounceTimers).toBe(0); }); it('should protect from double call', async () => { @@ -61,4 +63,13 @@ describe('File watcher', () => { it('should not crash if I unwatch a file which is not watched', async () => { await expect(unWatchFile('fake-file.txt')).resolves.toBeUndefined(); }); + + it('should clean debounce timer references after unwatching', async () => { + const cb = vi.fn(); + await watchFile(TEST_FILE, cb); + writeFileSync(TEST_FILE, 'change with pending debounce'); + await new Promise((r) => setTimeout(r, 25)); + await unWatchFile(TEST_FILE); + expect(getWatcherDiagnostics().debounceTimers).toBe(0); + }); }); From 5fd185dbb5fceff4e60e9604a2657e0edfe7176a Mon Sep 17 00:00:00 2001 From: Ashminita Date: Wed, 27 May 2026 10:56:36 +0530 Subject: [PATCH 05/52] fix(toc): fix alignment of sidebar contents & deep collapsing --- apps/renderer/src/components/Sidebar.tsx | 49 +++++++++++-------- apps/renderer/src/hooks/useCollapsibleToc.ts | 12 +++-- .../src/utils/helpers/sidebar-helper.ts | 13 +++-- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/apps/renderer/src/components/Sidebar.tsx b/apps/renderer/src/components/Sidebar.tsx index 3720acf..d15de10 100644 --- a/apps/renderer/src/components/Sidebar.tsx +++ b/apps/renderer/src/components/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useEffect,useRef } from 'react'; import { SidebarProps } from '../types/component-types'; import { Icons } from '../utils/constants/icon-contants'; import { getItemClasses } from '../utils/helpers/sidebar-helper'; @@ -6,12 +7,16 @@ import { useCollapsibleToc } from '../hooks/useCollapsibleToc'; //sidebar component export function Sidebar({tocItems,activeId,onSelect,isVisible=true, onClose }: SidebarProps & { onClose: () => void }) { const { visibleItems, toggleItem, hasChildren, isCollapsed } = useCollapsibleToc(tocItems); + const activeItemRef = useRef(null); + useEffect(() => { + activeItemRef.current?.scrollIntoView({ block: 'nearest' }); + }, [activeId]); if(!isVisible||tocItems.length===0){ return null; } return ( -