diff --git a/src/main/window/index.js b/src/main/window/index.js index a4e55d8..24b45c2 100644 --- a/src/main/window/index.js +++ b/src/main/window/index.js @@ -16,8 +16,15 @@ function createMainWindow() { y: bounds.y, show: true, webPreferences: { - nodeIntegration: true, - contextIsolation: false, + // The preload script exposes `window.electronAPI` — the only IPC + // bridge the renderer uses. Without this, every renderer call returns + // CHANNEL_MISSING and file/folder/save all silently no-op. + preload: path.join(__dirname, '../../preload.js'), + // contextIsolation MUST be true for the preload's contextBridge + // exposeInMainWorld call to succeed. Without it, the preload throws + // on load and the renderer never gets the IPC bridge. + contextIsolation: true, + nodeIntegration: false, spellcheck: true }, icon: path.join(__dirname, '../../../assets/icon.png') diff --git a/src/renderer/lib/commands/register-menu-commands.ts b/src/renderer/lib/commands/register-menu-commands.ts index 6d9c4b6..ba607d4 100644 --- a/src/renderer/lib/commands/register-menu-commands.ts +++ b/src/renderer/lib/commands/register-menu-commands.ts @@ -102,6 +102,10 @@ export function registerMenuCommands(): void { }); register('view.toggleSidebar', () => useAppStore.getState().toggleSidebar()); register('view.togglePreview', () => useAppStore.getState().togglePreview()); + register('file.opened', (payload?: { path?: string; content?: string }) => { + if (!payload?.path) return; + void useFileStore.getState().openFileFromMain(payload.path, payload.content ?? ''); + }); } export function useRegisterMenuCommands(): void { @@ -118,7 +122,6 @@ export function useRegisterMenuCommands(): void { export function useBridgeNativeMenu(): void { useMenuAction('file-save', 'file.save'); useMenuAction('toggle-preview', 'view.togglePreview'); - useMenuAction('toggle-command-palette', 'command.palette'); useMenuAction('toggle-sidebar-panel', 'view.sidebarPanel', (panel) => panel as string); useMenuAction('toggle-bottom-panel', 'view.bottomPanel'); useMenuAction('toggle-find', 'find.toggle'); diff --git a/src/renderer/stores/file-store.ts b/src/renderer/stores/file-store.ts index 55463b7..b44c530 100644 --- a/src/renderer/stores/file-store.ts +++ b/src/renderer/stores/file-store.ts @@ -36,6 +36,7 @@ interface FileState { loadChildren: (dirPath: string) => Promise; toggleExpanded: (dirPath: string) => void; openFile: (filePath: string) => Promise; + openFileFromMain: (filePath: string, content: string) => Promise; closeTab: (id: string) => void; setActiveTab: (id: string) => void; markTabClean: (id: string) => void; @@ -165,6 +166,25 @@ export const useFileStore = create()( }); }, + // Used when the main process has already read the file (e.g. file-opened + // IPC from File → Open menu). Skips the renderer-side IPC read since main + // already has the content. Preserves the editor's current buffer content + // if the file is already open. + openFileFromMain: async (filePath, content) => { + const existing = useFileStore.getState().openTabs.find((t) => t.id === filePath); + if (existing) { + set((s) => { s.activeTabId = filePath; }); + return; + } + + const title = filePath.split('/').pop() ?? filePath; + useEditorStore.getState().openBuffer(filePath, filePath, content); + set((s) => { + s.openTabs.push({ id: filePath, path: filePath, title, dirty: false }); + s.activeTabId = filePath; + }); + }, + closeTab: (id) => { set((s) => { const idx = s.openTabs.findIndex((t) => t.id === id); diff --git a/tests/unit/stores/file-store.test.ts b/tests/unit/stores/file-store.test.ts index 184b498..6a5a991 100644 --- a/tests/unit/stores/file-store.test.ts +++ b/tests/unit/stores/file-store.test.ts @@ -232,6 +232,37 @@ describe('useFileStore', () => { expect(useEditorStore.getState().buffers.size).toBe(0); }); + // --- openFileFromMain --- + + it('openFileFromMain does NOT call ipc.file.read (main already sent content)', async () => { + await useFileStore.getState().openFileFromMain('/root/from-menu.md', '# from menu'); + + expect(fakeRead).not.toHaveBeenCalled(); + const buf = useEditorStore.getState().buffers.get('/root/from-menu.md'); + expect(buf).not.toBeUndefined(); + expect(buf!.content).toBe('# from menu'); + const tabs = useFileStore.getState().openTabs; + expect(tabs).toHaveLength(1); + expect(tabs[0].path).toBe('/root/from-menu.md'); + expect(tabs[0].title).toBe('from-menu.md'); + expect(useFileStore.getState().activeTabId).toBe('/root/from-menu.md'); + }); + + it('openFileFromMain on an already-open file does not overwrite the editor buffer', async () => { + // Open once via the menu path + await useFileStore.getState().openFileFromMain('/root/x.md', '# menu version'); + // User then types something in the editor (simulated via updateContent) + useEditorStore.getState().updateContent('/root/x.md', '# user edited'); + + // Menu sends the same file again (e.g. user picks it from Recent Files) + await useFileStore.getState().openFileFromMain('/root/x.md', '# menu version'); + + // Buffer keeps the user's edits — the menu re-open must not clobber them + expect(useEditorStore.getState().buffers.get('/root/x.md')!.content).toBe('# user edited'); + expect(useFileStore.getState().openTabs).toHaveLength(1); + expect(useFileStore.getState().activeTabId).toBe('/root/x.md'); + }); + // --- closeTab --- it('closeTab removes the tab and updates activeTabId to the previous neighbor', async () => {