| 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 |