mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-12 03:43:01 +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
|
### 🎨 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*
|
- [#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
|
- [#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
|
- [#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.slugAction.title": "Optimize slug",
|
||||||
|
|
||||||
|
"panel.smartRenameAction.title": "Smart rename",
|
||||||
|
|
||||||
"panel.spinner.loading": "Loading...",
|
"panel.spinner.loading": "Loading...",
|
||||||
|
|
||||||
"panel.startServerbutton.start": "Start server",
|
"panel.startServerbutton.start": "Start server",
|
||||||
@@ -550,6 +552,13 @@
|
|||||||
"commands.article.rename.fileName.title": "Rename: {0}",
|
"commands.article.rename.fileName.title": "Rename: {0}",
|
||||||
"commands.article.rename.fileName.prompt": "File name",
|
"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.cache.cleared": "Cache cleared",
|
||||||
|
|
||||||
"commands.chatbot.title": "Ask me anything",
|
"commands.chatbot.title": "Ask me anything",
|
||||||
|
|||||||
+1
-1
@@ -2856,7 +2856,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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:*",
|
"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:*",
|
"build:ext": "npm run clean && npm-run-all --parallel dev:build:*",
|
||||||
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",
|
"watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export enum DashboardMessage {
|
|||||||
pinItem = 'pinItem',
|
pinItem = 'pinItem',
|
||||||
unpinItem = 'unpinItem',
|
unpinItem = 'unpinItem',
|
||||||
rename = 'rename',
|
rename = 'rename',
|
||||||
|
smartRename = 'smartRename',
|
||||||
moveFile = 'moveFile',
|
moveFile = 'moveFile',
|
||||||
|
|
||||||
// Media Dashboard
|
// Media Dashboard
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
LanguageIcon,
|
LanguageIcon,
|
||||||
EllipsisHorizontalIcon,
|
EllipsisHorizontalIcon,
|
||||||
ArrowRightCircleIcon
|
ArrowRightCircleIcon,
|
||||||
|
ArrowPathIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CustomScript, I18nConfig } from '../../../models';
|
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>) => {
|
const onRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
messageHandler.send(DashboardMessage.rename, path);
|
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>) => {
|
const onOpenWebsite = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -144,6 +150,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
|||||||
<span>{l10n.t(LocalizationKey.commonRename)}</span>
|
<span>{l10n.t(LocalizationKey.commonRename)}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={onSmartRename}>
|
||||||
|
<ArrowPathIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||||
|
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemSmartRename)}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{
|
{
|
||||||
settings?.websiteUrl && (
|
settings?.websiteUrl && (
|
||||||
<DropdownMenuItem onClick={onOpenWebsite}>
|
<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.
|
* Generate the update to be applied to the article.
|
||||||
* @param article
|
* @param article
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export class PagesListener extends BaseListener {
|
|||||||
case DashboardMessage.moveFile:
|
case DashboardMessage.moveFile:
|
||||||
await this.moveFile(msg.payload);
|
await this.moveFile(msg.payload);
|
||||||
break;
|
break;
|
||||||
|
case DashboardMessage.smartRename:
|
||||||
|
ArticleHelper.smartRename(msg.payload);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export class ArticleListener extends BaseListener {
|
|||||||
case CommandToCode.publish:
|
case CommandToCode.publish:
|
||||||
Article.toggleDraft();
|
Article.toggleDraft();
|
||||||
break;
|
break;
|
||||||
|
case CommandToCode.smartRename:
|
||||||
|
ArticleHelper.smartRename();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1696,6 +1696,10 @@ export enum LocalizationKey {
|
|||||||
* Optimize slug
|
* Optimize slug
|
||||||
*/
|
*/
|
||||||
panelSlugActionTitle = 'panel.slugAction.title',
|
panelSlugActionTitle = 'panel.slugAction.title',
|
||||||
|
/**
|
||||||
|
* Smart rename
|
||||||
|
*/
|
||||||
|
panelSmartRenameActionTitle = 'panel.smartRenameAction.title',
|
||||||
/**
|
/**
|
||||||
* Loading...
|
* Loading...
|
||||||
*/
|
*/
|
||||||
@@ -1772,6 +1776,26 @@ export enum LocalizationKey {
|
|||||||
* File name
|
* File name
|
||||||
*/
|
*/
|
||||||
commandsArticleRenameFileNamePrompt = 'commands.article.rename.fileName.prompt',
|
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
|
* Cache cleared
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type PanelAction =
|
|||||||
| 'openDashboard'
|
| 'openDashboard'
|
||||||
| 'createContent'
|
| 'createContent'
|
||||||
| 'optimizeSlug'
|
| 'optimizeSlug'
|
||||||
|
| 'smartRename'
|
||||||
| 'preview'
|
| 'preview'
|
||||||
| 'openOnWebsite'
|
| 'openOnWebsite'
|
||||||
| 'startStopServer'
|
| 'startStopServer'
|
||||||
|
|||||||
@@ -48,5 +48,6 @@ export enum CommandToCode {
|
|||||||
searchByType = 'search-by-type',
|
searchByType = 'search-by-type',
|
||||||
processMediaData = 'process-media-data',
|
processMediaData = 'process-media-data',
|
||||||
isServerStarted = 'is-server-started',
|
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 { CustomScript } from './CustomScript';
|
||||||
import { Preview } from './Preview';
|
import { Preview } from './Preview';
|
||||||
import { SlugAction } from './SlugAction';
|
import { SlugAction } from './SlugAction';
|
||||||
|
import { SmartRenameAction } from './SmartRenameAction';
|
||||||
import { StartServerButton } from './StartServerButton';
|
import { StartServerButton } from './StartServerButton';
|
||||||
import * as l10n from '@vscode/l10n';
|
import * as l10n from '@vscode/l10n';
|
||||||
import { LocalizationKey } from '../../localization';
|
import { LocalizationKey } from '../../localization';
|
||||||
@@ -52,6 +53,10 @@ const Actions: React.FunctionComponent<IActionsProps> = ({
|
|||||||
allActions.push(<SlugAction key="optimizeSlug" />);
|
allActions.push(<SlugAction key="optimizeSlug" />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata?.title && !disableActions.includes(`smartRename`)) {
|
||||||
|
allActions.push(<SmartRenameAction key="smartRename" />);
|
||||||
|
}
|
||||||
|
|
||||||
if (settings?.preview?.host && !disableActions.includes(`preview`)) {
|
if (settings?.preview?.host && !disableActions.includes(`preview`)) {
|
||||||
if ((metadata && typeof metadata.slug !== "undefined") || !metadata) {
|
if ((metadata && typeof metadata.slug !== "undefined") || !metadata) {
|
||||||
allActions.push(<Preview key="preview" />);
|
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