Rename ReactPlayground to Playground
This commit is contained in:
100
src/components/Playground/CodeEditor/Editor/ata.ts
Normal file
100
src/components/Playground/CodeEditor/Editor/ata.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { ATABootstrapConfig, setupTypeAcquisition } from '@typescript/ata'
|
||||
|
||||
type DelegateListener = Required<{
|
||||
[k in keyof ATABootstrapConfig['delegate']]: Set<NonNullable<ATABootstrapConfig['delegate'][k]>>
|
||||
}>
|
||||
|
||||
const createDelegate = (): DelegateListener => {
|
||||
return {
|
||||
receivedFile: new Set(),
|
||||
progress: new Set(),
|
||||
errorMessage: new Set(),
|
||||
finished: new Set(),
|
||||
started: new Set()
|
||||
}
|
||||
}
|
||||
|
||||
const delegateListener = createDelegate()
|
||||
|
||||
type InferSet<T> = T extends Set<infer U> ? U : never
|
||||
|
||||
export interface TypeHelper {
|
||||
dispose: () => void
|
||||
acquireType: (code: string) => void
|
||||
removeListener: <T extends keyof DelegateListener>(
|
||||
event: T,
|
||||
handler: InferSet<DelegateListener[T]>
|
||||
) => void
|
||||
addListener: <T extends keyof DelegateListener>(
|
||||
event: T,
|
||||
handler: InferSet<DelegateListener[T]>
|
||||
) => void
|
||||
}
|
||||
|
||||
export const createATA = async (): Promise<TypeHelper> => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const ts = await import('https://esm.sh/typescript@5.3.3')
|
||||
const ata = setupTypeAcquisition({
|
||||
projectName: 'monaco-ts',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
typescript: ts,
|
||||
logger: console,
|
||||
fetcher: (input, init) => {
|
||||
try {
|
||||
return fetch(input, init)
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
return new Promise(() => {})
|
||||
},
|
||||
delegate: {
|
||||
receivedFile: (code, path) => {
|
||||
delegateListener.receivedFile.forEach((fn) => fn(code, path))
|
||||
},
|
||||
started: () => {
|
||||
delegateListener.started.forEach((fn) => fn())
|
||||
},
|
||||
progress: (downloaded, estimatedTotal) => {
|
||||
delegateListener.progress.forEach((fn) => fn(downloaded, estimatedTotal))
|
||||
},
|
||||
finished: (files) => {
|
||||
delegateListener.finished.forEach((fn) => fn(files))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const acquireType = (code: string) => ata(code)
|
||||
|
||||
const addListener = <T extends keyof DelegateListener>(
|
||||
event: T,
|
||||
handler: InferSet<DelegateListener[T]>
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
delegateListener[event].add(handler)
|
||||
}
|
||||
|
||||
const removeListener = <T extends keyof DelegateListener>(
|
||||
event: T,
|
||||
handler: InferSet<DelegateListener[T]>
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
delegateListener[event].delete(handler)
|
||||
}
|
||||
|
||||
const dispose = () => {
|
||||
for (const key in delegateListener) {
|
||||
delegateListener[key as keyof DelegateListener].clear()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
acquireType,
|
||||
addListener,
|
||||
removeListener,
|
||||
dispose
|
||||
}
|
||||
}
|
||||
43
src/components/Playground/CodeEditor/Editor/editor.scss
Normal file
43
src/components/Playground/CodeEditor/Editor/editor.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.monaco-editor-light {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--border);
|
||||
|
||||
.jsx-tag-angle-bracket {
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.jsx-text {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.jsx-tag-name {
|
||||
color: #800000;
|
||||
}
|
||||
|
||||
.jsx-tag-attribute-key {
|
||||
color: #f00;
|
||||
}
|
||||
}
|
||||
|
||||
.monaco-editor-vs-dark {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--border);
|
||||
|
||||
.jsx-tag-angle-bracket {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.jsx-text {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.jsx-tag-name {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.jsx-tag-attribute-key {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
}
|
||||
109
src/components/Playground/CodeEditor/Editor/hooks.ts
Normal file
109
src/components/Playground/CodeEditor/Editor/hooks.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { editor, IPosition, Selection } from 'monaco-editor'
|
||||
import ScrollType = editor.ScrollType
|
||||
import { Monaco } from '@monaco-editor/react'
|
||||
import { getWorker, MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight'
|
||||
import { createATA, TypeHelper } from '@/components/Playground/CodeEditor/Editor/ata'
|
||||
|
||||
export const useEditor = () => {
|
||||
const doOpenEditor = (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
input: { options: { selection: Selection } }
|
||||
) => {
|
||||
const selection = input.options ? input.options.selection : null
|
||||
if (selection) {
|
||||
if (
|
||||
typeof selection.endLineNumber === 'number' &&
|
||||
typeof selection.endColumn === 'number'
|
||||
) {
|
||||
editor.setSelection(selection)
|
||||
editor.revealRangeInCenter(selection, ScrollType.Immediate)
|
||||
} else {
|
||||
const position: IPosition = {
|
||||
lineNumber: selection.startLineNumber,
|
||||
column: selection.startColumn
|
||||
}
|
||||
editor.setPosition(position)
|
||||
editor.revealPositionInCenter(position, ScrollType.Immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadJsxSyntaxHighlight = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
const monacoJsxSyntaxHighlight = new MonacoJsxSyntaxHighlight(getWorker(), monaco)
|
||||
const { highlighter, dispose } = monacoJsxSyntaxHighlight.highlighterBuilder({ editor })
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
highlighter()
|
||||
})
|
||||
highlighter()
|
||||
|
||||
return { highlighter, dispose }
|
||||
}
|
||||
|
||||
const autoLoadExtraLib = async (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
defaultValue: string,
|
||||
onWatch: (typeHelper: TypeHelper) => () => void
|
||||
) => {
|
||||
const typeHelper = await createATA()
|
||||
|
||||
onWatch(typeHelper)
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
typeHelper.acquireType(editor.getValue())
|
||||
})
|
||||
|
||||
const addLibraryToRuntime = (code: string, path: string) => {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
|
||||
}
|
||||
|
||||
typeHelper.addListener('receivedFile', addLibraryToRuntime)
|
||||
typeHelper.acquireType(defaultValue)
|
||||
|
||||
return typeHelper
|
||||
}
|
||||
|
||||
return {
|
||||
doOpenEditor,
|
||||
loadJsxSyntaxHighlight,
|
||||
autoLoadExtraLib
|
||||
}
|
||||
}
|
||||
|
||||
export const useTypesProgress = () => {
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [finished, setFinished] = useState(false)
|
||||
|
||||
const onWatch = (typeHelper: TypeHelper) => {
|
||||
const handleStarted = () => {
|
||||
setFinished(false)
|
||||
}
|
||||
typeHelper.addListener('started', handleStarted)
|
||||
|
||||
const handleProgress = (progress: number, total: number) => {
|
||||
setProgress(progress)
|
||||
setTotal(total)
|
||||
}
|
||||
typeHelper.addListener('progress', handleProgress)
|
||||
|
||||
const handleFinished = () => {
|
||||
setFinished(true)
|
||||
}
|
||||
typeHelper.addListener('progress', handleFinished)
|
||||
|
||||
return () => {
|
||||
typeHelper.removeListener('started', handleStarted)
|
||||
typeHelper.removeListener('progress', handleProgress)
|
||||
typeHelper.removeListener('finished', handleFinished)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
progress,
|
||||
total,
|
||||
finished,
|
||||
onWatch
|
||||
}
|
||||
}
|
||||
108
src/components/Playground/CodeEditor/Editor/index.tsx
Normal file
108
src/components/Playground/CodeEditor/Editor/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react'
|
||||
import { editor, Selection } from 'monaco-editor'
|
||||
import MonacoEditor, { Monaco } from '@monaco-editor/react'
|
||||
import '@/components/Playground/CodeEditor/Editor/editor.scss'
|
||||
import { IEditorOptions, IFiles, ITheme } from '@/components/Playground/shared'
|
||||
import { fileNameToLanguage } from '@/components/Playground/files'
|
||||
import { useEditor, useTypesProgress } from '@/components/Playground/CodeEditor/Editor/hooks'
|
||||
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig'
|
||||
|
||||
interface EditorProps {
|
||||
files?: IFiles
|
||||
selectedFileName?: string
|
||||
readonly?: boolean
|
||||
onChange?: (code: string | undefined) => void
|
||||
options?: IEditorOptions
|
||||
theme?: ITheme
|
||||
onJumpFile?: (fileName: string) => void
|
||||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({
|
||||
files = {},
|
||||
selectedFileName = '',
|
||||
readonly,
|
||||
theme,
|
||||
onChange,
|
||||
options,
|
||||
onJumpFile
|
||||
}) => {
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor>()
|
||||
const { doOpenEditor, loadJsxSyntaxHighlight, autoLoadExtraLib } = useEditor()
|
||||
const jsxSyntaxHighlightRef = useRef<{
|
||||
highlighter: (code?: string | undefined) => void
|
||||
dispose: () => void
|
||||
}>({
|
||||
highlighter: () => undefined,
|
||||
dispose: () => undefined
|
||||
})
|
||||
const { onWatch } = useTypesProgress()
|
||||
const file = files[selectedFileName] ?? { name: 'Untitled' }
|
||||
|
||||
const handleOnEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
editorRef.current = editor
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||
void editor.getAction('editor.action.formatDocument')?.run()
|
||||
})
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
jsx: monaco.languages.typescript.JsxEmit.Preserve,
|
||||
esModuleInterop: true
|
||||
})
|
||||
|
||||
files &&
|
||||
Object.entries(files).forEach(([key]) => {
|
||||
if (!monaco.editor.getModel(monaco.Uri.parse(`file:///${key}`))) {
|
||||
monaco.editor.createModel(
|
||||
files[key].value,
|
||||
fileNameToLanguage(key),
|
||||
monaco.Uri.parse(`file:///${key}`)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
editor['_codeEditorService'].doOpenEditor = function (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
input: { options: { selection: Selection }; resource: { path: string } }
|
||||
) {
|
||||
const path = input.resource.path
|
||||
if (!path.startsWith('/node_modules/')) {
|
||||
onJumpFile?.(path.replace('/', ''))
|
||||
doOpenEditor(editor, input)
|
||||
}
|
||||
}
|
||||
|
||||
jsxSyntaxHighlightRef.current = loadJsxSyntaxHighlight(editor, monaco)
|
||||
|
||||
void autoLoadExtraLib(editor, monaco, file.value, onWatch)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.focus()
|
||||
jsxSyntaxHighlightRef?.current?.highlighter?.()
|
||||
}, [file.name])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MonacoEditor
|
||||
theme={theme}
|
||||
path={file.name}
|
||||
className={`monaco-editor-${theme ?? 'light'}`}
|
||||
language={file.language}
|
||||
value={file.value}
|
||||
onChange={onChange}
|
||||
onMount={handleOnEditorDidMount}
|
||||
options={{
|
||||
...MonacoEditorConfig,
|
||||
...options,
|
||||
theme: undefined,
|
||||
readOnly: readonly
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Editor
|
||||
34
src/components/Playground/CodeEditor/Editor/monacoConfig.ts
Normal file
34
src/components/Playground/CodeEditor/Editor/monacoConfig.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { editor } from 'monaco-editor'
|
||||
|
||||
export const MonacoEditorConfig: editor.IStandaloneEditorConstructionOptions = {
|
||||
automaticLayout: true,
|
||||
cursorBlinking: 'smooth',
|
||||
fontLigatures: true,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
fontSize: 14,
|
||||
showDeprecated: true,
|
||||
showUnused: true,
|
||||
showFoldingControls: 'mouseover',
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
inlineSuggest: {
|
||||
enabled: false
|
||||
},
|
||||
fixedOverflowWidgets: true,
|
||||
smoothScrolling: true,
|
||||
smartSelect: {
|
||||
selectSubwords: true,
|
||||
selectLeadingAndTrailingWhitespace: true
|
||||
},
|
||||
tabSize: 2,
|
||||
overviewRulerBorder: false, // 不要滚动条的边框
|
||||
// 滚动条设置
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 6, // 竖滚动条
|
||||
horizontalScrollbarSize: 6 // 横滚动条
|
||||
}
|
||||
// lineNumbers: 'off', // 隐藏控制行号
|
||||
}
|
||||
Reference in New Issue
Block a user