From 6bf25456513449a7b6fe8bc2e02bccd7dda541c7 Mon Sep 17 00:00:00 2001 From: sayomaki Date: Thu, 14 May 2026 00:45:16 +0800 Subject: [PATCH 1/7] feat: minimal monaco editor --- package.json | 2 + src/commons/editor/EditorContainer.tsx | 20 +++- src/commons/editor/MonacoEditor.tsx | 75 ++++++++++++++ .../editor/__tests__/EditorContainer.test.tsx | 98 +++++++++++++++++++ .../editor/__tests__/MonacoEditor.test.tsx | 96 ++++++++++++++++++ src/commons/editor/monaco/setupMonaco.ts | 51 ++++++++++ src/commons/featureFlags/publicFlags.ts | 2 + src/features/monaco/flagMonacoEditorEnable.ts | 10 ++ yarn.lock | 62 ++++++++++++ 9 files changed, 412 insertions(+), 4 deletions(-) create mode 100644 src/commons/editor/MonacoEditor.tsx create mode 100644 src/commons/editor/__tests__/EditorContainer.test.tsx create mode 100644 src/commons/editor/__tests__/MonacoEditor.test.tsx create mode 100644 src/commons/editor/monaco/setupMonaco.ts create mode 100644 src/features/monaco/flagMonacoEditorEnable.ts diff --git a/package.json b/package.json index a44a9923c0..0b66c0fa77 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@mantine/hooks": "^9.0.0", + "@monaco-editor/react": "^4.7.0", "@octokit/rest": "^22.0.0", "@reduxjs/toolkit": "^1.9.7", "@sentry/react": "^10.5.0", @@ -77,6 +78,7 @@ "lz-string": "^1.4.4", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-hast": "^13.0.0", + "monaco-editor": "^0.55.1", "normalize.css": "^8.0.1", "phaser": "~3.90.0", "query-string": "^9.0.0", diff --git a/src/commons/editor/EditorContainer.tsx b/src/commons/editor/EditorContainer.tsx index 07ad91db8b..142aa72f9f 100644 --- a/src/commons/editor/EditorContainer.tsx +++ b/src/commons/editor/EditorContainer.tsx @@ -1,11 +1,15 @@ import _ from 'lodash'; -import { useContext } from 'react'; +import { lazy, Suspense, useContext } from 'react'; +import { flagMonacoEditorEnable } from 'src/features/monaco/flagMonacoEditorEnable'; +import { useFeature } from '../featureFlags/useFeature'; import type { EditorTabState } from '../workspace/WorkspaceTypes'; import { WorkspaceSettingsContext } from '../WorkspaceSettingsContext'; import Editor, { type EditorProps, type EditorTabStateProps } from './Editor'; import EditorTabContainer from './tabs/EditorTabContainer'; +const MonacoEditor = lazy(() => import('./MonacoEditor')); + type OwnProps = { baseFilePath?: string; isFolderModeEnabled: boolean; @@ -34,13 +38,21 @@ export const convertEditorTabStateToProps = ( }; const createNormalEditorTab = - (editorProps: Omit) => + (editorProps: Omit, useMonacoEditor: boolean) => (editorTabStateProps: EditorTabStateProps) => { - return ; + const editorPropsWithTab = { ...editorProps, ...editorTabStateProps }; + return useMonacoEditor ? ( + + + + ) : ( + + ); }; const EditorContainer: React.FC = (props: EditorContainerProps) => { const [workspaceSettings] = useContext(WorkspaceSettingsContext)!; + const useMonacoEditor = useFeature(flagMonacoEditorEnable); const { baseFilePath, isFolderModeEnabled, @@ -52,7 +64,7 @@ const EditorContainer: React.FC = (props: EditorContainerP } = props; editorProps.editorBinding = workspaceSettings.editorBinding; - const createEditorTab = createNormalEditorTab(editorProps); + const createEditorTab = createNormalEditorTab(editorProps, useMonacoEditor); if (activeEditorTabIndex === null) { return ( diff --git a/src/commons/editor/MonacoEditor.tsx b/src/commons/editor/MonacoEditor.tsx new file mode 100644 index 0000000000..1d77409d84 --- /dev/null +++ b/src/commons/editor/MonacoEditor.tsx @@ -0,0 +1,75 @@ +import './monaco/setupMonaco'; + +import { Card } from '@blueprintjs/core'; +import MonacoReactEditor from '@monaco-editor/react'; +import { useCallback } from 'react'; + +import { EditorProps } from './Editor'; + +const languageByExtension: Record = { + c: 'c', + cc: 'cpp', + cpp: 'cpp', + css: 'css', + h: 'cpp', + html: 'html', + java: 'java', + js: 'javascript', + json: 'json', + jsx: 'javascript', + py: 'python', + ts: 'typescript', + tsx: 'typescript' +}; + +const getLanguage = ({ filePath, mode }: Pick): string => { + if (mode) { + return mode; + } + const extension = filePath?.split('.').pop()?.toLowerCase(); + return (extension && languageByExtension[extension]) || 'javascript'; +}; + +const MonacoEditor: React.FC = props => { + const handleChange = useCallback( + (value: string | undefined, event: unknown) => { + const newValue = value ?? ''; + props.handleEditorValueChange(props.editorTabIndex, newValue); + props.handleUpdateHasUnsavedChanges?.(true); + props.onChange?.(newValue, event); + }, + [props] + ); + + return ( + +
+ +
+
+ ); +}; + +export default MonacoEditor; diff --git a/src/commons/editor/__tests__/EditorContainer.test.tsx b/src/commons/editor/__tests__/EditorContainer.test.tsx new file mode 100644 index 0000000000..a3993b1780 --- /dev/null +++ b/src/commons/editor/__tests__/EditorContainer.test.tsx @@ -0,0 +1,98 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { mockInitialStore } from 'src/commons/mocks/StoreMocks'; +import { + defaultWorkspaceSettings, + WorkspaceSettingsContext +} from 'src/commons/WorkspaceSettingsContext'; +import { flagMonacoEditorEnable } from 'src/features/monaco/flagMonacoEditorEnable'; +import { vi } from 'vitest'; + +import EditorContainer, { EditorContainerProps } from '../EditorContainer'; + +vi.mock('../MonacoEditor', () => ({ + default: (props: any) => ( +