codemirror.svelte.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { EditorView, basicSetup } from "codemirror";
  2. import { Compartment, EditorState, type Extension } from "@codemirror/state";
  3. import { lineNumbers, ViewUpdate } from "@codemirror/view";
  4. import { vim } from "@replit/codemirror-vim";
  5. import { json as cmJson } from "@codemirror/lang-json";
  6. import { html as cmHtml } from "@codemirror/lang-html";
  7. import { getSetting, setSetting } from "./settings.svelte";
  8. import ls from "./localstorage";
  9. import type { HttpResponseBody } from "./types";
  10. const jsonExt = cmJson();
  11. const htmlExt = cmHtml();
  12. const vimExtension = vim();
  13. const relativeLines = lineNumbersRelative();
  14. let vimEnabled: boolean = $state(ls.VIM_MODE.get());
  15. export const isVimEnabled = () => vimEnabled;
  16. const langConfig = new Compartment();
  17. const stateChangeListener = new Compartment();
  18. const vimConfig = new Compartment();
  19. const lineWrapConfig = new Compartment();
  20. const editorPadding = EditorView.theme({
  21. ".cm-content": {
  22. marginBottom: "4rem",
  23. },
  24. ".cm-scroller": {
  25. marginBottom: "2rem",
  26. },
  27. "@media (min-height: 900px)": {
  28. ".cm-content": {
  29. marginBottom: "3rem",
  30. },
  31. ".cm-scroller": {
  32. marginBottom: "1rem",
  33. },
  34. },
  35. "@media (min-height: 1200px)": {
  36. ".cm-content": {
  37. marginBottom: "2rem",
  38. },
  39. ".cm-scroller": {
  40. marginBottom: "0.5rem",
  41. },
  42. },
  43. });
  44. export function init(
  45. id: string,
  46. lineWrap: boolean,
  47. vimMode: boolean,
  48. type?: HttpResponseBody["type"],
  49. ): EditorView {
  50. const extensions = [
  51. basicSetup,
  52. editorPadding,
  53. stateChangeListener.of(EditorView.updateListener.of(() => {})),
  54. ];
  55. const langExtensions = [];
  56. if (type != null) {
  57. const lang = langExt(type);
  58. if (lang != null) {
  59. langExtensions.push(lang);
  60. }
  61. }
  62. extensions.push(langConfig.of(langExtensions));
  63. const vimExtensions = [];
  64. if (vimEnabled && vimMode) {
  65. vimExtensions.push(vimExtension, relativeLines);
  66. }
  67. extensions.push(vimConfig.of(vimExtensions));
  68. if (lineWrap) {
  69. extensions.push(lineWrapConfig.of([EditorView.lineWrapping]));
  70. } else {
  71. extensions.push(lineWrapConfig.of([]));
  72. }
  73. return new EditorView({
  74. parent: document.getElementById(id) ?? undefined,
  75. state: EditorState.create({ extensions }),
  76. });
  77. }
  78. export function clearContent(view: EditorView) {
  79. view.dispatch({
  80. changes: {
  81. from: 0,
  82. to: view.state.doc.length,
  83. insert: "",
  84. },
  85. });
  86. }
  87. export function setContent(
  88. view: EditorView,
  89. content?: string,
  90. type?: HttpResponseBody["type"],
  91. ) {
  92. if (type != null) {
  93. const lang = langExt(type);
  94. view.dispatch({
  95. effects: langConfig.reconfigure(lang != null ? [lang] : []),
  96. });
  97. }
  98. view.dispatch({
  99. changes: {
  100. from: 0,
  101. to: view.state.doc.length,
  102. insert: content ?? "",
  103. },
  104. });
  105. }
  106. export function setUpdateHandler(
  107. view: EditorView,
  108. fn: (update: ViewUpdate) => void,
  109. ) {
  110. view.dispatch({
  111. effects: stateChangeListener.reconfigure(EditorView.updateListener.of(fn)),
  112. });
  113. }
  114. export function toggleVim(view: EditorView) {
  115. vimEnabled = !vimEnabled;
  116. view.dispatch({
  117. effects: vimConfig.reconfigure(
  118. vimEnabled ? [vimExtension, relativeLines] : [],
  119. ),
  120. });
  121. ls.VIM_MODE.set(vimEnabled);
  122. }
  123. export function toggleWrap(view: EditorView, value: boolean) {
  124. view.dispatch({
  125. effects: lineWrapConfig.reconfigure(value ? [EditorView.lineWrapping] : []),
  126. });
  127. ls.WRAP_RESPONSE.set(value);
  128. }
  129. /** Copy the contents of the editor to the system clipboard. */
  130. export async function copyContent(view: EditorView) {
  131. await navigator.clipboard.writeText(view.state.doc.toString());
  132. }
  133. /** Parse and stringify the editor contents as JSON in pretty mode. */
  134. export function formatJson(view: EditorView) {
  135. try {
  136. view.dispatch({
  137. changes: {
  138. from: 0,
  139. to: view.state.doc.length,
  140. insert: JSON.stringify(JSON.parse(view.state.doc.toString()), null, 2),
  141. },
  142. });
  143. } catch (e) {
  144. console.warn(e);
  145. }
  146. }
  147. /**
  148. * Sets the gutter to display relative lines for VIM motions.
  149. */
  150. function lineNumbersRelative(): Extension {
  151. return [lineNumbers({ formatNumber: relativeLineNumbers })];
  152. }
  153. function relativeLineNumbers(lineNo: number, state: EditorState) {
  154. const cursorLine = state.doc.lineAt(
  155. state.selection.asSingle().ranges[0].to,
  156. ).number;
  157. // Absolute number for the current line
  158. if (lineNo === cursorLine) return lineNo.toString();
  159. // Relative number for all other lines
  160. return Math.abs(cursorLine - lineNo).toString();
  161. }
  162. function langExt(type: HttpResponseBody["type"]): Extension | null {
  163. switch (type) {
  164. case "TextPlain":
  165. return null;
  166. case "TextHtml": {
  167. return htmlExt;
  168. }
  169. case "Json": {
  170. return jsonExt;
  171. }
  172. }
  173. }