#773 - Rename files

This commit is contained in:
Elio Struyf
2024-03-14 16:38:17 +01:00
parent 31e344f358
commit c82c081fce
11 changed files with 164 additions and 42 deletions
+2
View File
@@ -8,6 +8,8 @@
### 🎨 Enhancements
- [#773](https://github.com/estruyf/vscode-front-matter/issues/773): Added the ability to rename content files
### ⚡️ Optimizations
### 🐞 Fixes
+5
View File
@@ -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",
+43 -20
View File
@@ -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<vscode.TextEdit[]> {
public static async setLastModifiedDateOnSave(document: TextDocument): Promise<TextEdit[]> {
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 || '',
+13
View File
@@ -0,0 +1,13 @@
import * as React from 'react';
export interface IRenameIconProps {
className: string;
}
export const RenameIcon: React.FunctionComponent<IRenameIconProps> = ({
className
}: React.PropsWithChildren<IRenameIconProps>) => {
return (
<svg className={className} fill="currentColor" aria-hidden="true" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 2a.5.5 0 0 0 0 1h1v14h-1a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-1V3h1a.5.5 0 0 0 0-1h-3Zm-4 2h4v1h-4C3.67 5 3 5.67 3 6.5v7c0 .83.67 1.5 1.5 1.5h4v1h-4A2.5 2.5 0 0 1 2 13.5v-7A2.5 2.5 0 0 1 4.5 4Zm11 11h-4v1h4a2.5 2.5 0 0 0 2.5-2.5v-7A2.5 2.5 0 0 0 15.5 4h-4v1h4c.83 0 1.5.67 1.5 1.5v7c0 .83-.67 1.5-1.5 1.5Z" fill="currentColor"></path></svg>
);
};
+1
View File
@@ -30,6 +30,7 @@ export enum DashboardMessage {
getPinnedItems = 'getPinnedItems',
pinItem = 'pinItem',
unpinItem = 'unpinItem',
rename = 'rename',
// Media Dashboard
getMedia = 'getMedia',
@@ -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<IContentActionsProps> = ({
setShowDeletionAlert(true);
};
const onRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, 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<IContentActionsProps> = ({
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onRename}>
<RenameIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonRename)}</span>
</DropdownMenuItem>
{
settings?.websiteUrl && (
<DropdownMenuItem onClick={openOnWebsite}>
@@ -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<IActionsBarProps> = ({
<span>{l10n.t(LocalizationKey.commonView)}</span>
</ActionsBarItem>
{
view === NavigationType.Contents && (
<ActionsBarItem
disabled={selectedFiles.length === 0 || selectedFiles.length > 1}
onClick={() => {
messageHandler.send(DashboardMessage.rename, selectedFiles[0]);
setSelectedFiles([]);
}}
>
<RenameIcon className="w-4 h-4 mr-2" aria-hidden="true" />
<span>{l10n.t(LocalizationKey.commonRename)}</span>
</ActionsBarItem>
)
}
{
view === NavigationType.Media && (
<>
+2 -21
View File
@@ -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,
+48 -1
View File
@@ -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
+3
View File
@@ -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;
}
}
+20
View File
@@ -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
*/