From 3c85dde0d215bbfc8f02b3de4367bf01a47a9783 Mon Sep 17 00:00:00 2001 From: JayBridge <12310903@mail.sustech.edu.cn> Date: Sat, 7 Feb 2026 14:12:34 +0800 Subject: [PATCH] feat: add functionality to create new CSV files and enhance UI for file creation --- README.md | 1 + src/i18n/en.ts | 5 ++ src/i18n/zh-cn.ts | 7 ++- src/main.ts | 63 +++++++++++++++++++++++- src/utils/create-csv-modal.ts | 90 +++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/utils/create-csv-modal.ts diff --git a/README.md b/README.md index b089de2..5a3afd7 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ A plugin designed to view and edit `CSV files` directly within Obsidian. - **Toggle** between the table view and raw source-mode. - **Edit** cells directly by clicking and typing. - **Manage** rows and columns (add, delete, move) with a simple right-click on the header. +- **Create** new CSV files: use the command palette or right-click in the File Explorer to quickly create a new `.csv` file. A unique name like `new.csv` or `new-1.csv` will be generated automatically. - **Switch Delimiter Non‑Destructively**: Auto‑detects the file delimiter (comma, semicolon, tab, etc.). Changing the delimiter in the toolbar only re-parses the view; it does NOT rewrite your file. Your original delimiter is preserved when saving edits. - **Clickable URLs**: Plain-text URLs and Markdown-style links (`[text](url)`) in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the edit button (✎) to edit the cell content. diff --git a/src/i18n/en.ts b/src/i18n/en.ts index b28b551..4d71826 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -45,8 +45,13 @@ export const enUS = { deleteCol: 'Delete this column', moveColLeft: 'Move column left', moveColRight: 'Move column right', + createNewCsv: 'Create new CSV file' } , + commands: { + createNewCsv: 'Create new CSV file', + fileExists: 'File already exists' + }, tableMessages: { atLeastOneRow: 'At least one row must remain', atLeastOneColumn: 'At least one column must remain' diff --git a/src/i18n/zh-cn.ts b/src/i18n/zh-cn.ts index 110bc8b..81da342 100644 --- a/src/i18n/zh-cn.ts +++ b/src/i18n/zh-cn.ts @@ -10,7 +10,7 @@ export const zhCN = { sourceMode: '源码模式', tableMode: '表格模式', insertRowBefore: '上方插入行', - insertRowAfter: '下方插入行', + insertRowAfter: '下方插入行' }, editBar: { placeholder: '编辑选中单元格...' @@ -47,8 +47,13 @@ export const zhCN = { deleteCol: '删除本列', moveColLeft: '向左移动一列', moveColRight: '向右移动一列', + createNewCsv: '创建新 CSV 文件' } , + commands: { + createNewCsv: '创建新 CSV 文件', + fileExists: '文件已存在' + }, tableMessages: { atLeastOneRow: '至少需要保留一行', atLeastOneColumn: '至少需要保留一列' diff --git a/src/main.ts b/src/main.ts index 92c7e37..844cb5a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ -import { Plugin, WorkspaceLeaf, moment } from "obsidian"; +import { Plugin, WorkspaceLeaf, moment, TFile, Notice } from "obsidian"; import { CSVView, VIEW_TYPE_CSV } from "./view"; import { SourceView, VIEW_TYPE_CSV_SOURCE } from "./source-view"; import { i18n } from "./i18n"; +import { FileUtils } from "./utils/file-utils"; interface CSVPluginSettings { csvSettings: string; @@ -43,6 +44,66 @@ export default class CSVPlugin extends Plugin { // 将.csv文件扩展名与视图类型绑定 this.registerExtensions(["csv"], VIEW_TYPE_CSV); + + // Command: create new csv file (from command palette) — direct creation, no modal + this.addCommand({ + id: 'csv-lite-create-new-csv-file', + name: i18n.t('commands.createNewCsv'), + callback: () => { + this.createCsvInFolder(''); + } + }); + + // File explorer context menu: create csv file inside folder or next to file — direct creation + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file) => { + menu.addItem((item) => { + item.setTitle(i18n.t('contextMenu.createNewCsv') || i18n.t('commands.createNewCsv')) + .setIcon('file-plus') + .onClick(() => { + let defaultFolder = ''; + if ((file as any).path) { + const fp = (file as any).path as string; + // if path has a slash, take parent folder + const idx = fp.lastIndexOf('/'); + if (idx > 0) defaultFolder = fp.substring(0, idx); + } + this.createCsvInFolder(defaultFolder); + }); + }); + }) + ); + } + + // Create a new CSV in the given folder with an auto-incremented name if necessary + private async createCsvInFolder(folder: string) { + const baseName = 'new.csv'; + let name = baseName; + let idx = 0; + + while (this.app.vault.getAbstractFileByPath(folder ? `${folder}/${name}` : name)) { + idx++; + name = `new-${idx}.csv`; + if (idx > 1000) { + new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file'); + return null; + } + } + + const path = folder ? `${folder}/${name}` : name; + + try { + await FileUtils.withRetry(() => this.app.vault.create(path, '')); + const created = this.app.vault.getAbstractFileByPath(path) as TFile | null; + if (created) { + await this.app.workspace.getLeaf(true).openFile(created); + } + return created; + } catch (err) { + console.error('CreateCsv: failed to create file', err); + new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file'); + return null; + } } onunload() { diff --git a/src/utils/create-csv-modal.ts b/src/utils/create-csv-modal.ts new file mode 100644 index 0000000..d3f379d --- /dev/null +++ b/src/utils/create-csv-modal.ts @@ -0,0 +1,90 @@ +import { App, Modal, Setting, Notice } from 'obsidian'; +import { i18n } from '../i18n'; + +export interface CreateCsvModalOptions { + defaultFolder?: string; + onSubmit: (path: string) => Promise; +} + +export class CreateCsvModal extends Modal { + private defaultFolder: string; + private onSubmit: (path: string) => Promise; + private fileNameInput!: HTMLInputElement; + private folderInput!: HTMLInputElement; + + constructor(app: App, options: CreateCsvModalOptions) { + super(app); + this.defaultFolder = options.defaultFolder ?? ''; + this.onSubmit = options.onSubmit; + this.titleEl.setText(i18n.t('commands.createNewCsv')); + } + + onOpen() { + const { contentEl } = this; + + // filename setting + new Setting(contentEl) + .setName(i18n.t('modal.fileName') || 'File name') + .setDesc(i18n.t('modal.fileNameDesc') || 'File name (will have .csv appended if missing)') + .addText((text) => { + this.fileNameInput = text.inputEl; + text.setPlaceholder('new-file.csv'); + }); + + // folder setting + new Setting(contentEl) + .setName(i18n.t('modal.folder') || 'Folder') + .setDesc(i18n.t('modal.folderDesc') || 'Relative folder inside vault (leave empty for root)') + .addText((text) => { + this.folderInput = text.inputEl; + this.folderInput.value = this.defaultFolder ?? ''; + text.setPlaceholder('Folder/Subfolder'); + }); + + // buttons + const btnContainer = contentEl.createDiv({ cls: 'mod-cta-container' }); + const submitBtn = btnContainer.createEl('button', { text: i18n.t('buttons.create') || 'Create' }); + submitBtn.addEventListener('click', async (e) => { + e.preventDefault(); + await this.handleSubmit(); + }); + + const cancelBtn = btnContainer.createEl('button', { text: i18n.t('buttons.cancel') || 'Cancel' }); + cancelBtn.addEventListener('click', () => this.close()); + } + + async handleSubmit() { + const rawName = this.fileNameInput?.value.trim() ?? ''; + let folder = this.folderInput?.value.trim() ?? ''; + + if (!rawName) { + new Notice(i18n.t('modal.errors.missingFileName') || 'Please provide a file name'); + return; + } + + let name = rawName; + if (!name.toLowerCase().endsWith('.csv')) { + name = `${name}.csv`; + } + + // normalize folder path: remove leading/trailing slashes + folder = folder.replace(/^\/+|\/+$/g, ''); + + const path = folder ? `${folder}/${name}` : name; + + // prevent overwriting existing files + const existing = this.app.vault.getAbstractFileByPath(path); + if (existing) { + new Notice(i18n.t('commands.fileExists') || 'File already exists'); + return; + } + + try { + await this.onSubmit(path); + this.close(); + } catch (err) { + console.error('CreateCsvModal: failed to create file', err); + new Notice(i18n.t('modal.errors.createFailed') || 'Failed to create file'); + } + } +}