mtcute-repl/packages/repl/src/components/editor/Editor.tsx
2025-01-15 07:59:05 +03:00

148 lines
4 KiB
TypeScript

import { useColorModeValue } from '@kobalte/core'
import { editor as mEditor, Uri } from 'monaco-editor'
import { createEffect, on, onMount } from 'solid-js'
import { $activeTab, $tabs, type EditorTab } from '../../store/tabs.ts'
import { useStore } from '../../store/use-store.ts'
import { setupMonaco } from './utils/setup.ts'
import './Editor.css'
export interface EditorProps {
class?: string
}
const DEFAULT_CODE = `
/**
* This playground comes pre-loaded with all @mtcute/* libraries,
* as well as a pre-configured Telegram client (available as \`tg\` global variable).
*
* Exports from this file will become available in the REPL.
*/
export const self = await tg.getMe()
console.log(self)
`.trimStart()
function findChangedTab(a: EditorTab[], b: EditorTab[]) {
const set = new Set(a.map(tab => tab.id))
for (const tab of b) {
if (!set.has(tab.id)) return tab
}
return null
}
export default function Editor(props: EditorProps) {
const tabs = useStore($tabs)
const activeTab = useStore($activeTab)
let ref!: HTMLDivElement
let editor: mEditor.IStandaloneCodeEditor | undefined
const monacoTheme = useColorModeValue('latte', 'mocha')
// const monacoTheme = () => scheme() === 'dark' ? 'ayu-dark' : 'ayu-light'
const modelsByTab = new Map<string, mEditor.ITextModel>()
onMount(async () => {
editor = mEditor.create(ref, {
model: null,
automaticLayout: true,
minimap: {
enabled: false,
},
scrollbar: {
verticalScrollbarSize: 8,
},
lightbulb: {
enabled: 'onCode' as any,
},
quickSuggestions: {
other: true,
comments: true,
strings: true,
},
padding: { top: 8 },
acceptSuggestionOnCommitCharacter: true,
acceptSuggestionOnEnter: 'on',
accessibilitySupport: 'on',
inlayHints: {
enabled: 'on',
},
lineNumbersMinChars: 3,
theme: monacoTheme(),
scrollBeyondLastLine: false,
})
await setupMonaco()
for (const tab of tabs()) {
const model = mEditor.createModel(tab.main ? DEFAULT_CODE : '', 'typescript', Uri.parse(`file:///${tab.id}.ts`))
modelsByTab.set(tab.id, model)
}
editor.setModel(modelsByTab.get(activeTab())!)
// editor.onDidChangeModelContent(() => {
// props.onCodeChange(editor?.getValue() ?? '')
// })
return () => editor?.dispose()
})
createEffect(on(() => monacoTheme(), (theme) => {
if (!editor) return
editor.updateOptions({ theme })
}))
createEffect(on(activeTab, (tabId) => {
if (!editor) return
const model = modelsByTab.get(tabId)
if (!model) return
editor.setModel(model)
}))
createEffect(on(tabs, (tabs, prevTabs) => {
if (!editor || !prevTabs) return
if (tabs.length === prevTabs.length) {
// verify filenames
for (const tab of tabs) {
const oldName = prevTabs.find(prevTab => prevTab.id === tab.id)?.fileName
if (!oldName) continue // weird flex but ok
if (oldName === tab.fileName) continue
// renamed
const oldModel = modelsByTab.get(tab.id)
if (!oldModel) continue
const newModel = mEditor.createModel(oldModel.getValue(), 'typescript', Uri.parse(`file:///${tab.fileName}`))
modelsByTab.set(tab.id, newModel)
if (editor.getModel() === oldModel) {
editor.setModel(newModel)
}
oldModel.dispose()
}
return
}
if (tabs.length > prevTabs.length) {
// a tab was created
const changed = findChangedTab(prevTabs, tabs)
if (!changed) return
const model = mEditor.createModel('', 'typescript', Uri.parse(`file:///${changed.fileName}`))
modelsByTab.set(changed.id, model)
editor.setModel(model)
} else {
// a tab was deleted
const changed = findChangedTab(tabs, prevTabs)
if (!changed) return
modelsByTab.delete(changed.id)
}
}))
return (
<div
data-monaco-root
class={props.class}
ref={ref}
/>
)
}