diff --git a/package-lock.json b/package-lock.json index ea66780197e60c6c22efa86d4ea45b8f6eceee91..cc0ef2a31e455dfe742776656d01386030eb6d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "highlight.js": "^11.11.1", "js-cookie": "^3.0.5", "lucide-svelte": "^0.479.0", + "monaco-editor": "^0.52.2", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "surrealdb": "^1.2.1", @@ -23,8 +24,10 @@ }, "devDependencies": { "@iconify/svelte": "^4.2.0", + "@monaco-editor/loader": "^1.5.0", "@playwright/test": "^1.51.0", "@sebastianwessel/surql-gen": "^2.7.1", + "@shikijs/monaco": "^3.2.1", "@skeletonlabs/skeleton": "3.0.0", "@skeletonlabs/skeleton-svelte": "1.0.0", "@sveltejs/adapter-auto": "^4.0.0", @@ -500,6 +503,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -656,6 +669,18 @@ "@shikijs/types": "3.2.1" } }, + "node_modules/@shikijs/monaco": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@shikijs/monaco/-/monaco-3.2.1.tgz", + "integrity": "sha512-9XaRuwETRRhi+4g1EdMsK1dx1mHuL1XnXWmDRFL2PkMrDIGqrzY9DGR+YnWlWuoEY0kU+vbCMxH7rog1yuWJvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.2.1", + "@shikijs/types": "3.2.1", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@shikijs/themes": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.2.1.tgz", @@ -4234,6 +4259,12 @@ "dev": true, "license": "MIT" }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "dev": true, @@ -5364,6 +5395,13 @@ "dev": true, "license": "MIT" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "dev": true, + "license": "MIT" + }, "node_modules/std-env": { "version": "3.8.0", "dev": true, diff --git a/package.json b/package.json index 7e72019e2c109b6a23431c2648fcddebf14fc858..80633da3a51159d90e8b2864b23e401483f56399 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ }, "devDependencies": { "@iconify/svelte": "^4.2.0", + "@monaco-editor/loader": "^1.5.0", "@playwright/test": "^1.51.0", "@sebastianwessel/surql-gen": "^2.7.1", + "@shikijs/monaco": "^3.2.1", "@skeletonlabs/skeleton": "3.0.0", "@skeletonlabs/skeleton-svelte": "1.0.0", "@sveltejs/adapter-auto": "^4.0.0", @@ -62,6 +64,7 @@ "highlight.js": "^11.11.1", "js-cookie": "^3.0.5", "lucide-svelte": "^0.479.0", + "monaco-editor": "^0.52.2", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "surrealdb": "^1.2.1", @@ -69,4 +72,4 @@ "ts-md5": "^1.3.1", "tslib": "^2.8.1" } -} \ No newline at end of file +} diff --git a/src/lib/components/CodeBlock.svelte b/src/lib/components/CodeBlock.svelte deleted file mode 100644 index 9cedffd4ec73eb2895dda1d1f79b0dd36c8f32b5..0000000000000000000000000000000000000000 --- a/src/lib/components/CodeBlock.svelte +++ /dev/null @@ -1,51 +0,0 @@ -<!-- @component Code Block based on: https://shiki.style/ --> - -<script module> - import { createHighlighterCoreSync } from 'shiki/core'; - import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; - // Themes - // https://shiki.style/themes - import themeDarkPlus from 'shiki/themes/dark-plus.mjs'; - // Languages - // https://shiki.style/languages - import console from 'shiki/langs/console.mjs'; - import html from 'shiki/langs/html.mjs'; - import css from 'shiki/langs/css.mjs'; - import js from 'shiki/langs/javascript.mjs'; - - // https://shiki.style/guide/sync-usage - const shiki = createHighlighterCoreSync({ - engine: createJavaScriptRegexEngine(), - // Implement your import theme. - themes: [themeDarkPlus], - // Implement your imported and supported languages. - langs: [console, html, css, js] - }); -</script> - -<script lang="ts"> - import type { CodeBlockProps } from './types'; - - let { - code = '', - lang = 'console', - theme = 'dark-plus', - // Base Style Props - base = ' overflow-hidden', - rounded = 'rounded-container', - shadow = '', - classes = '', - // Pre Style Props - preBase = '', - prePadding = '[&>pre]:p-4', - preClasses = '' - }: CodeBlockProps = $props(); - - // Shiki convert to HTML - const generatedHtml = shiki.codeToHtml(code, { lang, theme }); -</script> - -<div class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses}"> - <!-- Output Shiki's Generated HTML --> - {@html generatedHtml} -</div> diff --git a/src/lib/components/CodeBlock/CodeBlock.svelte b/src/lib/components/CodeBlock/CodeBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c763432f2c3fa00e6fba9dc76ee6cbc4a55e7048 --- /dev/null +++ b/src/lib/components/CodeBlock/CodeBlock.svelte @@ -0,0 +1,82 @@ +<!-- @component Code Block based on: https://shiki.style/ --> + +<script module> + import { createHighlighterCoreSync } from 'shiki/core'; + import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; + // Themes + // https://shiki.style/themes + import themeDarkPlus from 'shiki/themes/dark-plus.mjs'; + // Languages + // https://shiki.style/languages + import console from 'shiki/langs/console.mjs'; + import html from 'shiki/langs/html.mjs'; + import css from 'shiki/langs/css.mjs'; + import js from 'shiki/langs/javascript.mjs'; + + // https://shiki.style/guide/sync-usage + const shiki = createHighlighterCoreSync({ + engine: createJavaScriptRegexEngine(), + // Implement your import theme. + themes: [themeDarkPlus], + // Implement your imported and supported languages. + langs: [console, html, css, js] + }); + + const lineNumberTransformer = {}; +</script> + +<script lang="ts"> + import type { CodeBlockProps } from './types'; + + let { + code = '', + lang = 'console', + theme = 'dark-plus', + // Base Style Props + base = ' overflow-hidden', + rounded = 'rounded-container', + shadow = '', + classes = '', + // Pre Style Props + preBase = '', + prePadding = '[&>pre]:p-4', + preClasses = '' + }: CodeBlockProps = $props(); + + // Shiki convert to HTML + const generatedHtml = shiki.codeToHtml(code, { + lang, + theme, + transformers: [ + { + // code(node) { + // this.addClassToHast(node, 'language-js'); + // }, + line(node, line) { + // Create a span element for the line number + const lineNumberSpan: Element = { + type: 'element', + tagName: 'span', + properties: { + className: ['line-number'] + }, + children: [{ type: 'text', value: `${line}` }] + }; + + // Add the line number span at the beginning of the line's children + if (node.children) { + node.children.unshift(lineNumberSpan); + } + }, + span(node, line, col) { + node.properties['data-token'] = `token:${line}:${col}`; + } + } + ] + }); +</script> + +<div class="{base} {rounded} {shadow} {classes} {preBase} {prePadding} {preClasses}"> + <!-- Output Shiki's Generated HTML --> + {@html generatedHtml} +</div> diff --git a/src/lib/components/CodeBlock/transformers.ts b/src/lib/components/CodeBlock/transformers.ts new file mode 100644 index 0000000000000000000000000000000000000000..75a3e5b6015bde0ed06ab7fd17eea8a43eecb94f --- /dev/null +++ b/src/lib/components/CodeBlock/transformers.ts @@ -0,0 +1,21 @@ +import { codeToHtml } from 'shiki'; +import { type Element } from 'hast'; + +const code = await codeToHtml('foo\bar', { + lang: 'js', + theme: 'vitesse-dark', + transformers: [ + { + line(node, line) { + const lineNumberSpan: Element = { + type: 'element', + tagName: 'span', + properties: { + className: ['line-number'] + }, + children: [{ type: 'text', value: `${line}` }] + }; + } + } + ] +}); diff --git a/src/lib/components/CodeBlock/types.ts b/src/lib/components/CodeBlock/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..d086bc6cfe51e2d2767f07c5cc07fe9ac4690a97 --- /dev/null +++ b/src/lib/components/CodeBlock/types.ts @@ -0,0 +1,14 @@ +export interface CodeBlockProps { + code?: string; + lang?: 'console' | 'html' | 'css' | 'js'; + theme?: 'dark-plus'; + // Base Style Props + base?: string; + rounded?: string; + shadow?: string; + classes?: string; + // Pre Style Props + preBase?: string; + prePadding?: string; + preClasses?: string; +} diff --git a/src/lib/components/Editor/Editor.svelte b/src/lib/components/Editor/Editor.svelte new file mode 100644 index 0000000000000000000000000000000000000000..258ec3b30f2c01603499aa8fcac514dcd0bcf87b --- /dev/null +++ b/src/lib/components/Editor/Editor.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { onDestroy, onMount } from 'svelte'; + import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api'; + import { createEditor } from './editor'; + + const { code, lang = 'latex' }: { code: string; lang?: string } = $props(); + + let editor: Monaco.editor.IStandaloneCodeEditor; + let monaco: typeof Monaco; + let editorContainer: HTMLElement; + + let currentSelection: Monaco.Selection | null = null; + + function handleSelectionChange(selection: Monaco.Selection) { + currentSelection = selection; + console.log('Selection changed:', selection); + } + + onMount(async () => { + const result = await createEditor( + editorContainer, + { value: code, lang: lang }, + { + onSelectionChange: handleSelectionChange + } + ); + monaco = result?.monaco!; + editor = result?.editor!; + }); + + onDestroy(() => { + monaco?.editor.getModels().forEach((model) => model.dispose()); + editor?.dispose(); + }); +</script> + +<div> + <div class="container" bind:this={editorContainer} /> +</div> + +<style> + .container { + width: 100%; + height: 600px; + } +</style> diff --git a/src/lib/components/Editor/editor.ts b/src/lib/components/Editor/editor.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5932502de3d4edb8ddc97875009635b749a004c --- /dev/null +++ b/src/lib/components/Editor/editor.ts @@ -0,0 +1,60 @@ +import { browser } from '$app/environment'; +import loader from '@monaco-editor/loader'; +import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +export const createEditor = async ( + editorContainer: HTMLElement, + content: { value: string; lang: string }, + options: { onSelectionChange?: (selection: Monaco.Selection) => void } = {} +) => { + if (!browser) return; + + const monacoEditor = await import('monaco-editor'); + loader.config({ monaco: monacoEditor.default }); + + const monaco = await loader.init(); + + const editor = monaco.editor.create(editorContainer); + + editor.updateOptions({ + domReadOnly: true, + readOnly: true, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + lineNumbers: 'on', + scrollbar: { vertical: 'hidden' }, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + folding: false, + glyphMargin: false + }); + + const keepIds = ['editor.action.clipboardCopyAction']; + const contextmenu = editor.getContribution('editor.contrib.contextmenu')!; + const realMethod = contextmenu._getMenuActions; + contextmenu._getMenuActions = function () { + const items = realMethod.apply(contextmenu, arguments); + return items.filter(function (item) { + console.log(item.id); + return keepIds.includes(item.id) || item.id.includes('ICodeEditor'); + }); + }; + + editor.addAction({ + id: 'annotate', + label: 'Add Comment', + keybindings: undefined, + contextMenuGroupId: 'grady', + run: (editor) => { + const sel = editor.getSelection(); + if (options.onSelectionChange && sel !== null) { + options.onSelectionChange(sel); + } + } + }); + + const model = monaco.editor.createModel(content.value, content.lang); + editor.setModel(model); + + return { editor, monaco }; +}; diff --git a/src/routes/(app)/debug/editor/+page.svelte b/src/routes/(app)/debug/editor/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..186a0946b16655d397f354ad191b1fdf29d2edfa --- /dev/null +++ b/src/routes/(app)/debug/editor/+page.svelte @@ -0,0 +1,8 @@ +<script lang="ts"> + import Editor from '$lib/components/Editor/Editor.svelte'; +</script> + +<Editor /> + +<style> +</style> diff --git a/src/routes/(app)/submission/[id]/eval/+page.svelte b/src/routes/(app)/submission/[id]/eval/+page.svelte index 74d9c8b6f20706bd6659572fdd1be3231672836f..e724422075aaad2398093034b995b4dc718fcde1 100644 --- a/src/routes/(app)/submission/[id]/eval/+page.svelte +++ b/src/routes/(app)/submission/[id]/eval/+page.svelte @@ -1,5 +1,7 @@ <script lang="ts"> import { enhance } from '$app/forms'; + import Editor from '$lib/components/Editor/Editor.svelte'; + import CodeBlock from '../../../../../lib/components/CodeBlock/CodeBlock.svelte'; import type { PageData } from './$types'; const { data } = $props<{ data: PageData }>(); @@ -127,7 +129,9 @@ <div class="rounded-md border bg-gray-50 p-4"> <h2 class="mb-2 text-xl font-semibold">Submission Code</h2> - <pre class="h-64 overflow-auto rounded-md bg-gray-100 p-3 text-sm">{submissionCode}</pre> + <!--<pre class="h-64 overflow-auto rounded-md bg-gray-100 p-3 text-sm">{submissionCode}</pre>--> + <!-- <CodeBlock code={submissionCode} /> --> + <Editor code={submissionCode} lang={'python'} /> </div> <details class="rounded-md border bg-gray-50 p-4">