Merge pull request #1018 from estruyf/estruyf/smart-file-rename

feat: add smart rename to sync filename with front matter
This commit is contained in:
Elio Struyf
2026-03-30 14:12:00 +02:00
committed by GitHub
13 changed files with 228 additions and 4 deletions

View File

@@ -7,6 +7,7 @@
### 🎨 Enhancements
- [#545](https://github.com/estruyf/vscode-front-matter/issues/545): Add smart rename action to sync filename with front matter data
- [#937](https://github.com/estruyf/vscode-front-matter/issues/937): Dashboard "Structure" view for documentation sites *WIP*
- [#965](https://github.com/estruyf/vscode-front-matter/issues/965): Added SEO support for the keyword in the first paragraph
- [#973](https://github.com/estruyf/vscode-front-matter/issues/973): Support for number fields in the snippets

View File

@@ -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",

View File

@@ -2856,7 +2856,7 @@
]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
"dev": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
"vscode:prepublish": "npm run clean && npm run localization:generate && npm-run-all --parallel prod:*",
"build:ext": "npm run clean && npm-run-all --parallel dev:build:*",
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",

View File

@@ -32,6 +32,7 @@ export enum DashboardMessage {
pinItem = 'pinItem',
unpinItem = 'unpinItem',
rename = 'rename',
smartRename = 'smartRename',
moveFile = 'moveFile',
// Media Dashboard

View File

@@ -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';
@@ -73,7 +74,12 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
const onRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
messageHandler.send(DashboardMessage.rename, path);
}, [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();
@@ -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}>

View File

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

View File

@@ -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;
}
}

View File

@@ -25,6 +25,9 @@ export class ArticleListener extends BaseListener {
case CommandToCode.publish:
Article.toggleDraft();
break;
case CommandToCode.smartRename:
ArticleHelper.smartRename();
break;
}
}

View File

@@ -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
*/

View File

@@ -38,6 +38,7 @@ export type PanelAction =
| 'openDashboard'
| 'createContent'
| 'optimizeSlug'
| 'smartRename'
| 'preview'
| 'openOnWebsite'
| 'startStopServer'

View File

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

View File

@@ -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" />);

View File

@@ -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 };