| <script lang="ts"> |
| import '$lib/utils/codemirror'; |
| |
| import { basicSetup, EditorView } from 'codemirror'; |
| import { keymap, placeholder } from '@codemirror/view'; |
| import { Compartment, EditorState } from '@codemirror/state'; |
| |
| import { acceptCompletion } from '@codemirror/autocomplete'; |
| import { indentWithTab } from '@codemirror/commands'; |
| |
| import { indentUnit } from '@codemirror/language'; |
| import { languages } from '@codemirror/language-data'; |
| |
| import { oneDark } from '@codemirror/theme-one-dark'; |
| |
| import { onMount, createEventDispatcher, getContext, tick, onDestroy } from 'svelte'; |
| |
| import PyodideWorker from '$lib/workers/pyodide.worker?worker'; |
| |
| import { formatPythonCode } from '$lib/apis/utils'; |
| import { toast } from 'svelte-sonner'; |
| import { user } from '$lib/stores'; |
| |
| const dispatch = createEventDispatcher(); |
| const i18n = getContext('i18n'); |
| |
| export let boilerplate = ''; |
| export let value = ''; |
| |
| export let onSave = () => {}; |
| export let onChange = () => {}; |
| |
| let _value = ''; |
| |
| $: if (value) { |
| updateValue(); |
| } |
| |
| const updateValue = () => { |
| if (_value !== value) { |
| const changes = findChanges(_value, value); |
| _value = value; |
| |
| if (codeEditor && changes.length > 0) { |
| codeEditor.dispatch({ changes }); |
| } |
| } |
| }; |
| |
| |
| |
| |
| function findChanges(oldStr: string, newStr: string) { |
| // Find the start of the difference |
| let start = 0; |
| while (start < oldStr.length && start < newStr.length && oldStr[start] === newStr[start]) { |
| start++; |
| } |
| |
| if (oldStr === newStr) return []; |
| |
| let endOld = oldStr.length, |
| endNew = newStr.length; |
| while (endOld > start && endNew > start && oldStr[endOld - 1] === newStr[endNew - 1]) { |
| endOld--; |
| endNew--; |
| } |
| return [ |
| { |
| from: start, |
| to: endOld, |
| insert: newStr.slice(start, endNew) |
| } |
| ]; |
| } |
| |
| export let id = ''; |
| export let lang = ''; |
| |
| let codeEditor: EditorView | null = null; |
| |
| export const focus = () => { |
| codeEditor?.focus(); |
| }; |
| |
| let isDarkMode = false; |
| let editorTheme = new Compartment(); |
| let editorLanguage = new Compartment(); |
| |
| const getLang = async () => { |
| const language = languages.find((l) => l.alias.includes(lang)); |
| return await language?.load(); |
| }; |
| |
| let pyodideWorkerInstance = null; |
| |
| const getPyodideWorker = () => { |
| if (!pyodideWorkerInstance) { |
| pyodideWorkerInstance = new PyodideWorker(); // Your worker constructor |
| } |
| return pyodideWorkerInstance; |
| }; |
| |
| |
| let _formatReqId = 0; |
| |
| const formatPythonCodePyodide = (code) => { |
| return new Promise((resolve, reject) => { |
| const id = `format-${++_formatReqId}`; |
| let timeout; |
| const worker = getPyodideWorker(); |
| |
| const startTag = `--||CODE-START-${id}||--`; |
| const endTag = `--||CODE-END-${id}||--`; |
| |
| const script = ` |
| import black |
| print("${startTag}") |
| print(black.format_str("""${code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/"/g, '\\"')}""", mode=black.Mode())) |
| print("${endTag}") |
| `; |
| |
| const packages = ['black']; |
| |
| function handleMessage(event) { |
| const { id: eventId, stdout, stderr } = event.data; |
| if (eventId !== id) return; |
| clearTimeout(timeout); |
| worker.removeEventListener('message', handleMessage); |
| worker.removeEventListener('error', handleError); |
| |
| if (stderr) { |
| reject(stderr); |
| } else { |
| function extractBetweenDelimiters(stdout, start, end) { |
| console.log('stdout', stdout); |
| const startIdx = stdout.indexOf(start); |
| const endIdx = stdout.indexOf(end, startIdx + start.length); |
| if (startIdx === -1 || endIdx === -1) return null; |
| return stdout.slice(startIdx + start.length, endIdx).trim(); |
| } |
| |
| const formatted = extractBetweenDelimiters( |
| stdout && typeof stdout === 'string' ? stdout : '', |
| startTag, |
| endTag |
| ); |
| |
| resolve({ code: formatted }); |
| } |
| } |
| |
| function handleError(event) { |
| clearTimeout(timeout); |
| worker.removeEventListener('message', handleMessage); |
| worker.removeEventListener('error', handleError); |
| reject(event.message || 'Pyodide worker error'); |
| } |
| |
| worker.addEventListener('message', handleMessage); |
| worker.addEventListener('error', handleError); |
| |
| |
| worker.postMessage({ id, code: script, packages }); |
| |
| |
| timeout = setTimeout(() => { |
| worker.removeEventListener('message', handleMessage); |
| worker.removeEventListener('error', handleError); |
| try { |
| worker.terminate(); |
| } catch {} |
| pyodideWorkerInstance = null; |
| reject('Execution Time Limit Exceeded'); |
| }, 60000); |
| }); |
| }; |
| |
| export const formatPythonCodeHandler = async () => { |
| if (codeEditor) { |
| const res = await ( |
| $user?.role === 'admin' |
| ? formatPythonCode(localStorage.token, _value) |
| : formatPythonCodePyodide(_value) |
| ).catch((error) => { |
| toast.error(`${error}`); |
| return null; |
| }); |
| if (res && res.code) { |
| const formattedCode = res.code; |
| codeEditor.dispatch({ |
| changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }] |
| }); |
| |
| _value = formattedCode; |
| onChange(_value); |
| await tick(); |
| |
| toast.success($i18n.t('Code formatted successfully')); |
| return true; |
| } |
| return false; |
| } |
| return false; |
| }; |
| |
| let extensions = [ |
| basicSetup, |
| keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]), |
| indentUnit.of(' '), |
| placeholder($i18n.t('Enter your code here...')), |
| EditorView.updateListener.of((e) => { |
| if (e.docChanged) { |
| _value = e.state.doc.toString(); |
| onChange(_value); |
| } |
| }), |
| editorTheme.of([]), |
| editorLanguage.of([]) |
| ]; |
| |
| $: if (lang) { |
| setLanguage(); |
| } |
| |
| const setLanguage = async () => { |
| const language = await getLang(); |
| if (language && codeEditor) { |
| codeEditor.dispatch({ |
| effects: editorLanguage.reconfigure(language) |
| }); |
| } |
| }; |
| |
| onMount(() => { |
| if (value === '') { |
| value = boilerplate; |
| } |
| |
| _value = value; |
| |
| |
| isDarkMode = document.documentElement.classList.contains('dark'); |
| |
| |
| codeEditor = new EditorView({ |
| state: EditorState.create({ |
| doc: _value, |
| extensions: extensions |
| }), |
| parent: document.getElementById(`code-textarea-${id}`) |
| }); |
| |
| if (isDarkMode) { |
| codeEditor.dispatch({ |
| effects: editorTheme.reconfigure(oneDark) |
| }); |
| } |
| |
| |
| const observer = new MutationObserver((mutations) => { |
| mutations.forEach((mutation) => { |
| if (mutation.type === 'attributes' && mutation.attributeName === 'class') { |
| const _isDarkMode = document.documentElement.classList.contains('dark'); |
| |
| if (_isDarkMode !== isDarkMode) { |
| isDarkMode = _isDarkMode; |
| if (_isDarkMode) { |
| codeEditor.dispatch({ |
| effects: editorTheme.reconfigure(oneDark) |
| }); |
| } else { |
| codeEditor.dispatch({ |
| effects: editorTheme.reconfigure() |
| }); |
| } |
| } |
| } |
| }); |
| }); |
| |
| observer.observe(document.documentElement, { |
| attributes: true, |
| attributeFilter: ['class'] |
| }); |
| |
| const keydownHandler = async (e) => { |
| if ((e.ctrlKey || e.metaKey) && e.key === 's') { |
| e.preventDefault(); |
| |
| onSave(); |
| } |
| |
| |
| if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') { |
| e.preventDefault(); |
| await formatPythonCodeHandler(); |
| } |
| }; |
| |
| document.addEventListener('keydown', keydownHandler); |
| |
| return () => { |
| observer.disconnect(); |
| document.removeEventListener('keydown', keydownHandler); |
| // Must destroy EditorView so CodeMirror releases internal DOMObserver and DOM refs |
| if (codeEditor) { |
| codeEditor.destroy(); |
| codeEditor = null; |
| } |
| }; |
| }); |
| |
| onDestroy(() => { |
| if (pyodideWorkerInstance) { |
| pyodideWorkerInstance.terminate(); |
| } |
| }); |
| </script> |
|
|
| <div id="code-textarea-{id}" class="h-full w-full text-sm" /> |
|
|