diff --git a/packages/notebook/src/browser/libro-state-manager.ts b/packages/notebook/src/browser/libro-state-manager.ts new file mode 100644 index 0000000000..51af8401d9 --- /dev/null +++ b/packages/notebook/src/browser/libro-state-manager.ts @@ -0,0 +1,126 @@ +import { LibroView } from '@difizen/libro-jupyter/noeditor'; + +import { Autowired, Injectable } from '@opensumi/di'; +import { StorageProvider, URI } from '@opensumi/ide-core-common'; + +export interface INotebookViewState { + uri: string; + scrollTop: number; + lastActiveTime: number; +} + +export interface INotebookStateManager { + saveState(uri: URI, libroView: LibroView): void; + restoreState(uri: URI, libroView: LibroView): Promise; + clearState(uri: URI): void; + getAllStates(): Record; +} + +@Injectable() +export class LibroStateManager implements INotebookStateManager { + private static readonly STORAGE_KEY = 'libro-notebook-states'; + public static readonly LIBRO_SCROLL_ELEMENT = '.libro-view-content'; + + @Autowired(StorageProvider) + private readonly storageProvider: StorageProvider; + + private stateCache = new Map(); + + constructor() { + this.loadFromStorage(); + } + + private async loadFromStorage() { + try { + const storage = await this.storageProvider(new URI('libro-notebook-storage')); + const stored = await storage.get(LibroStateManager.STORAGE_KEY); + if (stored && typeof stored === 'object') { + Object.entries(stored).forEach(([uri, state]) => { + if (this.isValidState(state)) { + this.stateCache.set(uri, state as INotebookViewState); + } + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Failed to load notebook states from storage:', error); + } + } + + private async saveToStorage() { + try { + const storage = await this.storageProvider(new URI('libro-notebook-storage')); + const states: Record = {}; + this.stateCache.forEach((state, uri) => { + states[uri] = state; + }); + await storage.set(LibroStateManager.STORAGE_KEY, states); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Failed to save notebook states to storage:', error); + } + } + + private isValidState(state: any): state is INotebookViewState { + return state && typeof state === 'object' && typeof state.uri === 'string' && typeof state.scrollTop === 'number'; + } + + saveState(uri: URI, libroView: LibroView): void { + if (!libroView || !libroView.model || !libroView.container) { + return; + } + + try { + const libroViewContent = libroView.container.current?.querySelector(LibroStateManager.LIBRO_SCROLL_ELEMENT); + const state: INotebookViewState = { + uri: uri.toString(), + scrollTop: libroViewContent?.scrollTop || 0, + lastActiveTime: Date.now(), + }; + + this.stateCache.set(uri.toString(), state); + this.saveToStorage(); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Failed to save notebook state:', error); + } + } + + async restoreState(uri: URI, libroView: LibroView): Promise { + const state = this.stateCache.get(uri.toString()); + if (!state || !libroView || !libroView.model) { + return false; + } + + try { + const libroViewContent = libroView.container?.current?.querySelector(LibroStateManager.LIBRO_SCROLL_ELEMENT); + // 恢复 notebook 的滚动位置 + if (libroViewContent) { + libroViewContent.scrollTop = state.scrollTop; + } + + return true; + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Failed to restore notebook state:', error); + return false; + } + } + + clearState(uri: URI): void { + this.stateCache.delete(uri.toString()); + this.saveToStorage(); + } + + getAllStates(): Record { + const states: Record = {}; + this.stateCache.forEach((state, uri) => { + states[uri] = state; + }); + return states; + } + + getState(uri: URI): INotebookViewState | undefined { + return this.stateCache.get(uri.toString()); + } +} diff --git a/packages/notebook/src/browser/libro-state.ts b/packages/notebook/src/browser/libro-state.ts new file mode 100644 index 0000000000..26058971d0 --- /dev/null +++ b/packages/notebook/src/browser/libro-state.ts @@ -0,0 +1,10 @@ +import { Provider } from '@opensumi/di'; + +import { LibroStateManager } from './libro-state-manager'; + +export const LibroStateModule: Provider[] = [ + { + token: LibroStateManager, + useClass: LibroStateManager, + }, +]; diff --git a/packages/notebook/src/browser/libro.contribution.tsx b/packages/notebook/src/browser/libro.contribution.tsx index 8535d8b3f8..2f0d48df6a 100644 --- a/packages/notebook/src/browser/libro.contribution.tsx +++ b/packages/notebook/src/browser/libro.contribution.tsx @@ -44,6 +44,7 @@ import { LibroOpensumiModule } from './libro'; import { LibroDiffModule } from './libro/diff-view'; import { LibroOpener } from './libro-opener'; import { LibroVersionPreview } from './libro-preview.view'; +import { LibroStateModule } from './libro-state'; import { initLibroColorToken } from './libro.color.tokens'; import { LIBRO_COMPONENTS_SCHEME_ID, LIBRO_COMPONENT_ID, LIBRO_PREVIEW_COMPONENT_ID } from './libro.protocol'; import { OpensumiLibroView } from './libro.view'; @@ -132,6 +133,8 @@ export class LibroContribution initialize(app: IClientApp) { initLibroOpensumi(app.injector, manaContainer); + // 注册状态管理模块 + app.injector.addProviders(...LibroStateModule); } registerComponent(registry: ComponentRegistry) { diff --git a/packages/notebook/src/browser/libro.view.tsx b/packages/notebook/src/browser/libro.view.tsx index 99d47b6517..d488f6b811 100644 --- a/packages/notebook/src/browser/libro.view.tsx +++ b/packages/notebook/src/browser/libro.view.tsx @@ -1,11 +1,13 @@ import { DocumentCommands, LibroView } from '@difizen/libro-jupyter/noeditor'; import { CommandRegistry, Container, Disposable, ViewRender } from '@difizen/mana-app'; +import debounce from 'lodash/debounce'; import * as React from 'react'; import { message } from '@opensumi/ide-components'; import { localize, useInjectable } from '@opensumi/ide-core-browser'; import { ReactEditorComponent } from '@opensumi/ide-editor/lib/browser/types'; +import { LibroStateManager } from './libro-state-manager'; import styles from './libro.module.less'; import { ILibroOpensumiService } from './libro.service'; import { ManaContainer } from './mana'; @@ -16,16 +18,47 @@ export const OpensumiLibroView: ReactEditorComponent = (...params) => { const libroOpensumiService = useInjectable(ILibroOpensumiService); const manaContainer = useInjectable(ManaContainer); const commandRegistry = manaContainer.get(CommandRegistry); + const stateManager = useInjectable(LibroStateManager); const [libroView, setLibroView] = React.useState(undefined); + const [isStateRestored, setIsStateRestored] = React.useState(false); + const uri = params[0].resource.uri; + + // 保存状态的函数 + const saveNotebookState = React.useCallback(() => { + if (libroView && uri) { + stateManager.saveState(uri, libroView); + } + }, [libroView, uri, stateManager]); + + // 恢复状态的函数 + const restoreNotebookState = React.useCallback(async () => { + if (libroView && uri && !isStateRestored) { + const restored = await stateManager.restoreState(uri, libroView); + if (restored) { + setIsStateRestored(true); + } + } + }, [libroView, uri, stateManager, isStateRestored]); React.useEffect(() => { let autoSaveHandle: undefined | number; let modelChangeDisposer: undefined | Disposable; - libroOpensumiService.getOrCreateLibroView(params[0].resource.uri).then((libro) => { + + // 监听滚动变化(防抖) + const handleScroll = debounce(() => { + saveNotebookState(); + }, 500); + + libroOpensumiService.getOrCreateLibroView(uri).then((libro) => { setLibroView(libro); + + // 恢复状态 + restoreNotebookState(); + + // 监听模型变化 modelChangeDisposer = libro.model.onChanged(() => { - libroOpensumiService.updateDirtyStatus(params[0].resource.uri, true); + libroOpensumiService.updateDirtyStatus(uri, true); if (autoSaveHandle) { window.clearTimeout(autoSaveHandle); } @@ -42,15 +75,37 @@ export const OpensumiLibroView: ReactEditorComponent = (...params) => { }); }, AUTO_SAVE_DELAY); }); + libro.onSave(() => { - libroOpensumiService.updateDirtyStatus(params[0].resource.uri, false); + libroOpensumiService.updateDirtyStatus(uri, false); }); + + const libroViewContainer = libro.container?.current; + + if (libroViewContainer) { + const libroViewContent = libroViewContainer.querySelector(LibroStateManager.LIBRO_SCROLL_ELEMENT); + libroViewContent?.addEventListener('scroll', handleScroll); + } }); + return () => { modelChangeDisposer?.dispose(); window.clearTimeout(autoSaveHandle); + libroView?.container?.current + ?.querySelector(LibroStateManager.LIBRO_SCROLL_ELEMENT) + ?.removeEventListener('scroll', handleScroll); }; - }, []); + }, [libroOpensumiService, uri, commandRegistry, stateManager, saveNotebookState, restoreNotebookState]); + + // 当 notebook 完全加载后恢复状态 + React.useEffect(() => { + if (libroView && !isStateRestored) { + const timer = setTimeout(() => { + restoreNotebookState(); + }, 100); + return () => clearTimeout(timer); + } + }, [libroView, isStateRestored, restoreNotebookState]); return
{libroView && }
; };