open-webui/src/lib/components/common/CodeEditor.svelte

335 lines
8.1 KiB
Svelte
Raw Normal View History

2024-06-10 22:24:26 +00:00
<script lang="ts">
import { basicSetup, EditorView } from 'codemirror';
import { keymap, placeholder } from '@codemirror/view';
2024-06-10 23:02:23 +00:00
import { Compartment, EditorState } from '@codemirror/state';
2024-06-10 22:24:26 +00:00
import { acceptCompletion } from '@codemirror/autocomplete';
import { indentWithTab } from '@codemirror/commands';
2024-12-02 10:08:02 +00:00
import { indentUnit, LanguageDescription } from '@codemirror/language';
2024-10-05 19:38:09 +00:00
import { languages } from '@codemirror/language-data';
2024-06-10 22:24:26 +00:00
import { oneDark } from '@codemirror/theme-one-dark';
2025-06-08 16:27:08 +00:00
import { onMount, createEventDispatcher, getContext, tick, onDestroy } from 'svelte';
2024-10-05 19:38:09 +00:00
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
2024-06-11 00:12:48 +00:00
import { formatPythonCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner';
import { user } from '$lib/stores';
2024-06-11 00:12:48 +00:00
const dispatch = createEventDispatcher();
2024-06-24 19:26:07 +00:00
const i18n = getContext('i18n');
2024-06-10 22:24:26 +00:00
2024-06-10 23:35:42 +00:00
export let boilerplate = '';
2024-06-10 22:24:26 +00:00
export let value = '';
2025-02-22 09:16:58 +00:00
export let onSave = () => {};
export let onChange = () => {};
2024-10-05 19:04:36 +00:00
let _value = '';
$: if (value) {
updateValue();
}
const updateValue = () => {
2024-10-06 22:30:16 +00:00
if (_value !== value) {
2025-03-09 23:49:26 +00:00
const changes = findChanges(_value, value);
2024-10-06 22:30:16 +00:00
_value = value;
2025-03-09 23:49:26 +00:00
if (codeEditor && changes.length > 0) {
codeEditor.dispatch({ changes });
2024-10-06 22:30:16 +00:00
}
2024-10-05 19:04:36 +00:00
}
};
2025-03-09 23:49:26 +00:00
/**
* Finds multiple diffs in two strings and generates minimal change edits.
*/
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++;
2025-03-09 23:49:26 +00:00
}
// If equal, nothing to change
if (oldStr === newStr) return [];
// Find the end of the difference by comparing backwards
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)
}
];
2025-03-09 23:49:26 +00:00
}
2024-10-05 19:04:36 +00:00
export let id = '';
export let lang = '';
2024-06-10 22:24:26 +00:00
2024-06-10 23:02:23 +00:00
let codeEditor;
2025-02-25 04:19:32 +00:00
export const focus = () => {
codeEditor.focus();
};
2024-06-10 23:02:23 +00:00
let isDarkMode = false;
let editorTheme = new Compartment();
2024-10-05 19:38:09 +00:00
let editorLanguage = new Compartment();
2024-06-10 23:02:23 +00:00
2024-12-02 10:08:02 +00:00
languages.push(
LanguageDescription.of({
name: 'HCL',
extensions: ['hcl', 'tf'],
load() {
return import('codemirror-lang-hcl').then((m) => m.hcl());
}
})
);
languages.push(
LanguageDescription.of({
name: 'Elixir',
extensions: ['ex', 'exs'],
load() {
return import('codemirror-lang-elixir').then((m) => m.elixir());
}
})
);
2024-10-05 19:38:09 +00:00
const getLang = async () => {
const language = languages.find((l) => l.alias.includes(lang));
return await language?.load();
2024-10-05 19:04:36 +00:00
};
let pyodideWorkerInstance = null;
const getPyodideWorker = () => {
if (!pyodideWorkerInstance) {
pyodideWorkerInstance = new PyodideWorker(); // Your worker constructor
}
return pyodideWorkerInstance;
};
// Generate unique IDs for requests
let _formatReqId = 0;
const formatPythonCodePyodide = (code) => {
return new Promise((resolve, reject) => {
const id = `format-${++_formatReqId}`;
let timeout;
const worker = getPyodideWorker();
2025-07-29 13:08:47 +00:00
const startTag = `--||CODE-START-${id}||--`;
const endTag = `--||CODE-END-${id}||--`;
const script = `
import black
2025-07-29 13:08:47 +00:00
print("${startTag}")
print(black.format_str("""${code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/"/g, '\\"')}""", mode=black.Mode()))
2025-07-29 13:08:47 +00:00
print("${endTag}")
`;
const packages = ['black'];
function handleMessage(event) {
const { id: eventId, stdout, stderr } = event.data;
if (eventId !== id) return; // Only handle our message
clearTimeout(timeout);
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
if (stderr) {
reject(stderr);
} else {
2025-07-29 13:08:47 +00:00
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);
// Send to worker
worker.postMessage({ id, code: script, packages });
// Timeout
timeout = setTimeout(() => {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
try {
worker.terminate();
} catch {}
pyodideWorkerInstance = null;
reject('Execution Time Limit Exceeded');
}, 60000);
});
};
2024-06-11 00:30:07 +00:00
export const formatPythonCodeHandler = async () => {
2024-06-11 00:12:48 +00:00
if (codeEditor) {
const res = await (
$user?.role === 'admin'
? formatPythonCode(localStorage.token, _value)
: formatPythonCodePyodide(_value)
).catch((error) => {
2025-01-21 06:41:32 +00:00
toast.error(`${error}`);
2024-06-11 00:12:48 +00:00
return null;
});
if (res && res.code) {
const formattedCode = res.code;
codeEditor.dispatch({
changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }]
});
2024-06-11 00:24:42 +00:00
2024-10-06 19:07:45 +00:00
_value = formattedCode;
2025-02-24 05:39:34 +00:00
onChange(_value);
2024-10-06 19:07:45 +00:00
await tick();
2024-06-24 16:09:45 +00:00
toast.success($i18n.t('Code formatted successfully'));
2024-06-11 00:12:48 +00:00
return true;
}
return false;
}
2024-06-11 02:19:53 +00:00
return false;
2024-06-11 00:12:48 +00:00
};
2024-06-10 23:02:23 +00:00
let extensions = [
basicSetup,
keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
2024-06-11 00:12:48 +00:00
indentUnit.of(' '),
2024-06-10 23:02:23 +00:00
placeholder('Enter your code here...'),
EditorView.updateListener.of((e) => {
if (e.docChanged) {
2024-10-05 19:04:36 +00:00
_value = e.state.doc.toString();
2025-02-24 05:39:34 +00:00
onChange(_value);
2024-06-10 23:02:23 +00:00
}
}),
2024-10-05 19:38:09 +00:00
editorTheme.of([]),
editorLanguage.of([])
2024-06-10 23:02:23 +00:00
];
2024-10-05 19:38:09 +00:00
$: if (lang) {
setLanguage();
}
const setLanguage = async () => {
const language = await getLang();
2024-11-06 08:32:08 +00:00
if (language && codeEditor) {
2024-10-05 19:38:09 +00:00
codeEditor.dispatch({
effects: editorLanguage.reconfigure(language)
});
}
};
2024-06-10 22:24:26 +00:00
onMount(() => {
2024-06-11 04:53:51 +00:00
console.log(value);
if (value === '') {
value = boilerplate;
}
2024-06-11 00:24:42 +00:00
2024-10-05 19:04:36 +00:00
_value = value;
2024-06-10 23:02:23 +00:00
// Check if html class has dark mode
isDarkMode = document.documentElement.classList.contains('dark');
2024-06-10 22:24:26 +00:00
// python code editor, highlight python code
2024-06-10 23:02:23 +00:00
codeEditor = new EditorView({
2024-06-10 22:24:26 +00:00
state: EditorState.create({
2024-10-05 19:04:36 +00:00
doc: _value,
2024-06-10 23:02:23 +00:00
extensions: extensions
2024-06-10 22:24:26 +00:00
}),
2024-10-05 19:04:36 +00:00
parent: document.getElementById(`code-textarea-${id}`)
2024-06-10 22:24:26 +00:00
});
2024-06-10 23:02:23 +00:00
if (isDarkMode) {
codeEditor.dispatch({
effects: editorTheme.reconfigure(oneDark)
});
}
// listen to html class changes this should fire only when dark mode is toggled
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']
});
2024-06-11 05:33:25 +00:00
const keydownHandler = async (e) => {
2024-06-11 00:12:48 +00:00
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
2025-02-22 09:16:58 +00:00
onSave();
2024-06-11 00:12:48 +00:00
}
2024-06-11 05:33:25 +00:00
// Format code when Ctrl + Shift + F is pressed
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') {
e.preventDefault();
await formatPythonCodeHandler();
}
2024-06-11 00:12:48 +00:00
};
2024-06-11 05:33:25 +00:00
document.addEventListener('keydown', keydownHandler);
2024-06-11 00:12:48 +00:00
2024-06-10 23:02:23 +00:00
return () => {
observer.disconnect();
2024-06-11 05:33:25 +00:00
document.removeEventListener('keydown', keydownHandler);
2024-06-10 23:02:23 +00:00
};
2024-06-10 22:24:26 +00:00
});
2025-06-08 16:27:08 +00:00
onDestroy(() => {
if (pyodideWorkerInstance) {
pyodideWorkerInstance.terminate();
}
});
2024-06-10 22:24:26 +00:00
</script>
<div id="code-textarea-{id}" class="h-full w-full text-sm" />