kt-template-online-playground/src/monaco/Monaco.vue
2026-05-08 14:30:56 +08:00

196 lines
4.3 KiB
Vue

<script lang="ts" setup>
import {
computed,
inject,
onBeforeUnmount,
onMounted,
onWatcherCleanup,
shallowRef,
useTemplateRef,
watch,
} from 'vue'
import * as monaco from 'monaco-editor-core'
import { initMonaco } from './env'
import { getOrCreateModel } from './utils'
import { type EditorMode, injectKeyProps } from '../types'
import { registerHighlighter } from './highlight'
const props = withDefaults(
defineProps<{
filename: string
value?: string
readonly?: boolean
mode?: EditorMode
}>(),
{
readonly: false,
value: '',
mode: undefined,
},
)
const emit = defineEmits<{
(e: 'change', value: string): void
}>()
const containerRef = useTemplateRef('container')
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor>()
const {
store,
autoSave,
theme: replTheme,
editorOptions,
} = inject(injectKeyProps)!
initMonaco(store.value)
const lang = computed(() => (props.mode === 'css' ? 'css' : 'javascript'))
let editorInstance: monaco.editor.IStandaloneCodeEditor
function emitChangeEvent() {
emit('change', editorInstance.getValue())
}
onMounted(() => {
const theme = registerHighlighter()
if (!containerRef.value) {
throw new Error('Cannot find containerRef')
}
editorInstance = monaco.editor.create(containerRef.value, {
...(props.readonly
? { value: props.value, language: lang.value }
: { model: null }),
fontSize: 13,
tabSize: 2,
theme: replTheme.value === 'light' ? theme.light : theme.dark,
readOnly: props.readonly,
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
inlineSuggest: {
enabled: false,
},
fixedOverflowWidgets: true,
...editorOptions.value.monacoOptions,
})
editor.value = editorInstance
// Support for semantic highlighting
const t = (editorInstance as any)._themeService._theme
t.semanticHighlighting = true
t.getTokenStyleMetadata = (
type: string,
modifiers: string[],
_language: string,
) => {
const _readonly = modifiers.includes('readonly')
switch (type) {
case 'function':
case 'method':
return { foreground: 12 }
case 'class':
return { foreground: 11 }
case 'variable':
case 'property':
return { foreground: _readonly ? 19 : 9 }
default:
return { foreground: 0 }
}
}
watch(
() => props.value,
(value) => {
if (editorInstance.getValue() === value) return
editorInstance.setValue(value || '')
},
{ immediate: true },
)
watch(lang, (lang) =>
monaco.editor.setModelLanguage(editorInstance.getModel()!, lang),
)
if (!props.readonly) {
watch(
() => props.filename,
(_, oldFilename) => {
if (!editorInstance) return
const file = store.value.files[props.filename]
if (!file) return null
const model = getOrCreateModel(
monaco.Uri.parse(`file:///${props.filename}`),
file.language,
file.code,
)
const oldFile = oldFilename ? store.value.files[oldFilename] : null
if (oldFile) {
oldFile.editorViewState = editorInstance.saveViewState()
}
editorInstance.setModel(model)
if (file.editorViewState) {
editorInstance.restoreViewState(file.editorViewState)
editorInstance.focus()
}
},
{ immediate: true },
)
}
editorInstance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
// ignore save event
})
watch(
autoSave,
(autoSave) => {
if (autoSave) {
const disposable =
editorInstance.onDidChangeModelContent(emitChangeEvent)
onWatcherCleanup(() => disposable.dispose())
}
},
{ immediate: true },
)
// update theme
watch(replTheme, (n) => {
editorInstance.updateOptions({
theme: n === 'light' ? theme.light : theme.dark,
})
})
})
onBeforeUnmount(() => {
editor.value?.dispose()
})
defineExpose({
getEditorIns: () => editor.value,
getMonacoEditor: () => monaco.editor,
})
</script>
<template>
<div
ref="container"
class="editor"
@keydown.ctrl.s.prevent="emitChangeEvent"
@keydown.meta.s.prevent="emitChangeEvent"
/>
</template>
<style>
.editor {
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
}
</style>