✨ Added code collapse state persistence
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
113
frontend/src/views/editor/basic/foldStateExtension.ts
Normal file
113
frontend/src/views/editor/basic/foldStateExtension.ts
Normal 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});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
// 光标保护(防止方向键移动到分隔符上)
|
||||
|
||||
Reference in New Issue
Block a user