import "./codeEditor.less"; import React, { useEffect, useRef, forwardRef, useImperativeHandle } from "react"; import { EditorState } from "@codemirror/state"; import { defaultKeymap, history, historyField, undo, redo } from "@codemirror/commands"; import { foldGutter, foldKeymap } from "@codemirror/language"; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, scrollPastEnd, } from "@codemirror/view"; import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; import { languages } from "@codemirror/language-data"; import { css } from "@codemirror/lang-css"; import { basicLightHighlightStyle } from "cm6-theme-basic-light"; import { HighlightStyle } from "@codemirror/language"; import { syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; const highlightStyle = HighlightStyle.define([ { tag: tags.heading1, color: "black", fontSize: "1.75em", fontWeight: "700", class: "cm-header cm-header-1", }, { tag: tags.processingInstruction, color: "blue", }, // … ]); /*custom tokens */ import { Decoration, ViewPlugin, WidgetType } from "@codemirror/view"; import { tokenizeCustomMarkdown, customTags } from "./customMarkdownGrammar.js"; const customHighlightStyle = HighlightStyle.define([ { tag: tags.heading1, color: "#000", fontWeight: "700" }, { tag: tags.keyword, color: "#07a" }, // example for your markdown headings { tag: customTags.pageLine, color: "#f0a" }, { tag: customTags.snippetBreak, class: "cm-snippet-break", color: "#0af" }, { tag: customTags.inlineBlock, class: "cm-inline-block", backgroundColor: "#fffae6" }, { tag: customTags.emoji, class: "cm-emoji", color: "#fa0" }, { tag: customTags.superscript, class: "cm-superscript", verticalAlign: "super", fontSize: "0.8em" }, { tag: customTags.subscript, class: "cm-subscript", verticalAlign: "sub", fontSize: "0.8em" }, { tag: customTags.definitionTerm, class: "cm-dt", fontWeight: "bold", color: "#0a0" }, { tag: customTags.definitionDesc, class: "cm-dd", color: "#070" }, ]); const customHighlightPlugin = ViewPlugin.fromClass( class { constructor(view) { this.decorations = this.buildDecorations(view); } update(update) { if (update.docChanged) { this.decorations = this.buildDecorations(update.view); } } buildDecorations(view) { const widgets = []; const tokens = tokenizeCustomMarkdown(view.state.doc.toString()); // sort by line number tokens.sort((a, b) => a.line - b.line); tokens.forEach((tok) => { const line = view.state.doc.line(tok.line + 1); // CM lines are 1-based widgets.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from)); }); return Decoration.set(widgets); } }, { decorations: (v) => v.decorations, }, ); const CodeEditor = forwardRef( ( { value = "", onChange = () => {}, language = "", tab = "brewText", editorTheme = "default", view, style, ...props }, ref, ) => { const editorRef = useRef(null); const viewRef = useRef(null); const docsRef = useRef({}); const prevTabRef = useRef(tab); console.log(props); // --- init editor --- const createExtensions = ({ onChange, language, editorTheme }) => { const updateListener = EditorView.updateListener.of((update) => { if (update.docChanged) { onChange(update.state.doc.toString()); } }); const boldCommand = (view) => { const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to); const text = `**${selected}**`; view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length }, }); return true; }; const italicCommand = (view) => { const { from, to } = view.state.selection.main; const selected = view.state.doc.sliceString(from, to); const text = `*${selected}*`; view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length }, }); return true; }; const customKeymap = keymap.of([ { key: "Mod-b", run: boldCommand }, { key: "Mod-i", run: italicCommand }, ]); const languageExtension = language === "css" ? css() : markdown({ base: markdownLanguage, codeLanguages: languages }); const themeExtension = syntaxHighlighting(basicLightHighlightStyle); return [ history(), keymap.of(defaultKeymap), customKeymap, updateListener, EditorView.lineWrapping, scrollPastEnd(), languageExtension, highlightActiveLine(), highlightActiveLineGutter(), keymap.of(foldKeymap), foldGutter(), lineNumbers(), themeExtension, syntaxHighlighting(highlightStyle), customHighlightPlugin, syntaxHighlighting(customHighlightStyle), ]; }; useEffect(() => { if (!editorRef.current) return; // create initial editor state const state = EditorState.create({ doc: value, extensions: createExtensions({ onChange, language, editorTheme }), }); viewRef.current = new EditorView({ state, parent: editorRef.current, }); // save initial state for current tab docsRef.current[tab] = state; return () => viewRef.current?.destroy(); }, []); useEffect(() => { const view = viewRef.current; if (!view) return; const prevTab = prevTabRef.current; if (prevTab !== tab) { // save current state docsRef.current[prevTab] = view.state; // restore or create let nextState = docsRef.current[tab]; if (!nextState) { nextState = EditorState.create({ doc: value, extensions: createExtensions({ onChange, language, editorTheme }), }); } view.setState(nextState); prevTabRef.current = tab; } }, [tab]); useEffect(() => { const view = viewRef.current; if (!view) return; const current = view.state.doc.toString(); if (value !== current) { view.dispatch({ changes: { from: 0, to: current.length, insert: value }, }); } }, [value]); // --- exposed API --- useImperativeHandle(ref, () => ({ getValue: () => viewRef.current.state.doc.toString(), setValue: (text) => { const view = viewRef.current; view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, }); }, injectText: (text) => { const view = viewRef.current; const { from, to } = view.state.selection.main; view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length }, }); view.focus(); }, getCursorPosition: () => viewRef.current.state.selection.main.head, setCursorPosition: (pos) => { viewRef.current.dispatch({ selection: { anchor: pos } }); viewRef.current.focus(); }, undo: () => undo(viewRef.current), redo: () => redo(viewRef.current), historySize: () => { const view = viewRef.current; if (!view) return { done: 0, undone: 0 }; const h = view.state.field(historyField, false); if (!h) return { done: 0, undone: 0 }; return { done: h.done.length, undone: h.undone.length }; }, focus: () => viewRef.current.focus(), })); return
; }, ); export default CodeEditor;