CodeEditor.svelte 5.6 KB

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