CodeEditor.svelte 4.6 KB

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