2026-03-16 18:31:33 +01:00
|
|
|
import { useCallback, useRef } from 'react'
|
|
|
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
|
|
|
import { isTauri, mockInvoke } from '../mock-tauri'
|
2026-05-11 16:37:06 +02:00
|
|
|
import type { VaultEntry, WorkspaceIdentity } from '../types'
|
2026-03-16 18:31:33 +01:00
|
|
|
import { slugify } from './useNoteCreation'
|
2026-05-04 04:59:50 +02:00
|
|
|
import {
|
|
|
|
|
findByNotePath,
|
|
|
|
|
normalizeVaultRelativePath,
|
|
|
|
|
notePathFilename,
|
|
|
|
|
notePathsMatch,
|
|
|
|
|
vaultRelativePathLabel,
|
|
|
|
|
} from '../utils/notePathIdentity'
|
2026-05-11 16:37:06 +02:00
|
|
|
import { vaultPathForEntry } from '../utils/workspaces'
|
|
|
|
|
import { relativePathStem } from '../utils/wikilink'
|
2026-03-16 18:31:33 +01:00
|
|
|
|
|
|
|
|
interface RenameResult {
|
|
|
|
|
new_path: string
|
|
|
|
|
updated_files: number
|
2026-04-22 21:18:38 +02:00
|
|
|
failed_updates?: number
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { slugify }
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
interface RenameRequest {
|
|
|
|
|
path: string
|
|
|
|
|
newTitle: string
|
|
|
|
|
vaultPath: string
|
|
|
|
|
oldTitle?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FilenameRenameRequest {
|
|
|
|
|
path: string
|
|
|
|
|
newFilenameStem: string
|
|
|
|
|
vaultPath: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
interface FolderMoveRequest {
|
|
|
|
|
path: string
|
|
|
|
|
folderPath: string
|
|
|
|
|
vaultPath: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
interface WorkspaceMoveRequest {
|
|
|
|
|
path: string
|
|
|
|
|
sourceVaultPath: string
|
|
|
|
|
destinationVaultPath: string
|
|
|
|
|
replacementTarget?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
interface LoadNoteContentRequest {
|
|
|
|
|
path: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ReloadTabsAfterRenameRequest {
|
|
|
|
|
tabPaths: string[]
|
|
|
|
|
updateTabContent: (path: string, content: string) => void
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
type RenameCommand = 'rename_note' | 'rename_note_filename' | 'move_note_to_folder' | 'move_note_to_workspace'
|
|
|
|
|
type NoteCommandExtra = Record<string, unknown>
|
|
|
|
|
|
|
|
|
|
interface NoteCommandRequest {
|
|
|
|
|
command: RenameCommand
|
|
|
|
|
mockExtra: NoteCommandExtra
|
|
|
|
|
path: string
|
|
|
|
|
tauriExtra: NoteCommandExtra
|
|
|
|
|
vaultPath: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SingleValueNoteCommand {
|
|
|
|
|
command: RenameCommand
|
|
|
|
|
mockKey: string
|
|
|
|
|
tauriKey: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FILENAME_RENAME_COMMAND: SingleValueNoteCommand = {
|
|
|
|
|
command: 'rename_note_filename',
|
|
|
|
|
mockKey: 'new_filename_stem',
|
|
|
|
|
tauriKey: 'newFilenameStem',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FOLDER_MOVE_COMMAND: SingleValueNoteCommand = {
|
|
|
|
|
command: 'move_note_to_folder',
|
|
|
|
|
mockKey: 'folder_path',
|
|
|
|
|
tauriKey: 'folderPath',
|
|
|
|
|
}
|
2026-04-22 23:56:00 +02:00
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
/** Check if a note's filename doesn't match the slug of its current title. */
|
|
|
|
|
export function needsRenameOnSave(title: string, filename: string): boolean {
|
2026-04-17 23:32:36 +02:00
|
|
|
if (!filename.toLowerCase().endsWith('.md')) return false
|
2026-03-16 18:31:33 +01:00
|
|
|
return `${slugify(title)}.md` !== filename
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
export async function performRename({
|
|
|
|
|
path,
|
|
|
|
|
newTitle,
|
|
|
|
|
vaultPath,
|
|
|
|
|
oldTitle,
|
|
|
|
|
}: RenameRequest): Promise<RenameResult> {
|
2026-05-11 16:37:06 +02:00
|
|
|
const oldTitleArg = oldTitle ?? null
|
|
|
|
|
return invokeNoteCommand({
|
2026-04-22 23:56:00 +02:00
|
|
|
command: 'rename_note',
|
2026-05-11 16:37:06 +02:00
|
|
|
path,
|
|
|
|
|
vaultPath,
|
|
|
|
|
tauriExtra: { newTitle, oldTitle: oldTitleArg },
|
|
|
|
|
mockExtra: { new_title: newTitle, old_title: oldTitleArg },
|
2026-04-22 23:56:00 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function invokeRenameCommand(
|
|
|
|
|
params: {
|
|
|
|
|
command: RenameCommand
|
|
|
|
|
tauriArgs: Record<string, unknown>
|
|
|
|
|
mockArgs: Record<string, unknown>
|
|
|
|
|
},
|
|
|
|
|
): Promise<RenameResult> {
|
|
|
|
|
return isTauri()
|
|
|
|
|
? invoke<RenameResult>(params.command, params.tauriArgs)
|
|
|
|
|
: mockInvoke<RenameResult>(params.command, params.mockArgs)
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
function invokeStructuredRenameCommand(params: {
|
|
|
|
|
command: RenameCommand
|
|
|
|
|
args: Record<string, unknown>
|
|
|
|
|
mockArgs: Record<string, unknown>
|
|
|
|
|
}): Promise<RenameResult> {
|
|
|
|
|
return invokeRenameCommand({
|
|
|
|
|
command: params.command,
|
|
|
|
|
tauriArgs: { args: params.args },
|
|
|
|
|
mockArgs: params.mockArgs,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function noteCommandArgs({
|
|
|
|
|
mockExtra,
|
|
|
|
|
path,
|
|
|
|
|
style,
|
|
|
|
|
tauriExtra,
|
|
|
|
|
vaultPath,
|
|
|
|
|
}: {
|
|
|
|
|
mockExtra: NoteCommandExtra
|
|
|
|
|
path: string
|
|
|
|
|
style: 'mock' | 'tauri'
|
|
|
|
|
tauriExtra: NoteCommandExtra
|
|
|
|
|
vaultPath: string
|
|
|
|
|
}) {
|
|
|
|
|
return style === 'tauri'
|
|
|
|
|
? { vaultPath, oldPath: path, ...tauriExtra }
|
|
|
|
|
: { vault_path: vaultPath, old_path: path, ...mockExtra }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function invokeNoteCommand(params: NoteCommandRequest): Promise<RenameResult> {
|
|
|
|
|
return invokeStructuredRenameCommand({
|
|
|
|
|
command: params.command,
|
|
|
|
|
args: noteCommandArgs({ ...params, style: 'tauri' }),
|
|
|
|
|
mockArgs: noteCommandArgs({ ...params, style: 'mock' }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performSingleValueNoteCommand({
|
|
|
|
|
descriptor,
|
|
|
|
|
path,
|
|
|
|
|
value,
|
|
|
|
|
vaultPath,
|
|
|
|
|
}: {
|
|
|
|
|
descriptor: SingleValueNoteCommand
|
|
|
|
|
path: string
|
|
|
|
|
value: string
|
|
|
|
|
vaultPath: string
|
|
|
|
|
}) {
|
|
|
|
|
return invokeNoteCommand({
|
|
|
|
|
command: descriptor.command,
|
|
|
|
|
path,
|
|
|
|
|
vaultPath,
|
|
|
|
|
tauriExtra: { [descriptor.tauriKey]: value },
|
|
|
|
|
mockExtra: { [descriptor.mockKey]: value },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
export async function performFilenameRename({
|
|
|
|
|
path,
|
|
|
|
|
newFilenameStem,
|
|
|
|
|
vaultPath,
|
|
|
|
|
}: FilenameRenameRequest): Promise<RenameResult> {
|
2026-05-11 16:37:06 +02:00
|
|
|
return performSingleValueNoteCommand({
|
|
|
|
|
descriptor: FILENAME_RENAME_COMMAND,
|
|
|
|
|
path,
|
|
|
|
|
value: newFilenameStem,
|
|
|
|
|
vaultPath,
|
2026-04-22 23:56:00 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function performMoveNoteToFolder({
|
|
|
|
|
path,
|
|
|
|
|
folderPath,
|
|
|
|
|
vaultPath,
|
|
|
|
|
}: FolderMoveRequest): Promise<RenameResult> {
|
2026-05-11 16:37:06 +02:00
|
|
|
return performSingleValueNoteCommand({
|
|
|
|
|
descriptor: FOLDER_MOVE_COMMAND,
|
|
|
|
|
path,
|
|
|
|
|
value: folderPath,
|
|
|
|
|
vaultPath,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function performMoveNoteToWorkspace({
|
|
|
|
|
path,
|
|
|
|
|
sourceVaultPath,
|
|
|
|
|
destinationVaultPath,
|
|
|
|
|
replacementTarget,
|
|
|
|
|
}: WorkspaceMoveRequest): Promise<RenameResult> {
|
|
|
|
|
const tauriReplacementTarget = replacementTarget ?? null
|
2026-04-22 23:56:00 +02:00
|
|
|
return invokeRenameCommand({
|
2026-05-11 16:37:06 +02:00
|
|
|
command: 'move_note_to_workspace',
|
|
|
|
|
tauriArgs: {
|
|
|
|
|
args: {
|
|
|
|
|
sourceVaultPath,
|
|
|
|
|
destinationVaultPath,
|
|
|
|
|
oldPath: path,
|
|
|
|
|
replacementTarget: tauriReplacementTarget,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mockArgs: {
|
|
|
|
|
source_vault_path: sourceVaultPath,
|
|
|
|
|
destination_vault_path: destinationVaultPath,
|
|
|
|
|
old_path: path,
|
|
|
|
|
replacement_target: tauriReplacementTarget,
|
|
|
|
|
},
|
2026-04-10 17:59:21 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
export function buildRenamedEntry(entry: VaultEntry, newTitle: string, newPath: string): VaultEntry {
|
2026-05-04 04:59:50 +02:00
|
|
|
const filename = notePathFilename(newPath)
|
2026-04-25 23:08:30 +02:00
|
|
|
return { ...entry, path: newPath, filename, title: newTitle }
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 17:59:21 +02:00
|
|
|
export function buildFilenameRenamedEntry(entry: VaultEntry, newPath: string): VaultEntry {
|
2026-05-04 04:59:50 +02:00
|
|
|
const filename = notePathFilename(newPath)
|
2026-04-10 17:59:21 +02:00
|
|
|
return { ...entry, path: newPath, filename }
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
export function buildWorkspaceMovedEntry(
|
|
|
|
|
entry: VaultEntry,
|
|
|
|
|
newPath: string,
|
|
|
|
|
workspace: WorkspaceIdentity,
|
|
|
|
|
): VaultEntry {
|
|
|
|
|
return {
|
|
|
|
|
...buildFilenameRenamedEntry(entry, newPath),
|
|
|
|
|
workspace,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function workspaceMoveReplacementTarget({
|
|
|
|
|
entry,
|
|
|
|
|
sourceVaultPath,
|
|
|
|
|
destinationWorkspace,
|
|
|
|
|
}: {
|
|
|
|
|
entry: VaultEntry
|
|
|
|
|
sourceVaultPath: string
|
|
|
|
|
destinationWorkspace: WorkspaceIdentity
|
|
|
|
|
}): string {
|
|
|
|
|
const localTarget = relativePathStem(entry.path, sourceVaultPath)
|
|
|
|
|
const sourceAlias = entry.workspace?.alias
|
|
|
|
|
return destinationWorkspace.alias && destinationWorkspace.alias !== sourceAlias
|
|
|
|
|
? `${destinationWorkspace.alias}/${localTarget}`
|
|
|
|
|
: localTarget
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
export async function loadNoteContent({ path }: LoadNoteContentRequest): Promise<string> {
|
2026-03-16 18:31:33 +01:00
|
|
|
return isTauri()
|
|
|
|
|
? invoke<string>('get_note_content', { path })
|
|
|
|
|
: mockInvoke<string>('get_note_content', { path })
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
function rewriteSummaryLabel(params: { updatedFiles: number }): string {
|
|
|
|
|
return params.updatedFiles === 1 ? 'Updated 1 note' : `Updated ${params.updatedFiles} notes`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function manualUpdateWarning(params: { failedUpdates: number }): string {
|
|
|
|
|
const { failedUpdates } = params
|
|
|
|
|
return `${failedUpdates} linked note${failedUpdates > 1 ? 's' : ''} need${failedUpdates === 1 ? 's' : ''} manual updates`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatRewriteToast(
|
|
|
|
|
params: {
|
|
|
|
|
action: string
|
|
|
|
|
updatedFiles: number
|
|
|
|
|
failedUpdates?: number
|
|
|
|
|
preferBareUpdate?: boolean
|
|
|
|
|
},
|
|
|
|
|
): string {
|
|
|
|
|
const {
|
|
|
|
|
action,
|
|
|
|
|
updatedFiles,
|
|
|
|
|
failedUpdates = 0,
|
|
|
|
|
preferBareUpdate = false,
|
|
|
|
|
} = params
|
2026-04-22 21:18:38 +02:00
|
|
|
if (failedUpdates > 0) {
|
|
|
|
|
if (updatedFiles === 0) {
|
2026-04-22 23:56:00 +02:00
|
|
|
return `${action}, but ${manualUpdateWarning({ failedUpdates })}`
|
|
|
|
|
}
|
|
|
|
|
if (preferBareUpdate) {
|
|
|
|
|
return `${rewriteSummaryLabel({ updatedFiles })}, but ${manualUpdateWarning({ failedUpdates })}`
|
2026-04-22 21:18:38 +02:00
|
|
|
}
|
2026-04-22 23:56:00 +02:00
|
|
|
return `${action} and ${rewriteSummaryLabel({ updatedFiles }).toLowerCase()}, but ${manualUpdateWarning({ failedUpdates })}`
|
2026-04-22 21:18:38 +02:00
|
|
|
}
|
2026-04-22 23:56:00 +02:00
|
|
|
if (updatedFiles === 0) return action
|
|
|
|
|
return preferBareUpdate
|
|
|
|
|
? rewriteSummaryLabel({ updatedFiles })
|
|
|
|
|
: `${action} and ${rewriteSummaryLabel({ updatedFiles }).toLowerCase()}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renameToastMessage(updatedFiles: number, failedUpdates = 0): string {
|
|
|
|
|
return formatRewriteToast({ action: 'Renamed', updatedFiles, failedUpdates, preferBareUpdate: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function folderLabel(params: { folderPath: string }): string {
|
2026-05-04 04:59:50 +02:00
|
|
|
return vaultRelativePathLabel(params.folderPath)
|
2026-04-22 23:56:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function moveToastMessage(folderPath: string, updatedFiles: number, failedUpdates = 0): string {
|
|
|
|
|
return formatRewriteToast({
|
|
|
|
|
action: `Moved to "${folderLabel({ folderPath })}"`,
|
|
|
|
|
updatedFiles,
|
|
|
|
|
failedUpdates,
|
|
|
|
|
})
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
function moveWorkspaceToastMessage(workspaceLabel: string, updatedFiles: number, failedUpdates = 0): string {
|
|
|
|
|
return formatRewriteToast({
|
|
|
|
|
action: `Moved to "${workspaceLabel}"`,
|
|
|
|
|
updatedFiles,
|
|
|
|
|
failedUpdates,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 17:44:41 +02:00
|
|
|
export async function reloadVaultAfterRename(reloadVault?: () => Promise<unknown>): Promise<void> {
|
|
|
|
|
if (!reloadVault) return
|
|
|
|
|
try {
|
|
|
|
|
await reloadVault()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('Failed to reload vault after rename:', err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
/** Reload content for open tabs whose wikilinks may have changed after a rename. */
|
2026-04-13 17:44:41 +02:00
|
|
|
export async function reloadTabsAfterRename({
|
|
|
|
|
tabPaths,
|
|
|
|
|
updateTabContent,
|
|
|
|
|
}: ReloadTabsAfterRenameRequest): Promise<void> {
|
2026-03-16 18:31:33 +01:00
|
|
|
for (const tabPath of tabPaths) {
|
|
|
|
|
try {
|
2026-04-13 17:44:41 +02:00
|
|
|
updateTabContent(tabPath, await loadNoteContent({ path: tabPath }))
|
2026-03-16 18:31:33 +01:00
|
|
|
} catch { /* skip tabs that fail to reload */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Tab {
|
|
|
|
|
entry: VaultEntry
|
|
|
|
|
content: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:40:50 +02:00
|
|
|
function findRenameEntry(entries: VaultEntry[], tabs: Tab[], path: string): VaultEntry | undefined {
|
2026-05-04 04:59:50 +02:00
|
|
|
return findByNotePath(entries, path)
|
|
|
|
|
?? tabs.find((tab) => notePathsMatch(tab.entry.path, path))?.entry
|
2026-05-03 19:40:50 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
function resolveRenameVaultPath(entry: VaultEntry | undefined, fallbackVaultPath: string): string {
|
|
|
|
|
return entry ? vaultPathForEntry(entry, fallbackVaultPath) : fallbackVaultPath
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 17:59:21 +02:00
|
|
|
function renameErrorMessage(err: unknown): string {
|
|
|
|
|
const message = typeof err === 'string'
|
|
|
|
|
? err.trim()
|
|
|
|
|
: err instanceof Error
|
|
|
|
|
? err.message.trim()
|
|
|
|
|
: ''
|
|
|
|
|
if (message === 'A note with that name already exists' || message === 'Invalid filename') {
|
|
|
|
|
return message
|
|
|
|
|
}
|
|
|
|
|
return 'Failed to rename note'
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
function moveNoteErrorMessage(err: unknown): string {
|
|
|
|
|
const message = typeof err === 'string'
|
|
|
|
|
? err.trim()
|
|
|
|
|
: err instanceof Error
|
|
|
|
|
? err.message.trim()
|
|
|
|
|
: ''
|
|
|
|
|
return message || 'Failed to move note'
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 18:31:33 +01:00
|
|
|
export interface NoteRenameConfig {
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
setToastMessage: (msg: string | null) => void
|
2026-04-13 17:44:41 +02:00
|
|
|
reloadVault?: () => Promise<unknown>
|
2026-05-11 16:37:06 +02:00
|
|
|
onPathRenamed?: (oldPath: string, newPath: string) => void
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RenameTabDeps {
|
|
|
|
|
tabs: Tab[]
|
|
|
|
|
setTabs: React.Dispatch<React.SetStateAction<Tab[]>>
|
|
|
|
|
activeTabPathRef: React.MutableRefObject<string | null>
|
|
|
|
|
handleSwitchTab: (path: string) => void
|
|
|
|
|
updateTabContent: (path: string, content: string) => void
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
interface ApplyRenameOptions {
|
|
|
|
|
successMessage?: (result: RenameResult) => string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useRenameResultApplier(
|
|
|
|
|
config: NoteRenameConfig,
|
|
|
|
|
tabDeps: RenameTabDeps,
|
|
|
|
|
) {
|
2026-05-11 16:37:06 +02:00
|
|
|
const { entries, setToastMessage, reloadVault, onPathRenamed } = config
|
2026-03-16 18:31:33 +01:00
|
|
|
const { setTabs, activeTabPathRef, handleSwitchTab, updateTabContent } = tabDeps
|
|
|
|
|
|
|
|
|
|
const tabsRef = useRef(tabDeps.tabs)
|
|
|
|
|
// eslint-disable-next-line react-hooks/refs
|
|
|
|
|
tabsRef.current = tabDeps.tabs
|
|
|
|
|
|
2026-04-10 17:59:21 +02:00
|
|
|
const applyRenameResult = useCallback(async (
|
|
|
|
|
oldPath: string,
|
|
|
|
|
result: RenameResult,
|
|
|
|
|
buildEntry: (entry: VaultEntry | undefined, newPath: string) => VaultEntry,
|
|
|
|
|
onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void,
|
2026-04-22 23:56:00 +02:00
|
|
|
options?: ApplyRenameOptions,
|
2026-04-10 17:59:21 +02:00
|
|
|
) => {
|
2026-05-03 19:40:50 +02:00
|
|
|
const currentTabs = tabsRef.current
|
|
|
|
|
const entry = findRenameEntry(entries, currentTabs, oldPath)
|
2026-04-13 17:44:41 +02:00
|
|
|
const newContent = await loadNoteContent({ path: result.new_path })
|
2026-04-10 17:59:21 +02:00
|
|
|
const newEntry = buildEntry(entry, result.new_path)
|
2026-05-04 04:59:50 +02:00
|
|
|
const otherTabPaths = currentTabs
|
|
|
|
|
.filter((tab) => !notePathsMatch(tab.entry.path, oldPath) && !notePathsMatch(tab.entry.path, result.new_path))
|
|
|
|
|
.map((tab) => tab.entry.path)
|
2026-05-11 16:37:06 +02:00
|
|
|
if (!notePathsMatch(oldPath, result.new_path)) onPathRenamed?.(oldPath, result.new_path)
|
2026-05-04 04:59:50 +02:00
|
|
|
setTabs((prev) => prev.map((tab) => notePathsMatch(tab.entry.path, oldPath) ? { entry: newEntry, content: newContent } : tab))
|
|
|
|
|
if (notePathsMatch(activeTabPathRef.current, oldPath)) handleSwitchTab(result.new_path)
|
2026-04-10 17:59:21 +02:00
|
|
|
onEntryRenamed(oldPath, newEntry, newContent)
|
2026-04-13 17:44:41 +02:00
|
|
|
await reloadTabsAfterRename({ tabPaths: otherTabPaths, updateTabContent })
|
|
|
|
|
await reloadVaultAfterRename(reloadVault)
|
2026-04-22 23:56:00 +02:00
|
|
|
const successMessage = options?.successMessage
|
|
|
|
|
? options.successMessage(result)
|
|
|
|
|
: renameToastMessage(result.updated_files, result.failed_updates ?? 0)
|
|
|
|
|
setToastMessage(successMessage)
|
|
|
|
|
return result
|
2026-05-11 16:37:06 +02:00
|
|
|
}, [entries, setTabs, activeTabPathRef, handleSwitchTab, updateTabContent, reloadVault, setToastMessage, onPathRenamed])
|
2026-04-10 17:59:21 +02:00
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
return {
|
|
|
|
|
tabsRef,
|
|
|
|
|
applyRenameResult,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runRenameAction({
|
|
|
|
|
path,
|
|
|
|
|
perform,
|
|
|
|
|
applyRenameResult,
|
|
|
|
|
buildEntry,
|
|
|
|
|
onEntryRenamed,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
errorMessage,
|
|
|
|
|
logLabel,
|
|
|
|
|
successMessage,
|
|
|
|
|
allowUnchangedResult = false,
|
|
|
|
|
}: {
|
|
|
|
|
path: string
|
|
|
|
|
perform: () => Promise<RenameResult>
|
|
|
|
|
applyRenameResult: (
|
|
|
|
|
oldPath: string,
|
|
|
|
|
result: RenameResult,
|
|
|
|
|
buildEntry: (entry: VaultEntry | undefined, newPath: string) => VaultEntry,
|
|
|
|
|
onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void,
|
|
|
|
|
options?: ApplyRenameOptions,
|
|
|
|
|
) => Promise<RenameResult>
|
|
|
|
|
buildEntry: (entry: VaultEntry | undefined, newPath: string) => VaultEntry
|
|
|
|
|
onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void
|
|
|
|
|
setToastMessage: (message: string | null) => void
|
|
|
|
|
errorMessage: (err: unknown) => string
|
|
|
|
|
logLabel: string
|
|
|
|
|
successMessage?: (result: RenameResult) => string
|
|
|
|
|
allowUnchangedResult?: boolean
|
|
|
|
|
}): Promise<RenameResult | null> {
|
|
|
|
|
try {
|
|
|
|
|
const result = await perform()
|
2026-05-04 04:59:50 +02:00
|
|
|
if (allowUnchangedResult && notePathsMatch(result.new_path, path)) return result
|
2026-04-22 23:56:00 +02:00
|
|
|
await applyRenameResult(path, result, buildEntry, onEntryRenamed, { successMessage })
|
|
|
|
|
return result
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`${logLabel}:`, err)
|
|
|
|
|
setToastMessage(errorMessage(err))
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
type ApplyRenameResult = ReturnType<typeof useRenameResultApplier>['applyRenameResult']
|
|
|
|
|
|
|
|
|
|
function useWorkspaceMoveHandler({
|
|
|
|
|
applyRenameResult,
|
|
|
|
|
entries,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
tabsRef,
|
|
|
|
|
}: {
|
|
|
|
|
applyRenameResult: ApplyRenameResult
|
|
|
|
|
entries: VaultEntry[]
|
|
|
|
|
setToastMessage: (message: string | null) => void
|
|
|
|
|
tabsRef: React.MutableRefObject<Tab[]>
|
|
|
|
|
}) {
|
|
|
|
|
return useCallback(async (
|
|
|
|
|
path: string,
|
|
|
|
|
destinationWorkspace: WorkspaceIdentity,
|
|
|
|
|
vaultPath: string,
|
|
|
|
|
onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void,
|
|
|
|
|
) => {
|
|
|
|
|
const entry = findRenameEntry(entries, tabsRef.current, path)
|
|
|
|
|
if (!entry) {
|
|
|
|
|
setToastMessage('Failed to move note: note not found')
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sourceVaultPath = resolveRenameVaultPath(entry, vaultPath)
|
|
|
|
|
if (sourceVaultPath === destinationWorkspace.path) {
|
|
|
|
|
return { new_path: path, updated_files: 0, failed_updates: 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return runRenameAction({
|
|
|
|
|
path,
|
|
|
|
|
perform: () => performMoveNoteToWorkspace({
|
|
|
|
|
path,
|
|
|
|
|
sourceVaultPath,
|
|
|
|
|
destinationVaultPath: destinationWorkspace.path,
|
|
|
|
|
replacementTarget: workspaceMoveReplacementTarget({ entry, sourceVaultPath, destinationWorkspace }),
|
|
|
|
|
}),
|
|
|
|
|
applyRenameResult,
|
|
|
|
|
buildEntry: (currentEntry, newPath) => buildWorkspaceMovedEntry(currentEntry ?? entry, newPath, destinationWorkspace),
|
|
|
|
|
onEntryRenamed,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
errorMessage: moveNoteErrorMessage,
|
|
|
|
|
logLabel: 'Failed to move note to workspace',
|
|
|
|
|
successMessage: (result) => moveWorkspaceToastMessage(
|
|
|
|
|
destinationWorkspace.label,
|
|
|
|
|
result.updated_files,
|
|
|
|
|
result.failed_updates ?? 0,
|
|
|
|
|
),
|
|
|
|
|
allowUnchangedResult: true,
|
|
|
|
|
})
|
|
|
|
|
}, [applyRenameResult, entries, setToastMessage, tabsRef])
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 23:56:00 +02:00
|
|
|
export function useNoteRename(config: NoteRenameConfig, tabDeps: RenameTabDeps) {
|
|
|
|
|
const { entries, setToastMessage } = config
|
|
|
|
|
const { tabsRef, applyRenameResult } = useRenameResultApplier(config, tabDeps)
|
|
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
const handleRenameNote = useCallback(async (path: string, newTitle: string, vaultPath: string, onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void) => {
|
2026-05-03 19:40:50 +02:00
|
|
|
const entry = findRenameEntry(entries, tabsRef.current, path)
|
2026-05-11 16:37:06 +02:00
|
|
|
const renameVaultPath = resolveRenameVaultPath(entry, vaultPath)
|
2026-04-22 23:56:00 +02:00
|
|
|
await runRenameAction({
|
|
|
|
|
path,
|
2026-05-11 16:37:06 +02:00
|
|
|
perform: () => performRename({ path, newTitle, vaultPath: renameVaultPath, oldTitle: entry?.title }),
|
2026-04-22 23:56:00 +02:00
|
|
|
applyRenameResult,
|
2026-05-11 16:37:06 +02:00
|
|
|
buildEntry: (currentEntry, newPath) => buildRenamedEntry(currentEntry ?? ({} as VaultEntry), newTitle, newPath),
|
2026-04-22 23:56:00 +02:00
|
|
|
onEntryRenamed,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
errorMessage: renameErrorMessage,
|
|
|
|
|
logLabel: 'Failed to rename note',
|
|
|
|
|
})
|
2026-05-03 19:40:50 +02:00
|
|
|
}, [entries, tabsRef, applyRenameResult, setToastMessage])
|
2026-04-10 17:59:21 +02:00
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
const handleRenameFilename = useCallback(async (path: string, newFilenameStem: string, vaultPath: string, onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void) => {
|
|
|
|
|
const entry = findRenameEntry(entries, tabsRef.current, path)
|
|
|
|
|
const renameVaultPath = resolveRenameVaultPath(entry, vaultPath)
|
2026-04-22 23:56:00 +02:00
|
|
|
await runRenameAction({
|
|
|
|
|
path,
|
2026-05-11 16:37:06 +02:00
|
|
|
perform: () => performFilenameRename({ path, newFilenameStem, vaultPath: renameVaultPath }),
|
2026-04-22 23:56:00 +02:00
|
|
|
applyRenameResult,
|
2026-05-11 16:37:06 +02:00
|
|
|
buildEntry: (currentEntry, newPath) => buildFilenameRenamedEntry(currentEntry ?? ({} as VaultEntry), newPath),
|
2026-04-22 23:56:00 +02:00
|
|
|
onEntryRenamed,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
errorMessage: renameErrorMessage,
|
|
|
|
|
logLabel: 'Failed to rename note filename',
|
|
|
|
|
})
|
2026-05-11 16:37:06 +02:00
|
|
|
}, [entries, tabsRef, applyRenameResult, setToastMessage])
|
2026-04-22 23:56:00 +02:00
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
const handleMoveNoteToFolder = useCallback(async (path: string, folderPath: string, vaultPath: string, onEntryRenamed: (oldPath: string, newEntry: Partial<VaultEntry> & { path: string }, newContent: string) => void) => {
|
2026-05-04 04:59:50 +02:00
|
|
|
const normalizedFolderPath = normalizeVaultRelativePath(folderPath)
|
2026-05-11 16:37:06 +02:00
|
|
|
const entry = findRenameEntry(entries, tabsRef.current, path)
|
|
|
|
|
const moveVaultPath = resolveRenameVaultPath(entry, vaultPath)
|
2026-04-22 23:56:00 +02:00
|
|
|
return runRenameAction({
|
|
|
|
|
path,
|
2026-05-11 16:37:06 +02:00
|
|
|
perform: () => performMoveNoteToFolder({ path, folderPath: normalizedFolderPath, vaultPath: moveVaultPath }),
|
2026-04-22 23:56:00 +02:00
|
|
|
applyRenameResult,
|
2026-05-11 16:37:06 +02:00
|
|
|
buildEntry: (currentEntry, newPath) => buildFilenameRenamedEntry(currentEntry ?? ({} as VaultEntry), newPath),
|
2026-04-22 23:56:00 +02:00
|
|
|
onEntryRenamed,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
errorMessage: moveNoteErrorMessage,
|
|
|
|
|
logLabel: 'Failed to move note to folder',
|
2026-05-11 16:37:06 +02:00
|
|
|
successMessage: (result) => moveToastMessage(normalizedFolderPath, result.updated_files, result.failed_updates ?? 0),
|
2026-04-22 23:56:00 +02:00
|
|
|
allowUnchangedResult: true,
|
|
|
|
|
})
|
2026-05-11 16:37:06 +02:00
|
|
|
}, [entries, tabsRef, applyRenameResult, setToastMessage])
|
|
|
|
|
|
|
|
|
|
const handleMoveNoteToWorkspace = useWorkspaceMoveHandler({
|
|
|
|
|
applyRenameResult,
|
|
|
|
|
entries,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
tabsRef,
|
|
|
|
|
})
|
2026-03-16 18:31:33 +01:00
|
|
|
|
2026-05-11 16:37:06 +02:00
|
|
|
return { handleRenameNote, handleRenameFilename, handleMoveNoteToFolder, handleMoveNoteToWorkspace, tabsRef }
|
2026-03-16 18:31:33 +01:00
|
|
|
}
|