Added code collapse state persistence

This commit is contained in:
2026-01-03 23:06:08 +08:00
parent aae86d8b4e
commit 532d30aa93
8 changed files with 222 additions and 45 deletions

View File

@@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
// Helper methods
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
public visitSingle: (ctx: TomlContext) => Doc | string;
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
constructor() {
super();
@@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
const singleElement = getSingle(ctx);
return this.visit(singleElement);
};
}
// Store reference to inherited visit method and override it
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this);
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => {
if (!ctx) {
return '';
}
/**
* Override visit method to handle TOML CST nodes
* Accepts both single node and array of nodes as per base class signature
*/
visit(cstNode: any, param?: any): any {
// Handle array of nodes
if (Array.isArray(cstNode)) {
return cstNode.map(node => this.visit(node, param));
}
const ctx = cstNode;
if (!ctx) {
return '';
}
// 确保节点有name属性才调用基类方法
if (ctx.name) {
// Try to use the inherited visit method first
const originalVisit = super.visit;
if (originalVisit) {
try {
return originalVisit(ctx, inParam);
return originalVisit.call(this, ctx, param);
} catch (error) {
console.warn('Original visit method failed:', error);
// Fallback to manual dispatch
}
}
// Fallback: manually dispatch based on node name/type
const methodName = ctx.name;
if (methodName && typeof (this as any)[methodName] === 'function') {
if (typeof (this as any)[methodName] === 'function') {
const visitMethod = (this as any)[methodName];
try {
if (ctx.children) {
@@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
console.warn(`Visit method ${methodName} failed:`, error);
}
}
// Final fallback: return image if available
return ctx.image || '';
};
}
// Final fallback: return image if available
return ctx.image || '';
}
/**
* Visit the root TOML document
*/
toml(ctx: TomlDocument): Doc {
toml(ctx: any): Doc {
// Handle empty toml document
if (!ctx.expression) {
return [line];
@@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/**
* Visit an expression (keyval, table, or comment)
*/
expression(ctx: TomlExpression): Doc | string {
expression(ctx: any): Doc | string {
if (ctx.keyval) {
let keyValDoc = this.visit(ctx.keyval[0]);
if (ctx.Comment) {
@@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/**
* Visit a key-value pair
*/
keyval(ctx: TomlKeyVal): Doc {
keyval(ctx: any): Doc {
const keyDoc = this.visit(ctx.key[0]);
const valueDoc = this.visit(ctx.val[0]);
return [keyDoc, ' = ', valueDoc];

View File

@@ -7,6 +7,16 @@ export interface DocumentStats {
selectedCharacters: number;
}
export interface FoldRange {
// 字符偏移(备用)
from: number;
to: number;
// 行号
fromLine: number;
toLine: number;
}
export const useEditorStateStore = defineStore('editorState', () => {
// 光标位置存储 Record<docId, cursorPosition>
const cursorPositions = ref<Record<number, number>>({});
@@ -14,6 +24,9 @@ export const useEditorStateStore = defineStore('editorState', () => {
// 文档统计数据存储 Record<docId, DocumentStats>
const documentStats = ref<Record<number, DocumentStats>>({});
// 折叠状态存储 Record<docId, FoldRange[]>
const foldStates = ref<Record<number, FoldRange[]>>({});
// 保存光标位置
const saveCursorPosition = (docId: number, position: number) => {
cursorPositions.value[docId] = position;
@@ -38,25 +51,40 @@ export const useEditorStateStore = defineStore('editorState', () => {
};
};
// 保存折叠状态
const saveFoldState = (docId: number, foldRanges: FoldRange[]) => {
foldStates.value[docId] = foldRanges;
};
// 获取折叠状态
const getFoldState = (docId: number): FoldRange[] => {
return foldStates.value[docId] || [];
};
// 清除文档状态
const clearDocumentState = (docId: number) => {
delete cursorPositions.value[docId];
delete documentStats.value[docId];
delete foldStates.value[docId];
};
// 清除所有状态
const clearAllStates = () => {
cursorPositions.value = {};
documentStats.value = {};
foldStates.value = {};
};
return {
cursorPositions,
documentStats,
foldStates,
saveCursorPosition,
getCursorPosition,
saveDocumentStats,
getDocumentStats,
saveFoldState,
getFoldState,
clearDocumentState,
clearAllStates
};
@@ -64,7 +92,7 @@ export const useEditorStateStore = defineStore('editorState', () => {
persist: {
key: 'voidraft-editor-state',
storage: localStorage,
pick: ['cursorPositions']
pick: ['cursorPositions', 'foldStates']
}
});

View File

@@ -13,6 +13,7 @@ import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
import {createFoldStateExtension, restoreFoldState} from '@/views/editor/basic/foldStateExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import {
createDynamicExtensions,
@@ -118,6 +119,9 @@ export const useEditorStore = defineStore('editor', () => {
// 光标位置持久化扩展
const cursorPositionExtension = createCursorPositionExtension(docId);
// 折叠状态持久化扩展
const foldStateExtension = createFoldStateExtension(docId);
// 快捷键扩展
const keymapExtension = await createDynamicKeymapExtension();
@@ -136,6 +140,7 @@ export const useEditorStore = defineStore('editor', () => {
contentChangeExtension,
codeBlockExtension,
cursorPositionExtension,
foldStateExtension,
...dynamicExtensions,
];
@@ -227,6 +232,12 @@ export const useEditorStore = defineStore('editor', () => {
requestAnimationFrame(() => {
scrollToCursor(instance.view);
instance.view.focus();
// 恢复折叠状态
const savedFoldState = editorStateStore.getFoldState(instance.documentId);
if (savedFoldState.length > 0) {
restoreFoldState(instance.view, savedFoldState);
}
});
} catch (error) {
console.error('Error showing editor:', error);

View File

@@ -0,0 +1,113 @@
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import {foldedRanges, foldEffect, unfoldEffect} from '@codemirror/language';
import {StateEffect} from '@codemirror/state';
import {useEditorStateStore, type FoldRange} from '@/stores/editorStateStore';
import {createDebounce} from '@/common/utils/debounce';
/**
* 折叠状态持久化扩展
*/
export function createFoldStateExtension(documentId: number) {
return ViewPlugin.fromClass(
class FoldStatePlugin {
private readonly editorStateStore = useEditorStateStore();
private readonly debouncedSave;
constructor(private view: EditorView) {
const {debouncedFn, flush} = createDebounce(
() => this.saveFoldState(),
{delay: 500}
);
this.debouncedSave = {fn: debouncedFn, flush};
}
update(update: ViewUpdate) {
// 检查是否有折叠/展开操作
const hasFoldChange = update.transactions.some(tr =>
tr.effects.some(effect =>
effect.is(foldEffect) || effect.is(unfoldEffect)
)
);
if (hasFoldChange) {
this.debouncedSave.fn();
}
}
destroy() {
// 销毁时立即执行待保存的操作
this.debouncedSave.flush();
// 再保存一次确保最新状态
this.saveFoldState();
}
private saveFoldState() {
const foldRanges: FoldRange[] = [];
const foldCursor = foldedRanges(this.view.state).iter();
const doc = this.view.state.doc;
// 遍历所有折叠区间
while (foldCursor.value !== null) {
const from = foldCursor.from;
const to = foldCursor.to;
// 同时记录字符偏移和行号
const fromLine = doc.lineAt(from).number;
const toLine = doc.lineAt(to).number;
foldRanges.push({
from,
to,
fromLine,
toLine
});
foldCursor.next();
}
this.editorStateStore.saveFoldState(documentId, foldRanges);
}
}
);
}
/**
* 恢复折叠状态(基于行号,更稳定)
* @param view 编辑器视图
* @param foldRanges 要恢复的折叠区间
*/
export function restoreFoldState(view: EditorView, foldRanges: FoldRange[]) {
if (foldRanges.length === 0) return;
const doc = view.state.doc;
const effects: StateEffect<any>[] = [];
for (const range of foldRanges) {
try {
// 优先使用行号恢复
if (range.fromLine && range.toLine) {
// 确保行号在有效范围内
if (range.fromLine >= 1 && range.toLine <= doc.lines && range.fromLine <= range.toLine) {
const fromPos = doc.line(range.fromLine).from;
const toPos = doc.line(range.toLine).to;
effects.push(foldEffect.of({from: fromPos, to: toPos}));
continue;
}
}
// 使用字符偏移
if (range.from >= 0 && range.to <= doc.length && range.from < range.to) {
effects.push(foldEffect.of({from: range.from, to: range.to}));
}
} catch (error) {
// 忽略无效的折叠区间
console.warn('Failed to restore fold range:', range, error);
}
}
if (effects.length > 0) {
view.dispatch({effects});
}
}

View File

@@ -89,7 +89,6 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
showBackground = true,
enableAutoDetection = true,
defaultLanguage = 'text',
separatorHeight = 12,
} = options;
return [
@@ -108,8 +107,7 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
// 视觉装饰系统
...getBlockDecorationExtensions({
showBackground,
separatorHeight
showBackground
}),
// 光标保护(防止方向键移动到分隔符上)