CodeEditor.svelte 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <script lang="ts">
  2. import { basicSetup, EditorView } from 'codemirror';
  3. import { keymap, placeholder } from '@codemirror/view';
  4. import { Compartment, EditorState } from '@codemirror/state';
  5. import { acceptCompletion } from '@codemirror/autocomplete';
  6. import { indentWithTab } from '@codemirror/commands';
  7. import { indentUnit } from '@codemirror/language';
  8. import { languages } from '@codemirror/language-data';
  9. import { oneDark } from '@codemirror/theme-one-dark';
  10. import { onMount, createEventDispatcher, getContext, tick } from 'svelte';
  11. import { formatPythonCode } from '$lib/apis/utils';
  12. import { toast } from 'svelte-sonner';
  13. const dispatch = createEventDispatcher();
  14. const i18n = getContext('i18n');
  15. export let boilerplate = '';
  16. export let value = '';
  17. let _value = '';
  18. $: if (value) {
  19. updateValue();
  20. }
  21. const updateValue = () => {
  22. if (_value !== value) {
  23. _value = value;
  24. if (codeEditor) {
  25. codeEditor.dispatch({
  26. changes: [{ from: 0, to: codeEditor.state.doc.length, insert: _value }]
  27. });
  28. }
  29. }
  30. };
  31. export let id = '';
  32. export let lang = '';
  33. let codeEditor;
  34. let isDarkMode = false;
  35. let editorTheme = new Compartment();
  36. let editorLanguage = new Compartment();
  37. const getLang = async () => {
  38. const language = languages.find((l) => l.alias.includes(lang));
  39. return await language?.load();
  40. };
  41. export const formatPythonCodeHandler = async () => {
  42. if (codeEditor) {
  43. const res = await formatPythonCode(_value).catch((error) => {
  44. toast.error(error);
  45. return null;
  46. });
  47. if (res && res.code) {
  48. const formattedCode = res.code;
  49. codeEditor.dispatch({
  50. changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }]
  51. });
  52. _value = formattedCode;
  53. dispatch('change', { value: _value });
  54. await tick();
  55. toast.success($i18n.t('Code formatted successfully'));
  56. return true;
  57. }
  58. return false;
  59. }
  60. return false;
  61. };
  62. let extensions = [
  63. basicSetup,
  64. keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
  65. indentUnit.of(' '),
  66. placeholder('Enter your code here...'),
  67. EditorView.updateListener.of((e) => {
  68. if (e.docChanged) {
  69. _value = e.state.doc.toString();
  70. dispatch('change', { value: _value });
  71. }
  72. }),
  73. editorTheme.of([]),
  74. editorLanguage.of([])
  75. ];
  76. $: if (lang) {
  77. setLanguage();
  78. }
  79. const setLanguage = async () => {
  80. const language = await getLang();
  81. if (language && codeEditor) {
  82. codeEditor.dispatch({
  83. effects: editorLanguage.reconfigure(language)
  84. });
  85. }
  86. };
  87. onMount(() => {
  88. console.log(value);
  89. if (value === '') {
  90. value = boilerplate;
  91. }
  92. _value = value;
  93. // Check if html class has dark mode
  94. isDarkMode = document.documentElement.classList.contains('dark');
  95. // python code editor, highlight python code
  96. codeEditor = new EditorView({
  97. state: EditorState.create({
  98. doc: _value,
  99. extensions: extensions
  100. }),
  101. parent: document.getElementById(`code-textarea-${id}`)
  102. });
  103. if (isDarkMode) {
  104. codeEditor.dispatch({
  105. effects: editorTheme.reconfigure(oneDark)
  106. });
  107. }
  108. // listen to html class changes this should fire only when dark mode is toggled
  109. const observer = new MutationObserver((mutations) => {
  110. mutations.forEach((mutation) => {
  111. if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
  112. const _isDarkMode = document.documentElement.classList.contains('dark');
  113. if (_isDarkMode !== isDarkMode) {
  114. isDarkMode = _isDarkMode;
  115. if (_isDarkMode) {
  116. codeEditor.dispatch({
  117. effects: editorTheme.reconfigure(oneDark)
  118. });
  119. } else {
  120. codeEditor.dispatch({
  121. effects: editorTheme.reconfigure()
  122. });
  123. }
  124. }
  125. }
  126. });
  127. });
  128. observer.observe(document.documentElement, {
  129. attributes: true,
  130. attributeFilter: ['class']
  131. });
  132. const keydownHandler = async (e) => {
  133. if ((e.ctrlKey || e.metaKey) && e.key === 's') {
  134. e.preventDefault();
  135. dispatch('save');
  136. }
  137. // Format code when Ctrl + Shift + F is pressed
  138. if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') {
  139. e.preventDefault();
  140. await formatPythonCodeHandler();
  141. }
  142. };
  143. document.addEventListener('keydown', keydownHandler);
  144. return () => {
  145. observer.disconnect();
  146. document.removeEventListener('keydown', keydownHandler);
  147. };
  148. });
  149. </script>
  150. <div id="code-textarea-{id}" class="h-full w-full" />