From 857d55a7cd2e255f8aee7d78896e7c1a02f68787 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 18 Apr 2026 08:25:43 +0200 Subject: [PATCH] fix: stop cmd+s from renaming note titles --- src/hooks/useAppSave.test.ts | 24 ++++++++++++ src/hooks/useAppSave.ts | 38 ++----------------- .../smoke/breadcrumb-filename-rename.spec.ts | 2 +- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/hooks/useAppSave.test.ts b/src/hooks/useAppSave.test.ts index f2f8f348..28700ca4 100644 --- a/src/hooks/useAppSave.test.ts +++ b/src/hooks/useAppSave.test.ts @@ -488,6 +488,30 @@ describe('useAppSave', () => { expect(deps.handleRenameNote).not.toHaveBeenCalled() }) + it('does not rename an existing markdown note when Cmd+S saves a desynced H1/title state', async () => { + vi.mocked(isTauri).mockReturnValue(true) + + const notePath = '/vault/note-b.md' + const noteContent = '# Breadcrumb Sync Target\n\nBody' + const entry = makeEntry(notePath, 'Breadcrumb Sync Target', 'note-b.md') + + const { result } = renderSave({ + tabs: [{ entry, content: noteContent }], + activeTabPath: notePath, + unsavedPaths: new Set([notePath]), + }) + + await act(async () => { + await result.current.handleSave() + }) + + expect(vi.mocked(invoke)).toHaveBeenCalledWith('save_note_content', { + path: notePath, + content: noteContent, + }) + expect(deps.handleRenameNote).not.toHaveBeenCalled() + }) + it('remaps a buffered auto-save to the renamed path when untitled rename lands mid-idle window', async () => { const initialContent = '# Fresh Title\n\nInitial body' const bufferedContent = '# Fresh Title\n\nBody typed right before rename' diff --git a/src/hooks/useAppSave.ts b/src/hooks/useAppSave.ts index c3db7c47..cb59cbf3 100644 --- a/src/hooks/useAppSave.ts +++ b/src/hooks/useAppSave.ts @@ -1,7 +1,6 @@ import { startTransition, useCallback, useEffect, useRef, type MutableRefObject } from 'react' import { invoke } from '@tauri-apps/api/core' import { useEditorSaveWithLinks } from './useEditorSaveWithLinks' -import { needsRenameOnSave } from './useNoteRename' import { flushEditorContent } from '../utils/autoSave' import { extractH1TitleFromContent } from '../utils/noteTitle' import { isTauri } from '../mock-tauri' @@ -77,20 +76,6 @@ function findUnsavedFallback({ return { path: activeTab.entry.path, content: activeTab.content } } -function activeTabNeedsRename({ - tabs, - activeTabPath, -}: { - tabs: TabState[] - activeTabPath: string | null -}): { path: string; title: string } | null { - const activeTab = tabs.find(t => t.entry.path === activeTabPath) - if (!activeTab) return null - return needsRenameOnSave(activeTab.entry.title, activeTab.entry.filename) - ? { path: activeTab.entry.path, title: activeTab.entry.title } - : null -} - function isUntitledRenameCandidate(path: string): boolean { const filename = path.split('/').pop() ?? '' const stem = filename.replace(/\.md$/, '') @@ -546,16 +531,6 @@ function useRenameHandlers({ replaceRenamedEntry: (oldPath: string, newEntry: Partial & { path: string }, newContent: string) => void loadModifiedFiles: AppSaveDeps['loadModifiedFiles'] }) { - const handleRenameTab = useCallback(async (path: string, newTitle: string) => { - const currentPath = await preparePathForManualRename({ - path, - resolveCurrentPath, - savePendingForPath, - cancelPendingUntitledRename, - }) - await handleRenameNote(currentPath, newTitle, resolvedPath, replaceRenamedEntry).then(loadModifiedFiles) - }, [resolveCurrentPath, savePendingForPath, cancelPendingUntitledRename, handleRenameNote, resolvedPath, replaceRenamedEntry, loadModifiedFiles]) - const handleFilenameRename = useCallback(async (path: string, newFilenameStem: string) => { const currentPath = await preparePathForManualRename({ path, @@ -578,12 +553,11 @@ function useRenameHandlers({ .catch((err) => console.error('Title rename failed:', err)) }, [resolveCurrentPath, savePendingForPath, cancelPendingUntitledRename, handleRenameNote, resolvedPath, replaceRenamedEntry, loadModifiedFiles]) - return { handleRenameTab, handleFilenameRename, handleTitleSync } + return { handleFilenameRename, handleTitleSync } } function useHandleSaveAction({ handleSaveRaw, - handleRenameTab, tabs, activeTabPath, unsavedPaths, @@ -591,7 +565,6 @@ function useHandleSaveAction({ resolveCurrentPath, }: { handleSaveRaw: (unsavedFallback?: { path: string; content: string }) => Promise - handleRenameTab: (path: string, newTitle: string) => Promise tabs: TabState[] activeTabPath: string | null unsavedPaths: Set @@ -605,10 +578,8 @@ function useHandleSaveAction({ activeTabPath: resolvedActiveTabPath, unsavedPaths, })) - const flushedUntitledRename = await flushPendingUntitledRename(resolvedActiveTabPath ?? undefined) - const rename = activeTabNeedsRename({ tabs, activeTabPath: resolvedActiveTabPath }) - if (!flushedUntitledRename && rename) await handleRenameTab(rename.path, rename.title) - }, [handleSaveRaw, handleRenameTab, tabs, activeTabPath, unsavedPaths, flushPendingUntitledRename, resolveCurrentPath]) + await flushPendingUntitledRename(resolvedActiveTabPath ?? undefined) + }, [handleSaveRaw, tabs, activeTabPath, unsavedPaths, flushPendingUntitledRename, resolveCurrentPath]) } function useEditorPersistence({ @@ -741,7 +712,7 @@ function useAppSaveHandlers({ setToastMessage, flushPendingUntitledRename, }) - const { handleRenameTab, handleFilenameRename, handleTitleSync } = useRenameHandlers({ + const { handleFilenameRename, handleTitleSync } = useRenameHandlers({ resolveCurrentPath, savePendingForPath, cancelPendingUntitledRename, @@ -753,7 +724,6 @@ function useAppSaveHandlers({ }) const handleSave = useHandleSaveAction({ handleSaveRaw, - handleRenameTab, tabs, activeTabPath, unsavedPaths, diff --git a/tests/smoke/breadcrumb-filename-rename.spec.ts b/tests/smoke/breadcrumb-filename-rename.spec.ts index c4e318b1..fcfbc88b 100644 --- a/tests/smoke/breadcrumb-filename-rename.spec.ts +++ b/tests/smoke/breadcrumb-filename-rename.spec.ts @@ -57,7 +57,7 @@ async function setRawEditorContent(page: Page, content: string) { }, content) } -test('breadcrumb sync button and inline rename keep the file path aligned with H1 title changes', async ({ page }) => { +test('@smoke Cmd+S preserves the existing filename until the explicit breadcrumb sync action', async ({ page }) => { const syncedPath = path.join(tempVaultDir, 'note', 'breadcrumb-sync-target.md') const manuallyRenamedPath = path.join(tempVaultDir, 'note', 'manual-breadcrumb-name.md') const originalPath = path.join(tempVaultDir, 'note', 'note-b.md')