Skip to content

Commit fe009bd

Browse files
authored
feat: support provider with multi editor (#162)
1 parent e0c8418 commit fe009bd

File tree

8 files changed

+219
-2
lines changed

8 files changed

+219
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"fs": "INTEGRATION=filesystem npm run dev",
2222
"code": "INTEGRATION=code npm run dev",
2323
"diff-viewer": "INTEGRATION=diff-viewer npm run dev",
24+
"provider": "INTEGRATION=provider npm run dev",
2425
"build": "node scripts/build",
2526
"build:all": "yarn run bundle && yarn run generate && yarn run build",
2627
"bundle": "node scripts/bundle",

packages/core/src/api/exports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export { Emitter, Uri } from '@opensumi/ide-core-common';
44

55
export { getDefaultLayoutConfig } from '../core/layout';
66

7+
export { CodeEditor } from '../core/components/CodeEditor'
8+
79
export { BrowserFSFileType, HOME_ROOT, REPORT_NAME, WORKSPACE_ROOT };
810

911
export * from '../core/env';

packages/core/src/api/renderApp.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { REPORT_NAME, RuntimeConfig } from '@codeblitzjs/ide-sumi-core';
22
import { getDebugLogger, IReporterService, localize } from '@opensumi/ide-core-common';
33
import cls from 'classnames';
4-
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react';
4+
import React, { CSSProperties, useEffect, useMemo, useRef, useState, Fragment } from 'react';
55
import { createRoot } from 'react-dom/client';
66
import { useConstant } from '../core/hooks';
77
import { IPropsService, PropsServiceImpl } from '../core/props.service';
@@ -10,6 +10,7 @@ import styles from '../core/style.module.less';
1010
import { LandingProps, RootProps } from '../core/types';
1111
import { createApp } from './createApp';
1212
import { IAppInstance, IConfig } from './types';
13+
import { AppContext } from '../core/components/context'
1314

1415
export interface IAppRendererProps extends IConfig {
1516
onLoad?(app: IAppInstance): void;
@@ -132,3 +133,69 @@ export const AppRenderer: React.FC<IAppRendererProps> = ({ onLoad, Landing, ...o
132133
</Root>
133134
);
134135
};
136+
137+
export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> = ({ onLoad, children, ...opts }) => {
138+
const app = useConstant(() => {
139+
opts.appConfig.layoutComponent = () => <Fragment></Fragment>
140+
return createApp(opts)
141+
});
142+
const [clientApp, setClientApp] = useState<IAppInstance | null>(null);
143+
const appElementRef = useRef<React.FC | null>(null);
144+
const propsService = useConstant(() => new PropsServiceImpl<IAppRendererProps>());
145+
propsService.props = opts;
146+
147+
const runtimeConfig: RuntimeConfig = app.injector.get(RuntimeConfig);
148+
runtimeConfig.workspace = opts.runtimeConfig.workspace;
149+
150+
const [state, setState] = useState<{
151+
status: RootProps['status'];
152+
error?: RootProps['error'];
153+
}>(() => ({ status: 'pending' }));
154+
155+
useMemo(() => {
156+
app.injector.addProviders({
157+
token: IPropsService,
158+
useValue: propsService,
159+
});
160+
}, []);
161+
162+
useEffect(() => {
163+
app
164+
.start((appElement) => {
165+
appElementRef.current = appElement;
166+
setClientApp(app)
167+
setState({ status: 'success' });
168+
return Promise.resolve();
169+
})
170+
.then(() => {
171+
onLoad?.(app);
172+
})
173+
.catch((err: Error) => {
174+
setState({ error: err?.message || localize('error.unknown'), status: 'error' });
175+
176+
(app.injector.get(IReporterService) as IReporterService).point(
177+
REPORT_NAME.APP_START_ERROR,
178+
err?.message,
179+
{
180+
error: err,
181+
},
182+
);
183+
getDebugLogger().error(err);
184+
setTimeout(() => {
185+
throw err;
186+
});
187+
});
188+
189+
return () => {
190+
app.destroy();
191+
};
192+
}, []);
193+
194+
const contextValue = useMemo(() => ({ app: clientApp }), [clientApp])
195+
196+
return (
197+
<AppContext.Provider value={contextValue}>
198+
{children}
199+
</AppContext.Provider>
200+
)
201+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React, { useContext, useEffect, useMemo, useRef, useCallback } from 'react';
2+
3+
import * as path from 'path'
4+
import { URI, useInjectable } from '@opensumi/ide-core-browser';
5+
import { ConfigProvider, AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider'
6+
import { EditorCollectionService, ICodeEditor, IEditorDocumentModelRef } from '@opensumi/ide-editor/lib/common'
7+
import { IEditorDocumentModelService } from '@opensumi/ide-editor/lib/browser/doc-model/types'
8+
import { AppContext } from './context'
9+
10+
const noop = () => {}
11+
12+
export function useMemorizeFn<T extends (...args: any[]) => any>(fn: T) {
13+
const fnRef = useRef<T>(fn);
14+
fnRef.current = useMemo(() => fn, [fn]);
15+
return useCallback((...args: any) => fnRef.current(...args), []) as T;
16+
}
17+
18+
export interface ICodeEditorProps extends React.HTMLAttributes<HTMLDivElement> {
19+
uri: URI | string;
20+
21+
editorOptions?: any;
22+
23+
onEditorCreate?: (editor: ICodeEditor) => void;
24+
}
25+
26+
export const CodeEditorComponent = ({ uri, editorOptions, onEditorCreate, ...props }: ICodeEditorProps) => {
27+
const editorCollectionService: EditorCollectionService = useInjectable(EditorCollectionService);
28+
const documentService: IEditorDocumentModelService = useInjectable(IEditorDocumentModelService);
29+
const appConfig: AppConfig = useInjectable(AppConfig);
30+
31+
const containerRef = React.useRef<HTMLDivElement | null>(null);
32+
const uriStr = useMemo(() => {
33+
if (typeof uri === 'string') {
34+
return URI.file(path.join(appConfig.workspaceDir, uri)).toString()
35+
}
36+
return uri.toString()
37+
}, [uri])
38+
const fetchingUriRef = useRef<string>('');
39+
const documentModelRef = useRef<IEditorDocumentModelRef>();
40+
const editorRef = useRef<ICodeEditor>()
41+
const unmountRef = useRef(false);
42+
const onEditorCreateMemorizeFn = useMemorizeFn(onEditorCreate || noop)
43+
44+
const openDocumentModel = () => {
45+
if (editorRef.current && documentModelRef.current) {
46+
editorRef.current.open(documentModelRef.current)
47+
}
48+
}
49+
50+
React.useEffect(() => {
51+
if (containerRef.current) {
52+
editorRef.current?.dispose();
53+
editorRef.current = editorCollectionService.createCodeEditor(containerRef.current, {
54+
automaticLayout: true,
55+
...editorOptions,
56+
});
57+
onEditorCreateMemorizeFn(editorRef.current)
58+
openDocumentModel()
59+
}
60+
return () => {
61+
unmountRef.current = true;
62+
editorRef.current?.dispose()
63+
documentModelRef.current?.dispose()
64+
};
65+
}, []);
66+
67+
useEffect(() => {
68+
if (fetchingUriRef.current !== uriStr) {
69+
fetchingUriRef.current = uriStr
70+
documentService.createModelReference(new URI(uriStr), 'editor-react-component').then((ref) => {
71+
if (documentModelRef.current) {
72+
documentModelRef.current.dispose();
73+
}
74+
if (!unmountRef.current && ref.instance.uri.toString() === uriStr) {
75+
documentModelRef.current = ref;
76+
openDocumentModel()
77+
} else {
78+
ref.dispose();
79+
}
80+
});
81+
}
82+
}, [uriStr])
83+
84+
return <div ref={containerRef} {...props}></div>;
85+
};
86+
87+
export const CodeEditor = (props: ICodeEditorProps) => {
88+
const appContext = useContext(AppContext)
89+
if (!appContext.app) return null
90+
return (
91+
<ConfigProvider value={appContext.app.config}>
92+
<CodeEditorComponent {...props} />
93+
</ConfigProvider>
94+
)
95+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createContext } from 'react'
2+
import { ClientApp } from '@codeblitzjs/ide-sumi-core'
3+
4+
export const AppContext = createContext<{ app: ClientApp | null }>({ app: null })

packages/core/src/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ThemeType } from '@opensumi/ide-theme';
22
import { ComponentType } from 'react';
33

44
export interface LandingProps {
5-
status: 'loading' | 'success' | 'error';
5+
status: 'loading' | 'success' | 'error' | 'pending';
66
error?: string;
77
theme?: ThemeType;
88
className?: string;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#main {
2+
overflow: auto;
3+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { AppProvider, CodeEditor } from '@codeblitzjs/ide-core';
2+
import React from 'react';
3+
import { createRoot } from 'react-dom/client';
4+
import '@codeblitzjs/ide-core/languages';
5+
import '../index.css';
6+
import './index.css'
7+
8+
const App = () => (
9+
<AppProvider
10+
appConfig={{
11+
workspaceDir: 'my-workspace',
12+
layoutConfig: {},
13+
}}
14+
runtimeConfig={{
15+
biz: 'startup',
16+
workspace: {
17+
filesystem: {
18+
fs: 'FileIndexSystem',
19+
options: {
20+
// 初始全量文件索引
21+
requestFileIndex() {
22+
return Promise.resolve({
23+
'main.html': '<div id="root"></div>',
24+
'main.css': 'body {}',
25+
'main.js': 'console.log("main")',
26+
'package.json': '{\n "name": "startup"\n}',
27+
});
28+
},
29+
},
30+
}
31+
},
32+
}}
33+
>
34+
<CodeEditor
35+
uri="main.js"
36+
style={{ width: 1000, height: 300, marginBottom: 16 }}
37+
/>
38+
<CodeEditor
39+
uri="main.css"
40+
style={{ width: 1000, height: 300 }}
41+
/>
42+
</AppProvider>
43+
);
44+
45+
createRoot(document.getElementById('main') as HTMLElement).render(<App />);

0 commit comments

Comments
 (0)