import { EditorView, basicSetup } from "codemirror"; import { Compartment, EditorState, type Extension } from "@codemirror/state"; import { lineNumbers, ViewUpdate } from "@codemirror/view"; import { vim } from "@replit/codemirror-vim"; import { json as cmJson } from "@codemirror/lang-json"; import { html as cmHtml } from "@codemirror/lang-html"; import { getSetting, setSetting } from "./settings.svelte"; import ls from "./localstorage"; import type { HttpResponseBody } from "./types"; const jsonExt = cmJson(); const htmlExt = cmHtml(); const vimExtension = vim(); const relativeLines = lineNumbersRelative(); let vimEnabled: boolean = $state(ls.VIM_MODE.get()); export const isVimEnabled = () => vimEnabled; const langConfig = new Compartment(); const stateChangeListener = new Compartment(); const vimConfig = new Compartment(); const lineWrapConfig = new Compartment(); const editorPadding = EditorView.theme({ ".cm-content": { marginBottom: "4rem", }, ".cm-scroller": { marginBottom: "2rem", }, "@media (min-height: 900px)": { ".cm-content": { marginBottom: "3rem", }, ".cm-scroller": { marginBottom: "1rem", }, }, "@media (min-height: 1200px)": { ".cm-content": { marginBottom: "2rem", }, ".cm-scroller": { marginBottom: "0.5rem", }, }, }); export function init( id: string, lineWrap: boolean, vimMode: boolean, type?: HttpResponseBody["type"], ): EditorView { const extensions = [ basicSetup, editorPadding, stateChangeListener.of(EditorView.updateListener.of(() => {})), ]; const langExtensions = []; if (type != null) { const lang = langExt(type); if (lang != null) { langExtensions.push(lang); } } extensions.push(langConfig.of(langExtensions)); const vimExtensions = []; if (vimEnabled && vimMode) { vimExtensions.push(vimExtension, relativeLines); } extensions.push(vimConfig.of(vimExtensions)); if (lineWrap) { extensions.push(lineWrapConfig.of([EditorView.lineWrapping])); } else { extensions.push(lineWrapConfig.of([])); } return new EditorView({ parent: document.getElementById(id) ?? undefined, state: EditorState.create({ extensions }), }); } export function clearContent(view: EditorView) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "", }, }); } export function setContent( view: EditorView, content?: string, type?: HttpResponseBody["type"], ) { if (type != null) { const lang = langExt(type); view.dispatch({ effects: langConfig.reconfigure(lang != null ? [lang] : []), }); } view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content ?? "", }, }); } export function setUpdateHandler( view: EditorView, fn: (update: ViewUpdate) => void, ) { view.dispatch({ effects: stateChangeListener.reconfigure(EditorView.updateListener.of(fn)), }); } export function toggleVim(view: EditorView) { vimEnabled = !vimEnabled; view.dispatch({ effects: vimConfig.reconfigure( vimEnabled ? [vimExtension, relativeLines] : [], ), }); ls.VIM_MODE.set(vimEnabled); } export function toggleWrap(view: EditorView, value: boolean) { view.dispatch({ effects: lineWrapConfig.reconfigure(value ? [EditorView.lineWrapping] : []), }); ls.WRAP_RESPONSE.set(value); } /** Copy the contents of the editor to the system clipboard. */ export async function copyContent(view: EditorView) { await navigator.clipboard.writeText(view.state.doc.toString()); } /** Parse and stringify the editor contents as JSON in pretty mode. */ export function formatJson(view: EditorView) { try { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: JSON.stringify(JSON.parse(view.state.doc.toString()), null, 2), }, }); } catch (e) { console.warn(e); } } /** * Sets the gutter to display relative lines for VIM motions. */ function lineNumbersRelative(): Extension { return [lineNumbers({ formatNumber: relativeLineNumbers })]; } function relativeLineNumbers(lineNo: number, state: EditorState) { const cursorLine = state.doc.lineAt( state.selection.asSingle().ranges[0].to, ).number; // Absolute number for the current line if (lineNo === cursorLine) return lineNo.toString(); // Relative number for all other lines return Math.abs(cursorLine - lineNo).toString(); } function langExt(type: HttpResponseBody["type"]): Extension | null { switch (type) { case "TextPlain": return null; case "TextHtml": { return htmlExt; } case "Json": { return jsonExt; } } }