diff --git a/CHANGELOG.md b/CHANGELOG.md index 709aabf7..f1b0fcb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🎨 Enhancements +- [#773](https://github.com/estruyf/vscode-front-matter/issues/773): Added the ability to rename content files + ### ⚡️ Optimizations ### 🐞 Fixes diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 4e108527..800620e1 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -41,6 +41,7 @@ "common.translate": "Translate", "common.languages": "Languages", "common.scripts": "Scripts", + "common.rename": "Rename", "loading.initPages": "Loading content", @@ -525,6 +526,10 @@ "commands.article.setDate.error": "Something failed while parsing the date format. Check your \"{0}\" setting.", "commands.article.updateSlug.error": "Failed to rename file: {0}", + "commands.article.rename.fileNotExists.error": "The file did not exist", + "commands.article.rename.fileExists.error": "A file with the name \"{0}\" already exists", + "commands.article.rename.fileName.title": "Rename: {0}", + "commands.article.rename.fileName.prompt": "File name", "commands.cache.cleared": "Cache cleared", diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 667b7165..5b7b0028 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -1,3 +1,13 @@ +import { + Position, + TextDocument, + TextDocumentWillSaveEvent, + TextEdit, + Uri, + commands, + window, + workspace +} from 'vscode'; import { Folders } from './Folders'; import { DEFAULT_CONTENT_TYPE } from './../constants/ContentType'; import { isValidFile } from './../helpers/isValidFile'; @@ -13,7 +23,6 @@ import { TelemetryEvent, SETTING_SLUG_TEMPLATE } from './../constants'; -import * as vscode from 'vscode'; import { CustomPlaceholder, Field } from '../models'; import { format } from 'date-fns'; import { @@ -33,17 +42,35 @@ import { Telemetry } from '../helpers/Telemetry'; import { ParsedFrontMatter } from '../parsers'; import { MediaListener } from '../listeners/panel'; import { NavigationType } from '../dashboardWebView/models'; -import { Position } from 'vscode'; import { SNIPPET } from '../constants/Snippet'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; export class Article { + /** + * Registers the commands for the Article class. + * + * @param subscriptions - The array of subscriptions to register the commands with. + */ + public static async registerCommands(subscriptions: unknown[]) { + subscriptions.push( + commands.registerCommand(COMMAND_NAME.setLastModifiedDate, Article.setLastModifiedDate) + ); + + subscriptions.push(commands.registerCommand(COMMAND_NAME.generateSlug, Article.updateSlug)); + + // Inserting an image in Markdown + subscriptions.push(commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia)); + + // Inserting a snippet in Markdown + subscriptions.push(commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet)); + } + /** * Sets the article date */ public static async setDate() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -77,7 +104,7 @@ export class Article { * Sets the article lastmod date */ public static async setLastModifiedDate() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -91,9 +118,7 @@ export class Article { ArticleHelper.update(editor, updatedArticle as ParsedFrontMatter); } - public static async setLastModifiedDateOnSave( - document: vscode.TextDocument - ): Promise { + public static async setLastModifiedDateOnSave(document: TextDocument): Promise { const updatedArticle = this.setLastModifiedDateInner(document); if (typeof updatedArticle === 'undefined') { @@ -105,9 +130,7 @@ export class Article { return [update]; } - private static setLastModifiedDateInner( - document: vscode.TextDocument - ): ParsedFrontMatter | undefined { + private static setLastModifiedDateInner(document: TextDocument): ParsedFrontMatter | undefined { const article = ArticleHelper.getFrontMatterFromDocument(document); // Only set the date, if there is already front matter set @@ -160,7 +183,7 @@ export class Article { Telemetry.send(TelemetryEvent.generateSlug); const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string; - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; @@ -219,7 +242,7 @@ export class Article { // Check if the file name should be updated by the slug // This is required for systems like Jekyll if (updateFileName) { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (editor) { const ext = extname(editor.document.fileName); const fileName = basename(editor.document.fileName); @@ -237,7 +260,7 @@ export class Article { try { await editor.document.save(); - await vscode.workspace.fs.rename(editor.document.uri, vscode.Uri.file(newPath), { + await workspace.fs.rename(editor.document.uri, Uri.file(newPath), { overwrite: false }); } catch (e: unknown) { @@ -257,7 +280,7 @@ export class Article { * Retrieve the slug from the front matter */ public static getSlug() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -297,7 +320,7 @@ export class Article { * Toggle the page its draft mode */ public static async toggleDraft() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -315,7 +338,7 @@ export class Article { * Article auto updater * @param event */ - public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) { + public static async autoUpdate(event: TextDocumentWillSaveEvent) { const document = event.document; if (document && ArticleHelper.isSupportedFile(document)) { const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE); @@ -355,7 +378,7 @@ export class Article { * Insert an image from the media dashboard into the article */ public static async insertMedia() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -367,7 +390,7 @@ export class Article { const position = editor.selection.active; const selectionText = editor.document.getText(editor.selection); - await vscode.commands.executeCommand(COMMAND_NAME.dashboard, { + await commands.executeCommand(COMMAND_NAME.dashboard, { type: 'media', data: { pageBundle: !!contentType.pageBundle, @@ -386,7 +409,7 @@ export class Article { * Insert a snippet into the article */ public static async insertSnippet() { - const editor = vscode.window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { return; } @@ -442,7 +465,7 @@ export class Article { const article = ArticleHelper.getFrontMatter(editor); const contentType = article ? ArticleHelper.getContentType(article) : undefined; - await vscode.commands.executeCommand(COMMAND_NAME.dashboard, { + await commands.executeCommand(COMMAND_NAME.dashboard, { type: NavigationType.Snippets, data: { fileTitle: article?.data.title || '', diff --git a/src/components/icons/RenameIcon.tsx b/src/components/icons/RenameIcon.tsx new file mode 100644 index 00000000..d59dcc66 --- /dev/null +++ b/src/components/icons/RenameIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export interface IRenameIconProps { + className: string; +} + +export const RenameIcon: React.FunctionComponent = ({ + className +}: React.PropsWithChildren) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 7d658e9f..86859b5b 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -30,6 +30,7 @@ export enum DashboardMessage { getPinnedItems = 'getPinnedItems', pinItem = 'pinItem', unpinItem = 'unpinItem', + rename = 'rename', // Media Dashboard getMedia = 'getMedia', diff --git a/src/dashboardWebView/components/Contents/ContentActions.tsx b/src/dashboardWebView/components/Contents/ContentActions.tsx index 85bfdc06..b96825e8 100644 --- a/src/dashboardWebView/components/Contents/ContentActions.tsx +++ b/src/dashboardWebView/components/Contents/ContentActions.tsx @@ -13,6 +13,7 @@ import { COMMAND_NAME, GeneralCommands } from '../../../constants'; import { PinIcon } from '../Icons/PinIcon'; import { PinnedItemsAtom } from '../../state/atom/PinnedItems'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown'; +import { RenameIcon } from '../../../components/icons/RenameIcon'; export interface IContentActionsProps { title: string; @@ -56,6 +57,11 @@ export const ContentActions: React.FunctionComponent = ({ setShowDeletionAlert(true); }; + const onRename = React.useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + messageHandler.send(DashboardMessage.rename, path); + }, [path]) + const onDeleteConfirm = () => { if (path) { Messenger.send(DashboardMessage.deleteFile, path); @@ -220,6 +226,11 @@ export const ContentActions: React.FunctionComponent = ({ {l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)} + + + {l10n.t(LocalizationKey.commonRename)} + + { settings?.websiteUrl && ( diff --git a/src/dashboardWebView/components/Header/ActionsBar.tsx b/src/dashboardWebView/components/Header/ActionsBar.tsx index aca52030..d5b64b4f 100644 --- a/src/dashboardWebView/components/Header/ActionsBar.tsx +++ b/src/dashboardWebView/components/Header/ActionsBar.tsx @@ -13,6 +13,7 @@ import { CustomScript, ScriptType } from '../../../models'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown'; import { useFilesContext } from '../../providers/FilesProvider'; import { COMMAND_NAME, GeneralCommands } from '../../../constants'; +import { RenameIcon } from '../../../components/icons/RenameIcon'; export interface IActionsBarProps { view: NavigationType; @@ -195,6 +196,21 @@ export const ActionsBar: React.FunctionComponent = ({ {l10n.t(LocalizationKey.commonView)} + { + view === NavigationType.Contents && ( + 1} + onClick={() => { + messageHandler.send(DashboardMessage.rename, selectedFiles[0]); + setSelectedFiles([]); + }} + > + + ) + } + { view === NavigationType.Media && ( <> diff --git a/src/extension.ts b/src/extension.ts index 44fbfa71..d47a78b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -140,15 +140,8 @@ export async function activate(context: vscode.ExtensionContext) { const remap = vscode.commands.registerCommand(COMMAND_NAME.remap, Settings.remap); - const setLastModifiedDate = vscode.commands.registerCommand( - COMMAND_NAME.setLastModifiedDate, - Article.setLastModifiedDate - ); - - const generateSlug = vscode.commands.registerCommand( - COMMAND_NAME.generateSlug, - Article.updateSlug - ); + // Register all the article commands + Article.registerCommands(subscriptions); subscriptions.push( vscode.commands.registerCommand(COMMAND_NAME.initTemplate, () => @@ -291,16 +284,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand(COMMAND_NAME.chatbot, () => Chatbot.open(extensionPath)) ); - // Inserting an image in Markdown - subscriptions.push( - vscode.commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia) - ); - - // Inserting a snippet in Markdown - subscriptions.push( - vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet) - ); - // Create the editor experience for bulk scripts subscriptions.push( vscode.workspace.registerTextDocumentContentProvider( @@ -340,8 +323,6 @@ export async function activate(context: vscode.ExtensionContext) { createCategory, exportTaxonomy, remap, - setLastModifiedDate, - generateSlug, createFromTemplate, createTemplate, registerFolder, diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index ead20979..9ec64241 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -36,7 +36,7 @@ import { import { format, parse } from 'date-fns'; import { Notifications } from './Notifications'; import { Article } from '../commands'; -import { join, parse as parseFile } from 'path'; +import { dirname, join, parse as parseFile } from 'path'; import { EditorHelper } from '@estruyf/vscode'; import sanitize from '../helpers/Sanitize'; import { Field, ContentType as IContentType } from '../models'; @@ -183,6 +183,53 @@ export class ArticleHelper { } } + /** + * Renames a file. + * @param filePath - The path of the file to be renamed. + */ + public static async rename(filePath: string) { + filePath = parseWinPath(filePath); + const fileUri = Uri.file(filePath); + const file = workspace.openTextDocument(fileUri); + if (!file) { + Notifications.error(l10n.t(LocalizationKey.commandsArticleRenameFileNotExistsError)); + return; + } + + const folderPath = dirname(fileUri.fsPath); + const fileName = parseFile(filePath).base; + const fileNameWithoutExt = parseFile(filePath).name; + const fileExtension = parseFile(filePath).ext; + const newFileName = await window.showInputBox({ + title: l10n.t(LocalizationKey.commandsArticleRenameFileNameTitle, fileName), + prompt: l10n.t(LocalizationKey.commandsArticleRenameFileNamePrompt), + value: fileNameWithoutExt, + ignoreFocusOut: true, + validateInput: async (value) => { + try { + const newFileUri = Uri.joinPath(Uri.file(folderPath), `${value}${fileExtension}`); + console.log(newFileUri.fsPath); + const exists = await workspace.fs.readFile(newFileUri); + if (exists && value !== fileNameWithoutExt) { + return l10n.t(LocalizationKey.commandsArticleRenameFileExistsError, value); + } + } catch (e) { + // File does not exist + } + return undefined; + } + }); + + if (!newFileName) { + return; + } + + const newFileUri = Uri.joinPath(Uri.file(folderPath), `${newFileName}${fileExtension}`); + await workspace.fs.rename(fileUri, newFileUri, { + overwrite: true + }); + } + /** * Generate the update to be applied to the article. * @param article diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index 8133f093..685b928d 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -54,6 +54,9 @@ export class PagesListener extends BaseListener { case DashboardMessage.deleteFile: this.deletePage(msg.payload); break; + case DashboardMessage.rename: + ArticleHelper.rename(msg.payload); + break; } } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index d20ff6aa..defeba97 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -167,6 +167,10 @@ export enum LocalizationKey { * Scripts */ commonScripts = 'common.scripts', + /** + * Rename + */ + commonRename = 'common.rename', /** * Loading content */ @@ -1680,6 +1684,22 @@ export enum LocalizationKey { * Failed to rename file: {0} */ commandsArticleUpdateSlugError = 'commands.article.updateSlug.error', + /** + * The file did not exist + */ + commandsArticleRenameFileNotExistsError = 'commands.article.rename.fileNotExists.error', + /** + * A file with the name "{0}" already exists + */ + commandsArticleRenameFileExistsError = 'commands.article.rename.fileExists.error', + /** + * Rename: {0} + */ + commandsArticleRenameFileNameTitle = 'commands.article.rename.fileName.title', + /** + * File name + */ + commandsArticleRenameFileNamePrompt = 'commands.article.rename.fileName.prompt', /** * Cache cleared */