Skip to content

Commit 9c32255

Browse files
author
winjo
authored
feat: support diff editor (#167)
1 parent 0a665e9 commit 9c32255

10 files changed

Lines changed: 208 additions & 20 deletions

File tree

packages/core/src/api/exports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export { getDefaultLayoutConfig } from '../core/layout';
66

77
export { CodeEditor } from '../core/components/CodeEditor'
88

9+
export { DiffEditor } from '../core/components/DiffEditor'
10+
911
export { BrowserFSFileType, HOME_ROOT, REPORT_NAME, WORKSPACE_ROOT };
1012

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

packages/core/src/api/renderApp.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const AppRenderer: React.FC<IAppRendererProps> = ({ onLoad, Landing, ...o
136136

137137
export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> = ({ onLoad, children, ...opts }) => {
138138
const app = useConstant(() => {
139-
opts.appConfig.layoutComponent = () => <Fragment></Fragment>
139+
opts.appConfig.layoutComponent = () => null;
140140
return createApp(opts)
141141
});
142142
const [clientApp, setClientApp] = useState<IAppInstance | null>(null);
@@ -150,7 +150,7 @@ export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> =
150150
const [state, setState] = useState<{
151151
status: RootProps['status'];
152152
error?: RootProps['error'];
153-
}>(() => ({ status: 'pending' }));
153+
}>(() => ({ status: 'loading' }));
154154

155155
useMemo(() => {
156156
app.injector.addProviders({
@@ -191,7 +191,7 @@ export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> =
191191
};
192192
}, []);
193193

194-
const contextValue = useMemo(() => ({ app: clientApp }), [clientApp])
194+
const contextValue = useMemo(() => ({ app: clientApp, startState: state }), [clientApp, state])
195195

196196
return (
197197
<AppContext.Provider value={contextValue}>

packages/core/src/core/components/CodeEditor.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import React, { useContext, useEffect, useMemo, useRef, useCallback } from 'react';
1+
import React, { useContext, useEffect, useMemo, useRef } from 'react';
22

33
import * as path from 'path'
44
import { URI, useInjectable } from '@opensumi/ide-core-browser';
55
import { ConfigProvider, AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider'
66
import { EditorCollectionService, ICodeEditor, IEditorDocumentModelRef } from '@opensumi/ide-editor/lib/common'
77
import { IEditorDocumentModelService } from '@opensumi/ide-editor/lib/browser/doc-model/types'
88
import { AppContext } from './context'
9+
import { useMemorizeFn } from '../hooks'
10+
import { parseUri } from './util'
911

1012
const noop = () => {}
1113

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-
1814
export interface ICodeEditorProps extends React.HTMLAttributes<HTMLDivElement> {
1915
uri: URI | string;
2016

@@ -29,12 +25,7 @@ export const CodeEditorComponent = ({ uri, editorOptions, onEditorCreate, ...pro
2925
const appConfig: AppConfig = useInjectable(AppConfig);
3026

3127
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])
28+
const uriStr = useMemo(() => parseUri(uri, appConfig.workspaceDir), [uri])
3829
const fetchingUriRef = useRef<string>('');
3930
const documentModelRef = useRef<IEditorDocumentModelRef>();
4031
const editorRef = useRef<ICodeEditor>()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useContext, useEffect, useMemo, useRef, MutableRefObject } 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, IDiffEditor, 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+
import { useMemorizeFn } from '../hooks'
10+
import { parseUri } from './util'
11+
12+
const noop = () => {}
13+
14+
export interface ICodeEditorProps extends React.HTMLAttributes<HTMLDivElement> {
15+
originalUri: URI | string;
16+
17+
modifiedUri: URI | string;
18+
19+
editorOptions?: any;
20+
21+
onEditorCreate?: (editor: IDiffEditor) => void;
22+
}
23+
24+
export const DiffEditorComponent = ({ originalUri, modifiedUri, editorOptions, onEditorCreate, ...props }: ICodeEditorProps) => {
25+
const editorCollectionService: EditorCollectionService = useInjectable(EditorCollectionService);
26+
const documentService: IEditorDocumentModelService = useInjectable(IEditorDocumentModelService);
27+
const appConfig: AppConfig = useInjectable(AppConfig);
28+
29+
const containerRef = React.useRef<HTMLDivElement | null>(null);
30+
const editorRef = useRef<IDiffEditor>()
31+
const unmountRef = useRef(false);
32+
const onEditorCreateMemorizeFn = useMemorizeFn(onEditorCreate || noop)
33+
34+
const originalUriStr = useMemo(() => parseUri(originalUri, appConfig.workspaceDir), [originalUri])
35+
const originalFetchingUriRef = useRef<string>('');
36+
const originalDocumentModelRef = useRef<IEditorDocumentModelRef>();
37+
38+
const modifiedUriStr = useMemo(() => parseUri(modifiedUri, appConfig.workspaceDir), [modifiedUri])
39+
const modifiedFetchingUriRef = useRef<string>('');
40+
const modifiedDocumentModelRef = useRef<IEditorDocumentModelRef>();
41+
42+
const openDocumentModel = () => {
43+
if (editorRef.current && originalDocumentModelRef.current && modifiedDocumentModelRef.current) {
44+
editorRef.current.compare(originalDocumentModelRef.current, modifiedDocumentModelRef.current)
45+
}
46+
}
47+
48+
React.useEffect(() => {
49+
if (containerRef.current) {
50+
editorRef.current?.dispose();
51+
editorRef.current = editorCollectionService.createDiffEditor(containerRef.current, {
52+
automaticLayout: true,
53+
...editorOptions,
54+
});
55+
onEditorCreateMemorizeFn(editorRef.current)
56+
openDocumentModel()
57+
}
58+
return () => {
59+
unmountRef.current = true;
60+
editorRef.current?.dispose()
61+
originalDocumentModelRef.current?.dispose()
62+
modifiedDocumentModelRef.current?.dispose()
63+
};
64+
}, []);
65+
66+
useEffect(() => {
67+
const createModelReference = (
68+
fetchingUriRef: MutableRefObject<string>,
69+
uriStr: string,
70+
documentModelRef: MutableRefObject<IEditorDocumentModelRef | undefined>
71+
) => {
72+
if (fetchingUriRef.current !== uriStr) {
73+
fetchingUriRef.current = uriStr
74+
documentService.createModelReference(new URI(uriStr), 'diff-editor-react-component').then((ref) => {
75+
if (documentModelRef.current) {
76+
documentModelRef.current.dispose();
77+
}
78+
if (!unmountRef.current && ref.instance.uri.toString() === uriStr) {
79+
documentModelRef.current = ref;
80+
openDocumentModel()
81+
} else {
82+
ref.dispose();
83+
}
84+
});
85+
}
86+
}
87+
createModelReference(originalFetchingUriRef, originalUriStr, originalDocumentModelRef)
88+
createModelReference(modifiedFetchingUriRef, modifiedUriStr, modifiedDocumentModelRef)
89+
}, [originalUriStr, modifiedUriStr])
90+
91+
return <div ref={containerRef} {...props}></div>;
92+
};
93+
94+
export const DiffEditor = (props: ICodeEditorProps) => {
95+
const appContext = useContext(AppContext)
96+
if (!appContext.app) return null
97+
return (
98+
<ConfigProvider value={appContext.app.config}>
99+
<DiffEditorComponent {...props} />
100+
</ConfigProvider>
101+
)
102+
};
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { createContext } from 'react'
22
import { ClientApp } from '@codeblitzjs/ide-sumi-core'
33

4-
export const AppContext = createContext<{ app: ClientApp | null }>({ app: null })
4+
export const AppContext = createContext<{
5+
app: ClientApp | null,
6+
startState?: { status: 'loading' | 'success' | 'error', error?: string }
7+
}>({ app: null })
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { URI } from '@opensumi/ide-core-browser';
2+
import * as path from 'path'
3+
4+
export const parseUri = (uriInput: URI | string, workspaceDir: string) => {
5+
if (typeof uriInput === 'string') {
6+
let uri = URI.parse(uriInput)
7+
// 说明传的是路径
8+
if (uri.scheme === 'file' && !uriInput.startsWith('file:')) {
9+
uri = uri.withPath(path.join(workspaceDir, uri.codeUri.path))
10+
}
11+
return uri.toString();
12+
}
13+
return uriInput.toString()
14+
}

packages/core/src/core/hooks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injector } from '@opensumi/di';
2-
import { useRef } from 'react';
2+
import { useRef, useMemo, useCallback } from 'react';
33
import { IAppInstance } from '../api/types';
44

55
export const useConstant = <T>(fn: () => T): T => {
@@ -10,6 +10,12 @@ export const useConstant = <T>(fn: () => T): T => {
1010
return valueRef.current.v;
1111
};
1212

13+
export function useMemorizeFn<T extends (...args: any[]) => any>(fn: T) {
14+
const fnRef = useRef<T>(fn);
15+
fnRef.current = useMemo(() => fn, [fn]);
16+
return useCallback((...args: any) => fnRef.current(...args), []) as T;
17+
}
18+
1319
export let singleInjector: Injector | null = null;
1420

1521
export function setSingleInjector(inject) {

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' | 'pending';
5+
status: 'loading' | 'success' | 'error';
66
error?: string;
77
theme?: ThemeType;
88
className?: string;

packages/startup/src/provider/index.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { AppProvider, CodeEditor } from '@codeblitzjs/ide-core';
1+
import { AppProvider, CodeEditor, DiffEditor } from '@codeblitzjs/ide-core';
22
import React from 'react';
33
import { createRoot } from 'react-dom/client';
44
import '@codeblitzjs/ide-core/languages';
5+
import { SampleModule } from './module'
56
import '../index.css';
67
import './index.css'
78

@@ -10,6 +11,7 @@ const App = () => (
1011
appConfig={{
1112
workspaceDir: 'my-workspace',
1213
layoutConfig: {},
14+
modules: [SampleModule],
1315
}}
1416
runtimeConfig={{
1517
biz: 'startup',
@@ -34,10 +36,30 @@ const App = () => (
3436
<CodeEditor
3537
uri="main.js"
3638
style={{ width: 1000, height: 300, marginBottom: 16 }}
39+
editorOptions={{
40+
scrollbar: {
41+
alwaysConsumeMouseWheel: false
42+
}
43+
}}
3744
/>
3845
<CodeEditor
3946
uri="main.css"
4047
style={{ width: 1000, height: 300 }}
48+
editorOptions={{
49+
scrollbar: {
50+
alwaysConsumeMouseWheel: false
51+
}
52+
}}
53+
/>
54+
<DiffEditor
55+
originalUri="sample:/a1.js"
56+
modifiedUri="sample:/a2.js"
57+
editorOptions={{
58+
scrollbar: {
59+
alwaysConsumeMouseWheel: false
60+
}
61+
}}
62+
style={{ width: 1000, height: 300 }}
4163
/>
4264
</AppProvider>
4365
);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Autowired, Injectable, Provider } from '@opensumi/di';
2+
import { BrowserModule } from '@opensumi/ide-core-browser';
3+
import { IEditorDocumentModelContentProvider, BrowserEditorContribution, IEditorDocumentModelContentRegistry } from '@opensumi/ide-editor/lib/browser'
4+
import { URI, Emitter, Event, Domain } from '@opensumi/ide-core-common'
5+
6+
const contentMap = {
7+
'a1.js': `const add = (x, y) => {
8+
return x + y
9+
}
10+
`,
11+
'a2.js': `const add = (x, y) => {
12+
return x + y + 1
13+
}
14+
`,
15+
}
16+
17+
@Injectable()
18+
export class SampleSchemeDocumentProvider implements IEditorDocumentModelContentProvider {
19+
handlesScheme(scheme: string) {
20+
return scheme === 'sample';
21+
}
22+
23+
async provideEditorDocumentModelContent(uri: URI): Promise<string> {
24+
return contentMap[uri.codeUri.path.slice(1)]
25+
}
26+
27+
isReadonly() {
28+
return true;
29+
}
30+
31+
private _onDidChangeContent: Emitter<URI> = new Emitter();
32+
onDidChangeContent: Event<URI> = this._onDidChangeContent.event;
33+
}
34+
35+
@Domain(BrowserEditorContribution)
36+
class SampleContribution implements BrowserEditorContribution {
37+
@Autowired(SampleSchemeDocumentProvider)
38+
private readonly sampleSchemeDocumentProvider: SampleSchemeDocumentProvider;
39+
40+
registerEditorDocumentModelContentProvider(registry: IEditorDocumentModelContentRegistry): void {
41+
registry.registerEditorDocumentModelContentProvider(this.sampleSchemeDocumentProvider)
42+
}
43+
}
44+
45+
@Injectable()
46+
export class SampleModule extends BrowserModule {
47+
providers: Provider[] = [SampleContribution];
48+
}

0 commit comments

Comments
 (0)