返回 slidev
Monaco.vue
根目录 / packages / client / builtin / Monaco.vue
1 <!--
2 Monaco Editor
3 (auto transformed, you don't need to use this component directly)
4
5 Usage:
6
7 ```ts {monaco}
8 const your_code = 'here'
9 ```
10
11 Learn more: https://sli.dev/guide/syntax.html#monaco-editor
12 -->
13
14 <script setup lang="ts">
15 import type { RawAtValue } from '@slidev/types'
16 import type * as monaco from 'monaco-editor'
17 import { debounce } from '@antfu/utils'
18 import { whenever } from '@vueuse/core'
19 import lz from 'lz-string'
20 import { computed, defineAsyncComponent, nextTick, onMounted, ref } from 'vue'
21 import { useNav } from '../composables/useNav'
22 import { useSlideContext } from '../context'
23 import { configs } from '../env'
24 import { makeId } from '../logic/utils'
25
26 const props = withDefaults(
27 defineProps<{
28 codeLz?: string
29 diffLz?: string
30 lang?: string
31 readonly?: boolean
32 lines?: boolean
33 lineNumbers?: 'on' | 'off' | 'relative' | 'interval'
34 height?: number | string // Posible values: 'initial', 'auto', '100%', '200px', etc.
35 editorOptions?: monaco.editor.IEditorOptions
36 ata?: boolean
37 runnable?: boolean
38 writable?: string
39 autorun?: boolean | 'once'
40 showOutputAt?: RawAtValue
41 outputHeight?: string
42 highlightOutput?: boolean
43 runnerOptions?: Record<string, unknown>
44 }>(),
45 {
46 codeLz: '',
47 lang: 'typescript',
48 readonly: false,
49 lines: configs.lineNumbers,
50 height: 'initial',
51 ata: true,
52 runnable: false,
53 autorun: true,
54 highlightOutput: true,
55 },
56 )
57
58 const CodeRunner = defineAsyncComponent(() => import('../internals/CodeRunner.vue').then(r => r.default))
59
60 const code = ref(lz.decompressFromBase64(props.codeLz).trimEnd())
61 const diff = props.diffLz && ref(lz.decompressFromBase64(props.diffLz).trimEnd())
62 const isWritable = computed(() => props.writable && !props.readonly && __DEV__)
63 const lineNumbers = props.lineNumbers ?? (props.lines ? 'on' : 'off')
64
65 const langMap: Record<string, string> = {
66 ts: 'typescript',
67 js: 'javascript',
68 }
69 const lang = langMap[props.lang] ?? props.lang
70 const extMap: Record<string, string> = {
71 typescript: 'mts',
72 javascript: 'mjs',
73 ts: 'mts',
74 js: 'mjs',
75 }
76 const ext = extMap[props.lang] ?? props.lang
77
78 const container = ref<HTMLDivElement>()
79
80 const contentHeight = ref(0)
81 const initialHeight = ref<number>()
82 const height = computed(() => {
83 if (props.height === 'auto')
84 return `${contentHeight.value}px`
85 if (props.height === 'initial')
86 return `${initialHeight.value}px`
87 return props.height
88 })
89
90 const loadTypes = ref<() => void>()
91 const { $page: thisSlideNo, $renderContext: renderContext } = useSlideContext()
92 const { currentSlideNo } = useNav()
93 const stopWatchTypesLoading = whenever(
94 () => Math.abs(thisSlideNo.value - currentSlideNo.value) <= 1 && loadTypes.value,
95 (loadTypes) => {
96 if (['slide', 'presenter'].includes(renderContext.value))
97 loadTypes()
98 else
99 setTimeout(loadTypes, 5000)
100 },
101 )
102
103 onMounted(async () => {
104 // Lazy load monaco, so it will be bundled in async chunk
105 const { default: setup } = await import('../setup/monaco')
106 const { ata, monaco, editorOptions } = await setup()
107 const model = monaco.editor.createModel(code.value, lang, monaco.Uri.parse(`file:///${makeId()}.${ext}`))
108 model.onDidChangeContent(() => code.value = model.getValue())
109 const commonOptions = {
110 automaticLayout: true,
111 readOnly: props.readonly,
112 lineNumbers,
113 minimap: { enabled: false },
114 overviewRulerBorder: false,
115 overviewRulerLanes: 0,
116 padding: { top: 10, bottom: 10 },
117 lineNumbersMinChars: 3,
118 bracketPairColorization: { enabled: false },
119 tabSize: 2,
120 fontSize: 11.5,
121 fontFamily: 'var(--slidev-code-font-family)',
122 scrollBeyondLastLine: false,
123 useInlineViewWhenSpaceIsLimited: false,
124 ...editorOptions,
125 ...props.editorOptions,
126 } satisfies monaco.editor.IStandaloneEditorConstructionOptions & monaco.editor.IDiffEditorConstructionOptions
127
128 let editableEditor: monaco.editor.IStandaloneCodeEditor
129 if (diff) {
130 const diffModel = monaco.editor.createModel(diff.value, lang, monaco.Uri.parse(`file:///${makeId()}.${ext}`))
131 diffModel.onDidChangeContent(() => code.value = model.getValue())
132 const editor = monaco.editor.createDiffEditor(container.value!, {
133 renderOverviewRuler: false,
134 ...commonOptions,
135 })
136 editor.setModel({
137 original: model,
138 modified: diffModel,
139 })
140 const originalEditor = editor.getOriginalEditor()
141 const modifiedEditor = editor.getModifiedEditor()
142 const onContentSizeChange = () => {
143 const newHeight = Math.max(originalEditor.getContentHeight(), modifiedEditor.getContentHeight()) + 4
144 initialHeight.value ??= newHeight
145 contentHeight.value = newHeight
146 nextTick(() => editor.layout())
147 }
148 originalEditor.onDidContentSizeChange(onContentSizeChange)
149 modifiedEditor.onDidContentSizeChange(onContentSizeChange)
150 editableEditor = modifiedEditor
151 }
152 else {
153 const editor = monaco.editor.create(container.value!, {
154 model,
155 lineDecorationsWidth: 0,
156 ...commonOptions,
157 })
158 editor.onDidContentSizeChange((e) => {
159 const newHeight = e.contentHeight + 4
160 initialHeight.value ??= newHeight
161 contentHeight.value = newHeight
162 nextTick(() => editableEditor.layout())
163 })
164
165 editableEditor = editor
166 }
167 loadTypes.value = () => {
168 stopWatchTypesLoading()
169 import('#slidev/monaco-types')
170 if (props.ata) {
171 ata(editableEditor.getValue())
172 editableEditor.onDidChangeModelContent(debounce(1000, () => {
173 ata(editableEditor.getValue())
174 }))
175 }
176 }
177 const originalLayoutContentWidget = editableEditor.layoutContentWidget.bind(editableEditor)
178 editableEditor.layoutContentWidget = (widget: any) => {
179 originalLayoutContentWidget(widget)
180 const id = widget.getId()
181 if (id === 'editor.contrib.resizableContentHoverWidget') {
182 widget._resizableNode.domNode.style.transform = widget._positionPreference === 1
183 ? /* ABOVE */ `translateY(calc(100% * (var(--slidev-slide-scale) - 1)))`
184 : /* BELOW */ `` // reset
185 }
186 }
187
188 editableEditor.addAction({
189 id: 'slidev-save',
190 label: 'Save',
191 keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
192 run: () => {
193 if (!isWritable.value || !import.meta.hot?.send) {
194 console.warn('[Slidev] this monaco editor is not writable, save action is ignored.')
195 return
196 }
197 import.meta.hot.send('slidev:monaco-write', {
198 file: props.writable!,
199 content: editableEditor.getValue(),
200 })
201 },
202 })
203
204 nextTick(() => monaco.editor.remeasureFonts())
205 setTimeout(() => monaco.editor.remeasureFonts(), 1000)
206 })
207 </script>
208
209 <template>
210 <div class="relative slidev-monaco-container">
211 <div class="relative slidev-monaco-container-inner" :style="{ height }">
212 <div ref="container" class="absolute inset-0.5" />
213 </div>
214 <CodeRunner
215 v-if="props.runnable"
216 v-model="code"
217 :lang="lang"
218 :autorun="props.autorun"
219 :show-output-at="props.showOutputAt"
220 :height="props.outputHeight"
221 :highlight-output="props.highlightOutput"
222 :runner-options="props.runnerOptions"
223 />
224 </div>
225 </template>
226
227 <style>
228 div[widgetid='messageoverlay'] {
229 transform: translateY(calc(100% * (var(--slidev-slide-scale) - 1)));
230 }
231
232 .slidev-monaco-container {
233 position: relative;
234 margin: var(--slidev-code-margin);
235 line-height: var(--slidev-code-line-height);
236 border-radius: var(--slidev-code-radius);
237 background: var(--slidev-code-background);
238 }
239
240 .slidev-monaco-container-inner {
241 padding: var(--slidev-code-padding);
242 }
243
244 .slidev-monaco-container .monaco-editor {
245 --monaco-monospace-font: var(--slidev-code-font-family);
246 --vscode-editor-background: var(--slidev-code-background);
247 --vscode-editorGutter-background: var(--slidev-code-background);
248 }
249
250 /** Revert styles */
251 .slidev-monaco-container .monaco-editor a {
252 border-bottom: none;
253 }
254
255 .slidev-monaco-container .monaco-editor a:hover {
256 border-bottom: none;
257 }
258 </style>
259
259 lines Plain Text