fix: guard regeneration and scroll sync

Change-Id: I08e2c55d5e88a51bad9a007e57962ebe48dc7c64
This commit is contained in:
wujunchen 2026-04-25 19:52:27 +08:00
parent 5c2c97fdbe
commit 206a50f8d0
6 changed files with 59 additions and 22 deletions

38
main.js

File diff suppressed because one or more lines are too long

20
main.ts
View file

@ -5,7 +5,7 @@ import os from 'os';
import path from 'path';
import fs from 'fs';
import { findLineForAnchor } from './src/anchor';
import { serializeCacheFile, touchCacheEntry } from './src/cache';
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
import { translate } from './src/i18n';
import { cardToMarkdown, cardToPlain, cardsToMarkdown } from './src/markdown';
@ -17,7 +17,7 @@ import {
normalizeCardsPayload,
parseCardsJson,
} from './src/schema';
import { createRafThrottledHandler } from './src/scroll';
import { createRafThrottledHandler, visibleTopProbeY } from './src/scroll';
import {
buildAnthropicMessagesBody,
buildGeminiBody,
@ -1065,6 +1065,14 @@ class ParallelReaderPlugin extends Plugin {
else new Notice(this.t('noCancelableJob'));
}
confirmRegenerateEditedCards() {
const message = this.t('confirmRegenerateEditedCards');
if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
return window.confirm(message);
}
return true;
}
addFileMenuItems(menu, file) {
if (!(file instanceof TFile) || !file.path.endsWith('.md')) return;
menu.addSeparator();
@ -1176,6 +1184,10 @@ class ParallelReaderPlugin extends Plugin {
new Notice(this.t('alreadyGenerating'));
return;
}
if (shouldConfirmRegenerate(this.cacheGet(file.path), force) && !this.confirmRegenerateEditedCards()) {
new Notice(this.t('regenerateCancelled'));
return false;
}
let view = null;
return this.jobs.start(runningKey, async job => {
@ -1337,7 +1349,7 @@ class ParallelReaderPlugin extends Plugin {
// Find visible top line
const rect = scrollDom.getBoundingClientRect();
const topY = rect.top + 80; // offset to pick line just under the header
const topY = visibleTopProbeY(rect);
let topLine = 0;
try {
const pos = cm.posAtCoords({ x: rect.left + 20, y: topY });
@ -1770,9 +1782,11 @@ export const __test = {
pruneCacheEntries,
removeCardAt,
serializeCacheFile,
shouldConfirmRegenerate,
summarizeViaApi,
touchCacheEntry,
translate,
tokenLimitFieldForOpenAiChat,
updateCardAt,
visibleTopProbeY,
};

View file

@ -12,3 +12,7 @@ export function serializeCacheFile(entries) {
entries: entries || {},
});
}
export function shouldConfirmRegenerate(entry, force) {
return !!force && !!entry && typeof entry.updatedAt === 'string' && entry.updatedAt.trim().length > 0;
}

View file

@ -56,6 +56,8 @@ export const STRINGS = {
noExportContent: '当前没有可导出的对照笔记',
noCopyContent: '当前没有可复制的对照笔记',
noActiveCard: '当前没有可跳转的摘要卡片',
confirmRegenerateEditedCards: '这篇笔记的对照卡片已被手动编辑。重新生成会覆盖这些修改,是否继续?',
regenerateCancelled: '已取消重新生成',
cardDeleted: '已删除此卡片',
cardSaved: '已保存此卡片',
editCardTitle: '编辑摘要卡片',
@ -194,6 +196,8 @@ export const STRINGS = {
noExportContent: 'No parallel notes to export',
noCopyContent: 'No parallel notes to copy',
noActiveCard: 'No active summary card to jump',
confirmRegenerateEditedCards: 'These parallel-reader cards were edited manually. Regenerating will overwrite those edits. Continue?',
regenerateCancelled: 'Regeneration cancelled',
cardDeleted: 'Deleted this card',
cardSaved: 'Saved this card',
editCardTitle: 'Edit summary card',

View file

@ -2,6 +2,13 @@
export type RafThrottledHandler = (() => void) & { cancel: () => void };
export function visibleTopProbeY(rect, maxOffset = 80, ratio = 0.1) {
const top = Number(rect?.top) || 0;
const height = Math.max(0, Number(rect?.height) || 0);
const cappedOffset = Math.min(Number(maxOffset) || 0, height * ratio);
return top + Math.max(0, cappedOffset);
}
function defaultSchedule(callback: FrameRequestCallback): number {
if (typeof requestAnimationFrame === 'function') return requestAnimationFrame(callback);
return setTimeout(() => callback(Date.now()), 16) as unknown as number;

View file

@ -72,7 +72,9 @@ assert.strictEqual(typeof t.pruneCacheEntries, 'function');
assert.strictEqual(typeof t.removeCardAt, 'function');
assert.strictEqual(typeof t.activeIndexAfterCardDelete, 'function');
assert.strictEqual(typeof t.createRafThrottledHandler, 'function');
assert.strictEqual(typeof t.visibleTopProbeY, 'function');
assert.strictEqual(typeof t.serializeCacheFile, 'function');
assert.strictEqual(typeof t.shouldConfirmRegenerate, 'function');
assert.strictEqual(typeof t.translate, 'function');
assert.strictEqual(typeof t.updateCardAt, 'function');
@ -246,6 +248,8 @@ scheduledFrames.shift()(0);
assert.strictEqual(throttledCalls, 1);
throttled();
assert.strictEqual(scheduledFrames.length, 1, 'scroll handler should be schedulable after the frame runs');
assert.strictEqual(t.visibleTopProbeY({ top: 100, height: 300 }), 130);
assert.strictEqual(t.visibleTopProbeY({ top: 100, height: 1200 }), 180);
const cardList = [{ title: 'A' }, { title: 'B' }, { title: 'C' }];
assert.deepStrictEqual(t.removeCardAt(cardList, 1), [{ title: 'A' }, { title: 'C' }]);
assert.deepStrictEqual(t.removeCardAt(cardList, -1), cardList);
@ -270,6 +274,10 @@ const untouchedCacheEntry = { generatedAt: '2024-01-01T00:00:00.000Z' };
assert.strictEqual(t.touchCacheEntry(null), null);
assert.strictEqual(t.touchCacheEntry(untouchedCacheEntry, '2024-01-05T00:00:00.000Z'), untouchedCacheEntry);
assert.strictEqual(untouchedCacheEntry.lastAccessedAt, '2024-01-05T00:00:00.000Z');
assert.strictEqual(t.shouldConfirmRegenerate({ updatedAt: '2024-01-05T00:00:00.000Z' }, true), true);
assert.strictEqual(t.shouldConfirmRegenerate({ updatedAt: '2024-01-05T00:00:00.000Z' }, false), false);
assert.strictEqual(t.shouldConfirmRegenerate({ generatedAt: '2024-01-01T00:00:00.000Z' }, true), false);
assert.strictEqual(t.shouldConfirmRegenerate(null, true), false);
const serializedCache = t.serializeCacheFile({
'note.md': { generatedAt: '2024-01-01T00:00:00.000Z', cards: [{ title: 'A' }] },
});