diff --git a/packages/core/src/api/exports.ts b/packages/core/src/api/exports.ts index 90582a0e..03242bec 100644 --- a/packages/core/src/api/exports.ts +++ b/packages/core/src/api/exports.ts @@ -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'; diff --git a/packages/core/src/api/renderApp.tsx b/packages/core/src/api/renderApp.tsx index 132e0c0b..07f80af5 100644 --- a/packages/core/src/api/renderApp.tsx +++ b/packages/core/src/api/renderApp.tsx @@ -136,7 +136,7 @@ export const AppRenderer: React.FC = ({ onLoad, Landing, ...o export const AppProvider: React.FC> = ({ onLoad, children, ...opts }) => { const app = useConstant(() => { - opts.appConfig.layoutComponent = () => + opts.appConfig.layoutComponent = () => null; return createApp(opts) }); const [clientApp, setClientApp] = useState(null); @@ -150,7 +150,7 @@ export const AppProvider: React.FC> = const [state, setState] = useState<{ status: RootProps['status']; error?: RootProps['error']; - }>(() => ({ status: 'pending' })); + }>(() => ({ status: 'loading' })); useMemo(() => { app.injector.addProviders({ @@ -191,7 +191,7 @@ export const AppProvider: React.FC> = }; }, []); - const contextValue = useMemo(() => ({ app: clientApp }), [clientApp]) + const contextValue = useMemo(() => ({ app: clientApp, startState: state }), [clientApp, state]) return ( diff --git a/packages/core/src/core/components/CodeEditor.tsx b/packages/core/src/core/components/CodeEditor.tsx index 3c36ed86..494f1276 100644 --- a/packages/core/src/core/components/CodeEditor.tsx +++ b/packages/core/src/core/components/CodeEditor.tsx @@ -1,4 +1,4 @@ -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'; @@ -6,15 +6,11 @@ import { ConfigProvider, AppConfig } from '@opensumi/ide-core-browser/lib/react- 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 any>(fn: T) { - const fnRef = useRef(fn); - fnRef.current = useMemo(() => fn, [fn]); - return useCallback((...args: any) => fnRef.current(...args), []) as T; -} - export interface ICodeEditorProps extends React.HTMLAttributes { uri: URI | string; @@ -29,12 +25,7 @@ export const CodeEditorComponent = ({ uri, editorOptions, onEditorCreate, ...pro const appConfig: AppConfig = useInjectable(AppConfig); const containerRef = React.useRef(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(''); const documentModelRef = useRef(); const editorRef = useRef() diff --git a/packages/core/src/core/components/DiffEditor.tsx b/packages/core/src/core/components/DiffEditor.tsx new file mode 100644 index 00000000..5a04f998 --- /dev/null +++ b/packages/core/src/core/components/DiffEditor.tsx @@ -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 { + 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(null); + const editorRef = useRef() + const unmountRef = useRef(false); + const onEditorCreateMemorizeFn = useMemorizeFn(onEditorCreate || noop) + + const originalUriStr = useMemo(() => parseUri(originalUri, appConfig.workspaceDir), [originalUri]) + const originalFetchingUriRef = useRef(''); + const originalDocumentModelRef = useRef(); + + const modifiedUriStr = useMemo(() => parseUri(modifiedUri, appConfig.workspaceDir), [modifiedUri]) + const modifiedFetchingUriRef = useRef(''); + const modifiedDocumentModelRef = useRef(); + + 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, + uriStr: string, + documentModelRef: MutableRefObject + ) => { + 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
; +}; + +export const DiffEditor = (props: ICodeEditorProps) => { + const appContext = useContext(AppContext) + if (!appContext.app) return null + return ( + + + + ) +}; diff --git a/packages/core/src/core/components/context.ts b/packages/core/src/core/components/context.ts index 6058e21b..aba58a17 100644 --- a/packages/core/src/core/components/context.ts +++ b/packages/core/src/core/components/context.ts @@ -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 }) diff --git a/packages/core/src/core/components/util.ts b/packages/core/src/core/components/util.ts new file mode 100644 index 00000000..48e61cca --- /dev/null +++ b/packages/core/src/core/components/util.ts @@ -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() +} diff --git a/packages/core/src/core/hooks.ts b/packages/core/src/core/hooks.ts index 689dd82f..d0554c9d 100644 --- a/packages/core/src/core/hooks.ts +++ b/packages/core/src/core/hooks.ts @@ -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 = (fn: () => T): T => { @@ -10,6 +10,12 @@ export const useConstant = (fn: () => T): T => { return valueRef.current.v; }; +export function useMemorizeFn any>(fn: T) { + const fnRef = useRef(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) { diff --git a/packages/core/src/core/types.ts b/packages/core/src/core/types.ts index cd925817..9c695177 100644 --- a/packages/core/src/core/types.ts +++ b/packages/core/src/core/types.ts @@ -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; diff --git a/packages/startup/src/provider/index.tsx b/packages/startup/src/provider/index.tsx index c49e3690..a2eb2a39 100644 --- a/packages/startup/src/provider/index.tsx +++ b/packages/startup/src/provider/index.tsx @@ -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' @@ -10,6 +11,7 @@ const App = () => ( appConfig={{ workspaceDir: 'my-workspace', layoutConfig: {}, + modules: [SampleModule], }} runtimeConfig={{ biz: 'startup', @@ -34,10 +36,30 @@ const App = () => ( + ); diff --git a/packages/startup/src/provider/module.ts b/packages/startup/src/provider/module.ts new file mode 100644 index 00000000..db166f6c --- /dev/null +++ b/packages/startup/src/provider/module.ts @@ -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 { + return contentMap[uri.codeUri.path.slice(1)] + } + + isReadonly() { + return true; + } + + private _onDidChangeContent: Emitter = new Emitter(); + onDidChangeContent: Event = 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]; +}