CodeEditor.svelte 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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, LanguageDescription } 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. languages.push(
  38. LanguageDescription.of({
  39. name: 'HCL',
  40. extensions: ['hcl', 'tf'],
  41. load() {
  42. return import('codemirror-lang-hcl').then((m) => m.hcl());
  43. }
  44. })
  45. );
  46. const getLang = async () => {
  47. const language = languages.find((l) => l.alias.includes(lang));
  48. return await language?.load();
  49. };
  50. export const formatPythonCodeHandler = async () => {
  51. if (codeEditor) {
  52. const res = await formatPythonCode(_value).catch((error) => {
  53. toast.error(error);
  54. return null;
  55. });
  56. if (res && res.code) {
  57. const formattedCode = res.code;
  58. codeEditor.dispatch({
  59. changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }]
  60. });
  61. _value = formattedCode;
  62. dispatch('change', { value: _value });
  63. await tick();
  64. toast.success($i18n.t('Code formatted successfully'));
  65. return true;
  66. }
  67. return false;
  68. }
  69. return false;
  70. };
  71. let extensions = [
  72. basicSetup,
  73. keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]),
  74. indentUnit.of(' '),
  75. placeholder('Enter your code here...'),
  76. EditorView.updateListener.of((e) => {
  77. if (e.docChanged) {
  78. _value = e.state.doc.toString();
  79. dispatch('change', { value: _value });
  80. }
  81. }),
  82. editorTheme.of([]),
  83. editorLanguage.of([])
  84. ];
  85. $: if (lang) {
  86. setLanguage();
  87. }
  88. const setLanguage = async () => {
  89. const language = await getLang();
  90. if (language && codeEditor) {
  91. codeEditor.dispatch({
  92. effects: editorLanguage.reconfigure(language)
  93. });
  94. }
  95. };
  96. onMount(() => {
  97. console.log(value);
  98. if (value === '') {
  99. value = boilerplate;
  100. }
  101. _value = value;
  102. // Check if html class has dark mode
  103. isDarkMode = document.documentElement.classList.contains('dark');
  104. // python code editor, highlight python code
  105. codeEditor = new EditorView({
  106. state: EditorState.create({
  107. doc: _value,
  108. extensions: extensions
  109. }),
  110. parent: document.getElementById(`code-textarea-${id}`)
  111. });
  112. if (isDarkMode) {
  113. codeEditor.dispatch({
  114. effects: editorTheme.reconfigure(oneDark)
  115. });
  116. }
  117. // listen to html class changes this should fire only when dark mode is toggled
  118. const observer = new MutationObserver((mutations) => {
  119. mutations.forEach((mutation) => {
  120. if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
  121. const _isDarkMode = document.documentElement.classList.contains('dark');
  122. if (_isDarkMode !== isDarkMode) {
  123. isDarkMode = _isDarkMode;
  124. if (_isDarkMode) {
  125. codeEditor.dispatch({
  126. effects: editorTheme.reconfigure(oneDark)
  127. });
  128. } else {
  129. codeEditor.dispatch({
  130. effects: editorTheme.reconfigure()
  131. });
  132. }
  133. }
  134. }
  135. });
  136. });
  137. observer.observe(document.documentElement, {
  138. attributes: true,
  139. attributeFilter: ['class']
  140. });
  141. const keydownHandler = async (e) => {
  142. if ((e.ctrlKey || e.metaKey) && e.key === 's') {
  143. e.preventDefault();
  144. dispatch('save');
  145. }
  146. // Format code when Ctrl + Shift + F is pressed
  147. if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') {
  148. e.preventDefault();
  149. await formatPythonCodeHandler();
  150. }
  151. };
  152. document.addEventListener('keydown', keydownHandler);
  153. return () => {
  154. observer.disconnect();
  155. document.removeEventListener('keydown', keydownHandler);
  156. };
  157. });
  158. </script>
  159. <div id="code-textarea-{id}" class="h-full w-full" />