mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-08 06:14:36 +02:00
feat: add smart rename to sync filename with front matter
Add a 'Smart Rename' feature that regenerates the filename from current front matter values (title and publish date) using the same logic as content creation. This helps users keep filenames in sync when they change the title or publish date after file creation. Changes: - Add ArticleHelper.smartRename() core logic with page bundle support - Add SmartRenameAction button to the panel webview Actions section - Add smart rename option to dashboard content actions dropdown - Add CommandToCode.smartRename and DashboardMessage.smartRename - Add PanelAction type support for disabling the action - Add localization keys for all UI strings Closes #545 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -524,6 +524,8 @@
|
||||
|
||||
"panel.slugAction.title": "Optimize slug",
|
||||
|
||||
"panel.smartRenameAction.title": "Smart rename",
|
||||
|
||||
"panel.spinner.loading": "Loading...",
|
||||
|
||||
"panel.startServerbutton.start": "Start server",
|
||||
@@ -550,6 +552,13 @@
|
||||
"commands.article.rename.fileName.title": "Rename: {0}",
|
||||
"commands.article.rename.fileName.prompt": "File name",
|
||||
|
||||
"commands.article.smartRename.alreadyInSync": "The filename is already in sync with the front matter",
|
||||
"commands.article.smartRename.success": "File renamed from \"{0}\" to \"{1}\"",
|
||||
"commands.article.smartRename.unableToGenerate": "Unable to generate a new filename from the front matter",
|
||||
"commands.article.smartRename.fileExists.error": "A file with the name \"{0}\" already exists",
|
||||
|
||||
"dashboard.contents.contentActions.menuItem.smartRename": "Smart rename",
|
||||
|
||||
"commands.cache.cleared": "Cache cleared",
|
||||
|
||||
"commands.chatbot.title": "Ask me anything",
|
||||
|
||||
@@ -32,6 +32,7 @@ export enum DashboardMessage {
|
||||
pinItem = 'pinItem',
|
||||
unpinItem = 'unpinItem',
|
||||
rename = 'rename',
|
||||
smartRename = 'smartRename',
|
||||
moveFile = 'moveFile',
|
||||
|
||||
// Media Dashboard
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
TrashIcon,
|
||||
LanguageIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
ArrowRightCircleIcon
|
||||
ArrowRightCircleIcon,
|
||||
ArrowPathIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, I18nConfig } from '../../../models';
|
||||
@@ -75,6 +76,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
messageHandler.send(DashboardMessage.rename, path);
|
||||
}, [path])
|
||||
|
||||
const onSmartRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
messageHandler.send(DashboardMessage.smartRename, path);
|
||||
}, [path])
|
||||
|
||||
const onOpenWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
openOnWebsite(settings?.websiteUrl, path);
|
||||
@@ -144,6 +150,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
<span>{l10n.t(LocalizationKey.commonRename)}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={onSmartRename}>
|
||||
<ArrowPathIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemSmartRename)}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{
|
||||
settings?.websiteUrl && (
|
||||
<DropdownMenuItem onClick={onOpenWebsite}>
|
||||
|
||||
@@ -236,6 +236,146 @@ export class ArticleHelper {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart rename a file based on its front matter title and publish date.
|
||||
* Regenerates the expected filename using the same logic as content creation.
|
||||
* @param filePath - The path of the file to be renamed.
|
||||
*/
|
||||
public static async smartRename(filePath?: string) {
|
||||
if (!filePath) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
filePath = editor.document.uri.fsPath;
|
||||
}
|
||||
|
||||
filePath = parseWinPath(filePath);
|
||||
const fileUri = Uri.file(filePath);
|
||||
|
||||
const article = await ArticleHelper.getFrontMatterByPath(filePath);
|
||||
if (!article || !article.data) {
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.commandsArticleRenameFileNotExistsError)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const titleField = getTitleField();
|
||||
const title: string = article.data[titleField];
|
||||
if (!title) {
|
||||
Notifications.warning(
|
||||
l10n.t(LocalizationKey.commandsArticleSmartRenameUnableToGenerate)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = await ArticleHelper.getContentType(article);
|
||||
const articleDate = await ArticleHelper.getDate(article);
|
||||
|
||||
let filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
filePrefix = await ArticleHelper.getFilePrefix(
|
||||
filePrefix,
|
||||
filePath,
|
||||
contentType,
|
||||
title,
|
||||
articleDate
|
||||
);
|
||||
|
||||
const sanitizedName = ArticleHelper.sanitize(title);
|
||||
const parsed = parseFile(filePath);
|
||||
const folderPath = dirname(fileUri.fsPath);
|
||||
|
||||
let newFileName: string;
|
||||
if (contentType?.pageBundle) {
|
||||
// For page bundles, the folder name should be updated
|
||||
if (filePrefix && typeof filePrefix === 'string') {
|
||||
if (filePrefix.endsWith('/')) {
|
||||
newFileName = `${filePrefix}${sanitizedName}`;
|
||||
} else {
|
||||
newFileName = `${filePrefix}-${sanitizedName}`;
|
||||
}
|
||||
} else {
|
||||
newFileName = sanitizedName;
|
||||
}
|
||||
|
||||
const parentFolder = dirname(folderPath);
|
||||
const currentFolderName = parseFile(folderPath).base;
|
||||
|
||||
if (currentFolderName === newFileName) {
|
||||
Notifications.info(
|
||||
l10n.t(LocalizationKey.commandsArticleSmartRenameAlreadyInSync)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFolderPath = join(parentFolder, newFileName);
|
||||
if (await existsAsync(newFolderPath)) {
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.commandsArticleSmartRenameFileExistsError, newFileName)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await workspace.fs.rename(Uri.file(folderPath), Uri.file(newFolderPath), {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
Notifications.info(
|
||||
l10n.t(
|
||||
LocalizationKey.commandsArticleSmartRenameSuccess,
|
||||
currentFolderName,
|
||||
newFileName
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// For regular files, rename the file
|
||||
let newFileBase = `${sanitizedName}${parsed.ext}`;
|
||||
if (filePrefix && typeof filePrefix === 'string') {
|
||||
if (filePrefix.endsWith('/')) {
|
||||
newFileBase = `${filePrefix}${newFileBase}`;
|
||||
} else {
|
||||
newFileBase = `${filePrefix}-${newFileBase}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.base === newFileBase) {
|
||||
Notifications.info(
|
||||
l10n.t(LocalizationKey.commandsArticleSmartRenameAlreadyInSync)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFileUri = Uri.joinPath(Uri.file(folderPath), newFileBase);
|
||||
if (await existsAsync(newFileUri.fsPath)) {
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.commandsArticleSmartRenameFileExistsError, newFileBase)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the document if it's open in an editor before renaming
|
||||
const openEditors = vscode.window.visibleTextEditors.filter(
|
||||
(e) => parseWinPath(e.document.uri.fsPath) === filePath
|
||||
);
|
||||
for (const editor of openEditors) {
|
||||
await editor.document.save();
|
||||
}
|
||||
|
||||
await workspace.fs.rename(fileUri, newFileUri, {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
Notifications.info(
|
||||
l10n.t(
|
||||
LocalizationKey.commandsArticleSmartRenameSuccess,
|
||||
parsed.base,
|
||||
newFileBase
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the update to be applied to the article.
|
||||
* @param article
|
||||
|
||||
@@ -74,6 +74,9 @@ export class PagesListener extends BaseListener {
|
||||
case DashboardMessage.moveFile:
|
||||
await this.moveFile(msg.payload);
|
||||
break;
|
||||
case DashboardMessage.smartRename:
|
||||
ArticleHelper.smartRename(msg.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ export class ArticleListener extends BaseListener {
|
||||
case CommandToCode.publish:
|
||||
Article.toggleDraft();
|
||||
break;
|
||||
case CommandToCode.smartRename:
|
||||
ArticleHelper.smartRename();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1696,6 +1696,10 @@ export enum LocalizationKey {
|
||||
* Optimize slug
|
||||
*/
|
||||
panelSlugActionTitle = 'panel.slugAction.title',
|
||||
/**
|
||||
* Smart rename
|
||||
*/
|
||||
panelSmartRenameActionTitle = 'panel.smartRenameAction.title',
|
||||
/**
|
||||
* Loading...
|
||||
*/
|
||||
@@ -1772,6 +1776,26 @@ export enum LocalizationKey {
|
||||
* File name
|
||||
*/
|
||||
commandsArticleRenameFileNamePrompt = 'commands.article.rename.fileName.prompt',
|
||||
/**
|
||||
* The filename is already in sync with the front matter
|
||||
*/
|
||||
commandsArticleSmartRenameAlreadyInSync = 'commands.article.smartRename.alreadyInSync',
|
||||
/**
|
||||
* File renamed from "{0}" to "{1}"
|
||||
*/
|
||||
commandsArticleSmartRenameSuccess = 'commands.article.smartRename.success',
|
||||
/**
|
||||
* Unable to generate a new filename from the front matter
|
||||
*/
|
||||
commandsArticleSmartRenameUnableToGenerate = 'commands.article.smartRename.unableToGenerate',
|
||||
/**
|
||||
* A file with the name "{0}" already exists
|
||||
*/
|
||||
commandsArticleSmartRenameFileExistsError = 'commands.article.smartRename.fileExists.error',
|
||||
/**
|
||||
* Smart rename
|
||||
*/
|
||||
dashboardContentsContentActionsMenuItemSmartRename = 'dashboard.contents.contentActions.menuItem.smartRename',
|
||||
/**
|
||||
* Cache cleared
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,7 @@ export type PanelAction =
|
||||
| 'openDashboard'
|
||||
| 'createContent'
|
||||
| 'optimizeSlug'
|
||||
| 'smartRename'
|
||||
| 'preview'
|
||||
| 'openOnWebsite'
|
||||
| 'startStopServer'
|
||||
|
||||
@@ -48,5 +48,6 @@ export enum CommandToCode {
|
||||
searchByType = 'search-by-type',
|
||||
processMediaData = 'process-media-data',
|
||||
isServerStarted = 'is-server-started',
|
||||
runFieldAction = 'run-field-action'
|
||||
runFieldAction = 'run-field-action',
|
||||
smartRename = 'smart-rename'
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Collapsible } from './Collapsible';
|
||||
import { CustomScript } from './CustomScript';
|
||||
import { Preview } from './Preview';
|
||||
import { SlugAction } from './SlugAction';
|
||||
import { SmartRenameAction } from './SmartRenameAction';
|
||||
import { StartServerButton } from './StartServerButton';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
@@ -52,6 +53,10 @@ const Actions: React.FunctionComponent<IActionsProps> = ({
|
||||
allActions.push(<SlugAction key="optimizeSlug" />);
|
||||
}
|
||||
|
||||
if (metadata?.title && !disableActions.includes(`smartRename`)) {
|
||||
allActions.push(<SmartRenameAction key="smartRename" />);
|
||||
}
|
||||
|
||||
if (settings?.preview?.host && !disableActions.includes(`preview`)) {
|
||||
if ((metadata && typeof metadata.slug !== "undefined") || !metadata) {
|
||||
allActions.push(<Preview key="preview" />);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
|
||||
export interface ISmartRenameActionProps { }
|
||||
|
||||
const SmartRenameAction: React.FunctionComponent<
|
||||
ISmartRenameActionProps
|
||||
> = () => {
|
||||
const smartRename = () => {
|
||||
Messenger.send(CommandToCode.smartRename);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionButton onClick={smartRename} title={l10n.t(LocalizationKey.panelSmartRenameActionTitle)}>
|
||||
{l10n.t(LocalizationKey.panelSmartRenameActionTitle)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
SmartRenameAction.displayName = 'SmartRenameAction';
|
||||
export { SmartRenameAction };
|
||||
Reference in New Issue
Block a user