Merge pull request #44 from LIUBINfighter/dev

Some fixes (Fixes #41, Fixes #35)
This commit is contained in:
Jay Bridge 2025-12-07 05:14:08 +08:00 committed by GitHub
commit c73e596e08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 149 additions and 25 deletions

42
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: CI
on:
push:
branches: [ "dev", "master", "main" ]
pull_request:
branches: [ "dev", "master", "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Prettier check (if present)
run: |
if npx --no-install prettier --version >/dev/null 2>&1; then
npx prettier --check "**/*.{ts,js,json,md,css}" || (echo "Prettier check failed" && exit 1)
else
echo "Prettier not installed - skipping check"
fi
- name: TypeScript typecheck and build
run: npm run build
- name: Run tests
run: npm test

View file

@ -1,7 +1,7 @@
{
"id": "csv-lite",
"name": "CSV Lite",
"version": "1.1.4",
"version": "1.1.5",
"minAppVersion": "1.8.0",
"description": "Just open and edit CSV files directly, no more. Keep it simple.",
"author": "Jay Bridge",

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "obsidian-csv",
"version": "1.1.4",
"version": "1.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-csv",
"version": "1.1.4",
"version": "1.1.5",
"description": "CSV viewer and editor for Obsidian",
"main": "main.js",
"scripts": {

View file

@ -15,7 +15,8 @@ export const enUS = {
},
csv: {
error: 'Error',
parsingFailed: 'Failed to parse CSV. Please check file format.'
parsingFailed: 'Failed to parse CSV. Please check file format.',
parseWarning: 'CSV parse warning:'
},
settings: {
fieldSeparator: 'Field Separator',
@ -45,4 +46,16 @@ export const enUS = {
moveColLeft: 'Move column left',
moveColRight: 'Move column right',
}
,
tableMessages: {
atLeastOneRow: 'At least one row must remain',
atLeastOneColumn: 'At least one column must remain'
}
,
notifications: {
undo: 'Undid last action',
noMoreUndo: 'There is nothing more to undo',
redo: 'Redid action',
noMoreRedo: 'There is nothing more to redo'
}
};

View file

@ -17,7 +17,8 @@ export const zhCN = {
},
csv: {
error: '错误',
parsingFailed: 'CSV解析失败请检查文件格式'
parsingFailed: 'CSV解析失败请检查文件格式',
parseWarning: 'CSV解析提示:'
},
settings: {
fieldSeparator: '字段分隔符',
@ -47,4 +48,16 @@ export const zhCN = {
moveColLeft: '向左移动一列',
moveColRight: '向右移动一列',
}
,
tableMessages: {
atLeastOneRow: '至少需要保留一行',
atLeastOneColumn: '至少需要保留一列'
}
,
notifications: {
undo: '已撤销上一步操作',
noMoreUndo: '没有更多可撤销的操作',
redo: '已重做操作',
noMoreRedo: '没有更多可重做的操作'
}
};

View file

@ -3,7 +3,7 @@ import { EditorState, Extension, RangeSetBuilder } from "@codemirror/state";
import { EditorView, keymap, placeholder, lineNumbers, drawSelection, Decoration, ViewPlugin, ViewUpdate, DecorationSet } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
export const VIEW_TYPE_CSV_SOURCE = "csv-source-view";
export const VIEW_TYPE_CSV_SOURCE = "csv-lite-source-view";
// 分隔符高亮插件(逗号、分号、制表符)
const separatorHighlightPlugin = ViewPlugin.fromClass(class {
@ -62,7 +62,7 @@ export class SourceView extends TextFileView {
// 1. 在 view header 的 view-actions 区域插入切换按钮lucide/table 图标)
// 交互说明:
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-view
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-lite-view
// - 若有,则激活该 leafworkspace.setActiveLeaf
// - 若无,则新建 leaf 并打开目标视图。
// - 不主动关闭原有视图,用户可自行关闭。
@ -75,7 +75,7 @@ export class SourceView extends TextFileView {
btn.onclick = async () => {
const file = this.file;
if (!file) return;
const leaves = this.app.workspace.getLeavesOfType('csv-view');
const leaves = this.app.workspace.getLeavesOfType('csv-lite-view');
let found = false;
for (const leaf of leaves) {
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
@ -88,7 +88,7 @@ export class SourceView extends TextFileView {
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file, { active: true });
await newLeaf.setViewState({
type: 'csv-view',
type: 'csv-lite-view',
active: true,
state: { file: file.path }
});

View file

@ -114,14 +114,14 @@ export class CSVUtils {
const parseResult: any = Papa.parse(csvString, parseConfig as any);
if (parseResult.errors && parseResult.errors.length > 0) {
console.warn("CSV解析警告:", parseResult.errors);
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
console.warn("CSV parse warnings:", parseResult.errors);
new Notice(`${i18n.t("csv.parseWarning")} ${parseResult.errors[0].message}`);
}
return parseResult.data as string[][];
} catch (error) {
console.error("CSV解析错误:", error);
new Notice(`${i18n.t("csv.error")}: CSV解析失败请检查文件格式`);
console.error("CSV parse error:", error);
new Notice(i18n.t("csv.parsingFailed"));
return [[""]];
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from "obsidian";
import { i18n } from "../i18n";
export class HistoryManager<T> {
private history: T[] = [];
@ -38,10 +39,10 @@ export class HistoryManager<T> {
undo(): T | null {
if (this.canUndo()) {
this.currentIndex--;
new Notice("已撤销上一步操作");
new Notice(i18n.t("notifications.undo"));
return this.getCurrentState();
} else {
new Notice("没有更多可撤销的操作");
new Notice(i18n.t("notifications.noMoreUndo"));
return null;
}
}
@ -52,10 +53,10 @@ export class HistoryManager<T> {
redo(): T | null {
if (this.canRedo()) {
this.currentIndex++;
new Notice("已重做操作");
new Notice(i18n.t("notifications.redo"));
return this.getCurrentState();
} else {
new Notice("没有更多可重做的操作");
new Notice(i18n.t("notifications.noMoreRedo"));
return null;
}
}

View file

@ -1,4 +1,5 @@
import { Notice } from "obsidian";
import { i18n } from "../i18n";
export class TableUtils {
/**
@ -42,7 +43,7 @@ export class TableUtils {
*/
static deleteRow(tableData: string[][]): string[][] {
if (tableData.length <= 1) {
new Notice("至少需要保留一行");
new Notice(i18n.t("tableMessages.atLeastOneRow"));
return tableData;
}
@ -61,7 +62,7 @@ export class TableUtils {
*/
static deleteColumn(tableData: string[][]): string[][] {
if (!tableData[0] || tableData[0].length <= 1) {
new Notice("至少需要保留一列");
new Notice(i18n.t("tableMessages.atLeastOneColumn"));
return tableData;
}

View file

@ -19,7 +19,7 @@ import { renderTable } from "./view/table-render";
import { HighlightManager } from "./utils/highlight-manager";
import { setupHeaderContextMenu } from "./view/header-context-menu";
export const VIEW_TYPE_CSV = "csv-view";
export const VIEW_TYPE_CSV = "csv-lite-view";
export class CSVView extends TextFileView {
public file: TFile | null;
@ -559,7 +559,7 @@ export class CSVView extends TextFileView {
// 1. 在 view header 的 view-actions 区域插入切换按钮lucide/file-code 图标)
// 交互说明:
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-source-view
// - 点击时遍历所有 leaf查找同一文件的目标视图csv-lite-source-view
// - 若有,则激活该 leafworkspace.setActiveLeaf
// - 若无,则新建 leaf 并打开目标视图。
// - 不主动关闭原有视图,用户可自行关闭。
@ -572,7 +572,7 @@ export class CSVView extends TextFileView {
btn.onclick = async () => {
const file = this.file;
if (!file) return;
const leaves = this.app.workspace.getLeavesOfType('csv-source-view');
const leaves = this.app.workspace.getLeavesOfType('csv-lite-source-view');
let found = false;
for (const leaf of leaves) {
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
@ -585,7 +585,7 @@ export class CSVView extends TextFileView {
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file, { active: true, state: { mode: "source" } });
await newLeaf.setViewState({
type: "csv-source-view",
type: "csv-lite-source-view",
active: true,
state: { file: file.path }
});
@ -793,6 +793,9 @@ export class CSVView extends TextFileView {
document,
"keydown",
(event: KeyboardEvent) => {
// Only handle undo/redo when this view is the active leaf
if (this.app.workspace.activeLeaf !== this.leaf) return;
// 检测Ctrl+Z (或Mac上的Cmd+Z)
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
if (event.shiftKey) {
@ -983,7 +986,7 @@ export class CSVView extends TextFileView {
const leaf = this.app.workspace.getLeaf(true);
await leaf.openFile(file, { active: true, state: { mode: "source" } });
await leaf.setViewState({
type: "csv-source-view",
type: "csv-lite-source-view",
active: true,
state: { file: file.path }
});

View file

@ -0,0 +1,50 @@
import { TableHistoryManager } from "../src/utils/history-manager";
import { Notice } from "obsidian";
import { i18n } from "../src/i18n";
beforeEach(() => {
// reset Notice mock
(Notice as any).mockClear && (Notice as any).mockClear();
});
test('undo on empty history shows noMoreUndo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
// Initially only one state, cannot undo
const res = hm.undo();
expect(res).toBeNull();
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreUndo'));
});
test('undo when available shows undo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
hm.push([["b"]]);
// Now we can undo
const res = hm.undo();
expect(res).toEqual([["a"]]);
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.undo'));
});
test('redo on no-op shows noMoreRedo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
// Nothing to redo
const res = hm.redo();
expect(res).toBeNull();
expect(Notice).toHaveBeenCalledTimes(1);
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreRedo'));
});
test('redo when available shows redo message', () => {
const hm = new TableHistoryManager([["a"]], 10);
hm.push([["b"]]);
// undo back to first
const u = hm.undo();
expect(u).toEqual([["a"]]);
// redo
const r = hm.redo();
expect(r).toEqual([["b"]]);
// Two notices were called (undo, redo)
expect(Notice).toHaveBeenCalledTimes(2);
expect((Notice as any).mock.calls[1][0]).toBe(i18n.t('notifications.redo'));
});

View file

@ -1,3 +1,4 @@
{
"1.0.0": "0.15.0"
"1.0.0": "0.15.0",
"1.1.5": "1.8.0"
}