Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/notebook/src/browser/libro-state-manager.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;
clearState(uri: URI): void;
getAllStates(): Record<string, INotebookViewState>;
}

@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<string, INotebookViewState>();

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<string, INotebookViewState> = {};
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<boolean> {
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<string, INotebookViewState> {
const states: Record<string, INotebookViewState> = {};
this.stateCache.forEach((state, uri) => {
states[uri] = state;
});
return states;
}

getState(uri: URI): INotebookViewState | undefined {
return this.stateCache.get(uri.toString());
}
}
10 changes: 10 additions & 0 deletions packages/notebook/src/browser/libro-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Provider } from '@opensumi/di';

import { LibroStateManager } from './libro-state-manager';

export const LibroStateModule: Provider[] = [
{
token: LibroStateManager,
useClass: LibroStateManager,
},
];
3 changes: 3 additions & 0 deletions packages/notebook/src/browser/libro.contribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,6 +133,8 @@ export class LibroContribution

initialize(app: IClientApp) {
initLibroOpensumi(app.injector, manaContainer);
// 注册状态管理模块
app.injector.addProviders(...LibroStateModule);
}

registerComponent(registry: ComponentRegistry) {
Expand Down
63 changes: 59 additions & 4 deletions packages/notebook/src/browser/libro.view.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,16 +18,47 @@ export const OpensumiLibroView: ReactEditorComponent = (...params) => {
const libroOpensumiService = useInjectable<ILibroOpensumiService>(ILibroOpensumiService);
const manaContainer = useInjectable<Container>(ManaContainer);
const commandRegistry = manaContainer.get(CommandRegistry);
const stateManager = useInjectable<LibroStateManager>(LibroStateManager);

const [libroView, setLibroView] = React.useState<LibroView | undefined>(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);
}
Expand All @@ -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 <div className={styles.libroView}>{libroView && <ViewRender view={libroView}></ViewRender>}</div>;
};
Loading