mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-07 09:23:00 +02:00
#773 - Rename files
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#773](https://github.com/estruyf/vscode-front-matter/issues/773): Added the ability to rename content files
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
@@ -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
@@ -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 || '',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user