Skip to content

Commit

Permalink
feat: support diff editor (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
winjo authored Sep 9, 2024
1 parent 0a665e9 commit 9c32255
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 20 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/api/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export { getDefaultLayoutConfig } from '../core/layout';

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

export { DiffEditor } from '../core/components/DiffEditor'

export { BrowserFSFileType, HOME_ROOT, REPORT_NAME, WORKSPACE_ROOT };

export * from '../core/env';
6 changes: 3 additions & 3 deletions packages/core/src/api/renderApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const AppRenderer: React.FC<IAppRendererProps> = ({ onLoad, Landing, ...o

export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> = ({ onLoad, children, ...opts }) => {
const app = useConstant(() => {
opts.appConfig.layoutComponent = () => <Fragment></Fragment>
opts.appConfig.layoutComponent = () => null;
return createApp(opts)
});
const [clientApp, setClientApp] = useState<IAppInstance | null>(null);
Expand All @@ -150,7 +150,7 @@ export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> =
const [state, setState] = useState<{
status: RootProps['status'];
error?: RootProps['error'];
}>(() => ({ status: 'pending' }));
}>(() => ({ status: 'loading' }));

useMemo(() => {
app.injector.addProviders({
Expand Down Expand Up @@ -191,7 +191,7 @@ export const AppProvider: React.FC<React.PropsWithChildren<IAppRendererProps>> =
};
}, []);

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

return (
<AppContext.Provider value={contextValue}>
Expand Down
17 changes: 4 additions & 13 deletions packages/core/src/core/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import React, { useContext, useEffect, useMemo, useRef, useCallback } from 'react';
import React, { useContext, useEffect, useMemo, useRef } from 'react';

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

const noop = () => {}

export function useMemorizeFn<T extends (...args: any[]) => any>(fn: T) {
const fnRef = useRef<T>(fn);
fnRef.current = useMemo(() => fn, [fn]);
return useCallback((...args: any) => fnRef.current(...args), []) as T;
}

export interface ICodeEditorProps extends React.HTMLAttributes<HTMLDivElement> {
uri: URI | string;

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

const containerRef = React.useRef<HTMLDivElement | null>(null);
const uriStr = useMemo(() => {
if (typeof uri === 'string') {
return URI.file(path.join(appConfig.workspaceDir, uri)).toString()
}
return uri.toString()
}, [uri])
const uriStr = useMemo(() => parseUri(uri, appConfig.workspaceDir), [uri])
const fetchingUriRef = useRef<string>('');
const documentModelRef = useRef<IEditorDocumentModelRef>();
const editorRef = useRef<ICodeEditor>()
Expand Down
102 changes: 102 additions & 0 deletions packages/core/src/core/components/DiffEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useContext, useEffect, useMemo, useRef, MutableRefObject } from 'react';

import * as path from 'path'
import { URI, useInjectable } from '@opensumi/ide-core-browser';
import { ConfigProvider, AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider'
import { EditorCollectionService, IDiffEditor, IEditorDocumentModelRef } from '@opensumi/ide-editor/lib/common'
import { IEditorDocumentModelService } from '@opensumi/ide-editor/lib/browser/doc-model/types'
import { AppContext } from './context'
import { useMemorizeFn } from '../hooks'
import { parseUri } from './util'

const noop = () => {}

export interface ICodeEditorProps extends React.HTMLAttributes<HTMLDivElement> {
originalUri: URI | string;

modifiedUri: URI | string;

editorOptions?: any;

onEditorCreate?: (editor: IDiffEditor) => void;
}

export const DiffEditorComponent = ({ originalUri, modifiedUri, editorOptions, onEditorCreate, ...props }: ICodeEditorProps) => {
const editorCollectionService: EditorCollectionService = useInjectable(EditorCollectionService);
const documentService: IEditorDocumentModelService = useInjectable(IEditorDocumentModelService);
const appConfig: AppConfig = useInjectable(AppConfig);

const containerRef = React.useRef<HTMLDivElement | null>(null);
const editorRef = useRef<IDiffEditor>()
const unmountRef = useRef(false);
const onEditorCreateMemorizeFn = useMemorizeFn(onEditorCreate || noop)

const originalUriStr = useMemo(() => parseUri(originalUri, appConfig.workspaceDir), [originalUri])
const originalFetchingUriRef = useRef<string>('');
const originalDocumentModelRef = useRef<IEditorDocumentModelRef>();

const modifiedUriStr = useMemo(() => parseUri(modifiedUri, appConfig.workspaceDir), [modifiedUri])
const modifiedFetchingUriRef = useRef<string>('');
const modifiedDocumentModelRef = useRef<IEditorDocumentModelRef>();

const openDocumentModel = () => {
if (editorRef.current && originalDocumentModelRef.current && modifiedDocumentModelRef.current) {
editorRef.current.compare(originalDocumentModelRef.current, modifiedDocumentModelRef.current)
}
}

React.useEffect(() => {
if (containerRef.current) {
editorRef.current?.dispose();
editorRef.current = editorCollectionService.createDiffEditor(containerRef.current, {
automaticLayout: true,
...editorOptions,
});
onEditorCreateMemorizeFn(editorRef.current)
openDocumentModel()
}
return () => {
unmountRef.current = true;
editorRef.current?.dispose()
originalDocumentModelRef.current?.dispose()
modifiedDocumentModelRef.current?.dispose()
};
}, []);

useEffect(() => {
const createModelReference = (
fetchingUriRef: MutableRefObject<string>,
uriStr: string,
documentModelRef: MutableRefObject<IEditorDocumentModelRef | undefined>
) => {
if (fetchingUriRef.current !== uriStr) {
fetchingUriRef.current = uriStr
documentService.createModelReference(new URI(uriStr), 'diff-editor-react-component').then((ref) => {
if (documentModelRef.current) {
documentModelRef.current.dispose();
}
if (!unmountRef.current && ref.instance.uri.toString() === uriStr) {
documentModelRef.current = ref;
openDocumentModel()
} else {
ref.dispose();
}
});
}
}
createModelReference(originalFetchingUriRef, originalUriStr, originalDocumentModelRef)
createModelReference(modifiedFetchingUriRef, modifiedUriStr, modifiedDocumentModelRef)
}, [originalUriStr, modifiedUriStr])

return <div ref={containerRef} {...props}></div>;
};

export const DiffEditor = (props: ICodeEditorProps) => {
const appContext = useContext(AppContext)
if (!appContext.app) return null
return (
<ConfigProvider value={appContext.app.config}>
<DiffEditorComponent {...props} />
</ConfigProvider>
)
};
5 changes: 4 additions & 1 deletion packages/core/src/core/components/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createContext } from 'react'
import { ClientApp } from '@codeblitzjs/ide-sumi-core'

export const AppContext = createContext<{ app: ClientApp | null }>({ app: null })
export const AppContext = createContext<{
app: ClientApp | null,
startState?: { status: 'loading' | 'success' | 'error', error?: string }
}>({ app: null })
14 changes: 14 additions & 0 deletions packages/core/src/core/components/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { URI } from '@opensumi/ide-core-browser';
import * as path from 'path'

export const parseUri = (uriInput: URI | string, workspaceDir: string) => {
if (typeof uriInput === 'string') {
let uri = URI.parse(uriInput)
// 说明传的是路径
if (uri.scheme === 'file' && !uriInput.startsWith('file:')) {
uri = uri.withPath(path.join(workspaceDir, uri.codeUri.path))
}
return uri.toString();
}
return uriInput.toString()
}
8 changes: 7 additions & 1 deletion packages/core/src/core/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injector } from '@opensumi/di';
import { useRef } from 'react';
import { useRef, useMemo, useCallback } from 'react';
import { IAppInstance } from '../api/types';

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

export function useMemorizeFn<T extends (...args: any[]) => any>(fn: T) {
const fnRef = useRef<T>(fn);
fnRef.current = useMemo(() => fn, [fn]);
return useCallback((...args: any) => fnRef.current(...args), []) as T;
}

export let singleInjector: Injector | null = null;

export function setSingleInjector(inject) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ThemeType } from '@opensumi/ide-theme';
import { ComponentType } from 'react';

export interface LandingProps {
status: 'loading' | 'success' | 'error' | 'pending';
status: 'loading' | 'success' | 'error';
error?: string;
theme?: ThemeType;
className?: string;
Expand Down
24 changes: 23 additions & 1 deletion packages/startup/src/provider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AppProvider, CodeEditor } from '@codeblitzjs/ide-core';
import { AppProvider, CodeEditor, DiffEditor } from '@codeblitzjs/ide-core';
import React from 'react';
import { createRoot } from 'react-dom/client';
import '@codeblitzjs/ide-core/languages';
import { SampleModule } from './module'
import '../index.css';
import './index.css'

Expand All @@ -10,6 +11,7 @@ const App = () => (
appConfig={{
workspaceDir: 'my-workspace',
layoutConfig: {},
modules: [SampleModule],
}}
runtimeConfig={{
biz: 'startup',
Expand All @@ -34,10 +36,30 @@ const App = () => (
<CodeEditor
uri="main.js"
style={{ width: 1000, height: 300, marginBottom: 16 }}
editorOptions={{
scrollbar: {
alwaysConsumeMouseWheel: false
}
}}
/>
<CodeEditor
uri="main.css"
style={{ width: 1000, height: 300 }}
editorOptions={{
scrollbar: {
alwaysConsumeMouseWheel: false
}
}}
/>
<DiffEditor
originalUri="sample:/a1.js"
modifiedUri="sample:/a2.js"
editorOptions={{
scrollbar: {
alwaysConsumeMouseWheel: false
}
}}
style={{ width: 1000, height: 300 }}
/>
</AppProvider>
);
Expand Down
48 changes: 48 additions & 0 deletions packages/startup/src/provider/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Autowired, Injectable, Provider } from '@opensumi/di';
import { BrowserModule } from '@opensumi/ide-core-browser';
import { IEditorDocumentModelContentProvider, BrowserEditorContribution, IEditorDocumentModelContentRegistry } from '@opensumi/ide-editor/lib/browser'
import { URI, Emitter, Event, Domain } from '@opensumi/ide-core-common'

const contentMap = {
'a1.js': `const add = (x, y) => {
return x + y
}
`,
'a2.js': `const add = (x, y) => {
return x + y + 1
}
`,
}

@Injectable()
export class SampleSchemeDocumentProvider implements IEditorDocumentModelContentProvider {
handlesScheme(scheme: string) {
return scheme === 'sample';
}

async provideEditorDocumentModelContent(uri: URI): Promise<string> {
return contentMap[uri.codeUri.path.slice(1)]
}

isReadonly() {
return true;
}

private _onDidChangeContent: Emitter<URI> = new Emitter();
onDidChangeContent: Event<URI> = this._onDidChangeContent.event;
}

@Domain(BrowserEditorContribution)
class SampleContribution implements BrowserEditorContribution {
@Autowired(SampleSchemeDocumentProvider)
private readonly sampleSchemeDocumentProvider: SampleSchemeDocumentProvider;

registerEditorDocumentModelContentProvider(registry: IEditorDocumentModelContentRegistry): void {
registry.registerEditorDocumentModelContentProvider(this.sampleSchemeDocumentProvider)
}
}

@Injectable()
export class SampleModule extends BrowserModule {
providers: Provider[] = [SampleContribution];
}

0 comments on commit 9c32255

Please sign in to comment.