fix: stop cmd+s from renaming note titles

This commit is contained in:
lucaronin
2026-04-18 08:25:43 +02:00
parent b78dfad384
commit 857d55a7cd
3 changed files with 29 additions and 35 deletions

View File

@@ -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'

View File

@@ -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<VaultEntry> & { 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<void>
handleRenameTab: (path: string, newTitle: string) => Promise<void>
tabs: TabState[]
activeTabPath: string | null
unsavedPaths: Set<string>
@@ -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,

View File

@@ -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')