feat: add functionality to create new CSV files and enhance UI for file creation

This commit is contained in:
JayBridge 2026-02-07 14:12:34 +08:00
parent c7133cd538
commit 3c85dde0d2
5 changed files with 164 additions and 2 deletions

View file

@ -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 NonDestructively**: Autodetects 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.

View file

@ -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'

View file

@ -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: '至少需要保留一列'

View file

@ -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() {

View file

@ -0,0 +1,90 @@
import { App, Modal, Setting, Notice } from 'obsidian';
import { i18n } from '../i18n';
export interface CreateCsvModalOptions {
defaultFolder?: string;
onSubmit: (path: string) => Promise<void>;
}
export class CreateCsvModal extends Modal {
private defaultFolder: string;
private onSubmit: (path: string) => Promise<void>;
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');
}
}
}