mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-03 03:52:31 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
@@ -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}>
|
||||
|
||||
@@ -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" />);
|
||||
|
||||
25
src/panelWebView/components/SmartRenameAction.tsx
Normal file
25
src/panelWebView/components/SmartRenameAction.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user