Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const zhCN = {
sourceMode: '源码模式',
tableMode: '表格模式',
insertRowBefore: '上方插入行',
insertRowAfter: '下方插入行',
insertRowAfter: '下方插入行'
},
editBar: {
placeholder: '编辑选中单元格...'
Expand Down Expand Up @@ -47,8 +47,13 @@ export const zhCN = {
deleteCol: '删除本列',
moveColLeft: '向左移动一列',
moveColRight: '向右移动一列',
createNewCsv: '创建新 CSV 文件'
}
,
commands: {
createNewCsv: '创建新 CSV 文件',
fileExists: '文件已存在'
},
tableMessages: {
atLeastOneRow: '至少需要保留一行',
atLeastOneColumn: '至少需要保留一列'
Expand Down
63 changes: 62 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
90 changes: 90 additions & 0 deletions src/utils/create-csv-modal.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}