mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-20 07:23:38 +00:00
fix(editor): 修复 CodeEditor setValue 时滚动位置与折叠等视图状态丢失
使用 saveViewState/restoreViewState 替代 getPosition/setPosition,并放到 nextTick 中执行,避免被 setAutoHeight 的 setScrollTop(0) 覆盖,导致光标 位置变化时编辑器滚动跳回顶部。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
873a51fc87
commit
f1aedc4ce7
@ -214,24 +214,32 @@ const setEditorValue = (v: string | any, m: string | any) => {
|
||||
if (props.type === 'diff') {
|
||||
const originalModel = monaco.editor.createModel(values.value, 'text/javascript');
|
||||
const modifiedModel = monaco.editor.createModel(toString(m, props.language), 'text/javascript');
|
||||
const position = vsDiffEditor?.getPosition();
|
||||
// 保存视图状态(光标、选区、滚动、折叠等)
|
||||
const viewState = vsDiffEditor?.saveViewState();
|
||||
const result = vsDiffEditor?.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel,
|
||||
});
|
||||
if (position) {
|
||||
vsDiffEditor?.setPosition(position);
|
||||
vsDiffEditor?.focus();
|
||||
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0,这里也放到 nextTick 中
|
||||
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
|
||||
if (viewState) {
|
||||
nextTick(() => {
|
||||
vsDiffEditor?.restoreViewState(viewState);
|
||||
vsDiffEditor?.focus();
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// 保存当前光标位置
|
||||
const position = vsEditor?.getPosition();
|
||||
// 保存视图状态(光标、选区、滚动、折叠等)
|
||||
const viewState = vsEditor?.saveViewState();
|
||||
const result = vsEditor?.setValue(values.value);
|
||||
// 恢复光标位置
|
||||
if (position) {
|
||||
vsEditor?.setPosition(position);
|
||||
vsEditor?.focus();
|
||||
// setAutoHeight 内部会在 nextTick 中将 scrollTop 重置为 0,这里也放到 nextTick 中
|
||||
// 利用 Vue nextTick 队列的 FIFO 特性,保证恢复在重置之后执行
|
||||
if (viewState) {
|
||||
nextTick(() => {
|
||||
vsEditor?.restoreViewState(viewState);
|
||||
vsEditor?.focus();
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -20,8 +20,8 @@ const {
|
||||
vsEditorInstance: {
|
||||
getValue: vi.fn(() => 'editor-value'),
|
||||
setValue: vi.fn(),
|
||||
getPosition: vi.fn(() => ({ lineNumber: 1, column: 1 })),
|
||||
setPosition: vi.fn(),
|
||||
saveViewState: vi.fn(() => null),
|
||||
restoreViewState: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
layout: vi.fn(),
|
||||
setScrollTop: vi.fn(),
|
||||
@ -34,8 +34,8 @@ const {
|
||||
} as any,
|
||||
vsDiffEditorInstance: {
|
||||
getModifiedEditor: vi.fn(),
|
||||
getPosition: vi.fn(() => null),
|
||||
setPosition: vi.fn(),
|
||||
saveViewState: vi.fn(() => null),
|
||||
restoreViewState: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
layout: vi.fn(),
|
||||
@ -115,6 +115,9 @@ beforeEach(() => {
|
||||
vsEditorInstance.onDidBlurEditorWidget.mockImplementation((cb: any) => {
|
||||
blurHandlers.push(cb);
|
||||
});
|
||||
// 默认无视图状态,避免上一个用例 mockReturnValue 渗透
|
||||
vsEditorInstance.saveViewState.mockReturnValue(null);
|
||||
vsDiffEditorInstance.saveViewState.mockReturnValue(null);
|
||||
const modifiedEditor = {
|
||||
getValue: vi.fn(() => 'modified-value'),
|
||||
onDidChangeModelContent: vi.fn((cb: any) => diffContentChangeHandlers.push(cb)),
|
||||
@ -231,6 +234,86 @@ describe('CodeEditor', () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('setValue 后通过 saveViewState / restoreViewState 保留光标与滚动状态', async () => {
|
||||
const fakeViewState = { __fake: true } as any;
|
||||
vsEditorInstance.saveViewState.mockReturnValue(fakeViewState);
|
||||
|
||||
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
|
||||
await flush();
|
||||
vsEditorInstance.restoreViewState.mockClear();
|
||||
vsEditorInstance.focus.mockClear();
|
||||
|
||||
await wrapper.setProps({ initValues: 'xyz' } as any);
|
||||
await flush();
|
||||
|
||||
expect(vsEditorInstance.saveViewState).toHaveBeenCalled();
|
||||
expect(vsEditorInstance.restoreViewState).toHaveBeenCalledWith(fakeViewState);
|
||||
expect(vsEditorInstance.focus).toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('saveViewState 返回 null 时不调用 restoreViewState / focus', async () => {
|
||||
vsEditorInstance.saveViewState.mockReturnValue(null);
|
||||
|
||||
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
|
||||
await flush();
|
||||
vsEditorInstance.restoreViewState.mockClear();
|
||||
vsEditorInstance.focus.mockClear();
|
||||
|
||||
await wrapper.setProps({ initValues: 'xyz' } as any);
|
||||
await flush();
|
||||
|
||||
expect(vsEditorInstance.restoreViewState).not.toHaveBeenCalled();
|
||||
expect(vsEditorInstance.focus).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('restoreViewState 在 setAutoHeight 的 setScrollTop 之后执行', async () => {
|
||||
const fakeViewState = { __fake: true } as any;
|
||||
vsEditorInstance.saveViewState.mockReturnValue(fakeViewState);
|
||||
|
||||
const wrapper = mount(CodeEditor, {
|
||||
props: { initValues: 'a', autosize: { minRows: 1, maxRows: 10 } } as any,
|
||||
attachTo: document.body,
|
||||
});
|
||||
await flush();
|
||||
vsEditorInstance.setScrollTop.mockClear();
|
||||
vsEditorInstance.restoreViewState.mockClear();
|
||||
|
||||
// 行数变化触发 setAutoHeight 的 nextTick(其中会调用 setScrollTop(0))
|
||||
await wrapper.setProps({ initValues: 'a\nb\nc\nd\ne' } as any);
|
||||
await flush();
|
||||
|
||||
expect(vsEditorInstance.setScrollTop).toHaveBeenCalled();
|
||||
expect(vsEditorInstance.restoreViewState).toHaveBeenCalledWith(fakeViewState);
|
||||
const setScrollTopOrder = vsEditorInstance.setScrollTop.mock.invocationCallOrder[0];
|
||||
const restoreOrder = vsEditorInstance.restoreViewState.mock.invocationCallOrder[0];
|
||||
expect(setScrollTopOrder).toBeLessThan(restoreOrder);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('diff 模式下保留视图状态', async () => {
|
||||
const fakeViewState = { __fake_diff: true } as any;
|
||||
vsDiffEditorInstance.saveViewState.mockReturnValue(fakeViewState);
|
||||
|
||||
const wrapper = mount(CodeEditor, {
|
||||
props: { type: 'diff', initValues: 'a', modifiedValues: 'b' } as any,
|
||||
attachTo: document.body,
|
||||
});
|
||||
await flush();
|
||||
vsDiffEditorInstance.saveViewState.mockClear();
|
||||
vsDiffEditorInstance.restoreViewState.mockClear();
|
||||
vsDiffEditorInstance.focus.mockClear();
|
||||
|
||||
await wrapper.setProps({ initValues: 'x' } as any);
|
||||
await flush();
|
||||
|
||||
expect(vsDiffEditorInstance.saveViewState).toHaveBeenCalled();
|
||||
expect(vsDiffEditorInstance.restoreViewState).toHaveBeenCalledWith(fakeViewState);
|
||||
expect(vsDiffEditorInstance.focus).toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('expose getEditor / focus / setEditorValue', async () => {
|
||||
vsEditorInstance.getValue.mockReturnValue('editor-value');
|
||||
const wrapper = mount(CodeEditor, { props: { initValues: 'abc' } as any, attachTo: document.body });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user