0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-25 19:18:11 +00:00
This commit is contained in:
Víctor Losada Hernández
2026-03-25 17:23:38 +01:00
parent 456c149f7b
commit 52f2f532a7

View File

@@ -1,9 +1,6 @@
import "./codeEditor.less"; import './codeEditor.less';
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from "react"; 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 { import {
EditorView, EditorView,
keymap, keymap,
@@ -11,65 +8,54 @@ import {
highlightActiveLineGutter, highlightActiveLineGutter,
highlightActiveLine, highlightActiveLine,
scrollPastEnd, scrollPastEnd,
} from "@codemirror/view"; Decoration,
import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; ViewPlugin,
import { languages } from "@codemirror/language-data"; } from '@codemirror/view';
import { css } from "@codemirror/lang-css"; import { EditorState, Compartment } from '@codemirror/state';
import { basicLightHighlightStyle } from "cm6-theme-basic-light"; import { foldGutter, foldKeymap, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
import { defaultKeymap, history, historyField, undo, redo } from '@codemirror/commands';
import { languages } from '@codemirror/language-data';
import { css } from '@codemirror/lang-css';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
// themes import { tags } from '@lezer/highlight';
import * as themes from "@uiw/codemirror-themes-all"; // ######################### THEMES #############################
const themeList = Object.entries(themes) import * as themes from '@uiw/codemirror-themes-all';
.filter(([name, value]) => Array.isArray(value) && !name.endsWith("Init") && !name.endsWith("Style"))
.map(([name, theme]) => ({
name,
theme,
}));
console.log(themeList);
import { Compartment } from "@codemirror/state";
const themeCompartment = new Compartment(); const themeCompartment = new Compartment();
// custom highlighting // ######################### CUSTOM HIGHLIGHTS #############################
import { HighlightStyle } from "@codemirror/language";
import { syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
const highlightStyle = HighlightStyle.define([ const highlightStyle = HighlightStyle.define([
{ {
tag: tags.heading1, tag : tags.heading1,
color: "black", color : 'black',
fontSize: "1.75em", fontSize : '1.75em',
fontWeight: "700", fontWeight : '700',
class: "cm-header cm-header-1", class : 'cm-header cm-header-1',
}, },
{ {
tag: tags.processingInstruction, tag : tags.processingInstruction,
color: "blue", color : 'blue',
}, },
// … // …
]); ]);
/*custom tokens */ import { tokenizeCustomMarkdown, customTags } from './customMarkdownGrammar.js';
import { Decoration, ViewPlugin, WidgetType } from "@codemirror/view";
import { tokenizeCustomMarkdown, customTags } from "./customMarkdownGrammar.js";
const customHighlightStyle = HighlightStyle.define([ const customHighlightStyle = HighlightStyle.define([
{ tag: tags.heading1, color: "#000", fontWeight: "700" }, { tag: tags.heading1, color: '#000', fontWeight: '700' },
{ tag: tags.keyword, color: "#07a" }, // example for your markdown headings { tag: tags.keyword, color: '#07a' }, // example for your markdown headings
{ tag: customTags.pageLine, color: "#f0a" }, { tag: customTags.pageLine, color: '#f0a' },
{ tag: customTags.snippetBreak, class: "cm-snippet-break", color: "#0af" }, { tag: customTags.snippetBreak, class: 'cm-snippet-break', color: '#0af' },
{ tag: customTags.inlineBlock, class: "cm-inline-block", backgroundColor: "#fffae6" }, { tag: customTags.inlineBlock, class: 'cm-inline-block', backgroundColor: '#fffae6' },
{ tag: customTags.emoji, class: "cm-emoji", color: "#fa0" }, { tag: customTags.emoji, class: 'cm-emoji', color: '#fa0' },
{ tag: customTags.superscript, class: "cm-superscript", verticalAlign: "super", fontSize: "0.8em" }, { tag: customTags.superscript, class: 'cm-superscript', verticalAlign: 'super', fontSize: '0.8em' },
{ tag: customTags.subscript, class: "cm-subscript", verticalAlign: "sub", 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.definitionTerm, class: 'cm-dt', fontWeight: 'bold', color: '#0a0' },
{ tag: customTags.definitionDesc, class: "cm-dd", color: "#070" }, { tag: customTags.definitionDesc, class: 'cm-dd', color: '#070' },
]); ]);
const customHighlightPlugin = ViewPlugin.fromClass( const customHighlightPlugin = ViewPlugin.fromClass(
@@ -78,7 +64,7 @@ const customHighlightPlugin = ViewPlugin.fromClass(
this.decorations = this.buildDecorations(view); this.decorations = this.buildDecorations(view);
} }
update(update) { update(update) {
if (update.docChanged) { if(update.docChanged) {
this.decorations = this.buildDecorations(update.view); this.decorations = this.buildDecorations(update.view);
} }
} }
@@ -86,10 +72,10 @@ const customHighlightPlugin = ViewPlugin.fromClass(
const decos = []; const decos = [];
const tokens = tokenizeCustomMarkdown(view.state.doc.toString()); const tokens = tokenizeCustomMarkdown(view.state.doc.toString());
tokens.forEach((tok) => { tokens.forEach((tok)=>{
const line = view.state.doc.line(tok.line + 1); const line = view.state.doc.line(tok.line + 1);
if (tok.from != null && tok.to != null && tok.from < tok.to) { if(tok.from != null && tok.to != null && tok.from < tok.to) {
// inline decoration // inline decoration
decos.push( decos.push(
Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to), Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to),
@@ -101,76 +87,77 @@ const customHighlightPlugin = ViewPlugin.fromClass(
}); });
// sort by absolute start position // sort by absolute start position
decos.sort((a, b) => a.from - b.from || a.to - b.to); decos.sort((a, b)=>a.from - b.from || a.to - b.to);
return Decoration.set(decos); return Decoration.set(decos);
} }
}, },
{ {
decorations: (v) => v.decorations, decorations : (v)=>v.decorations,
}, },
); );
// ######################### COMPONENT #############################
const CodeEditor = forwardRef( const CodeEditor = forwardRef(
( (
{ {
value = "", value = '',
onChange = () => {}, onChange = ()=>{},
language = "", language = '',
tab = "brewText", tab = 'brewText',
editorTheme = "default", editorTheme = 'default',
view, view,
style, style,
...props ...props
}, },
ref, ref,
) => { )=>{
const editorRef = useRef(null); const editorRef = useRef(null);
const viewRef = useRef(null); const viewRef = useRef(null);
const docsRef = useRef({}); const docsRef = useRef({});
const prevTabRef = useRef(tab); const prevTabRef = useRef(tab);
// --- init editor --- const createExtensions = ({ onChange, language, editorTheme })=>{
const createExtensions = ({ onChange, language, editorTheme }) => { const updateListener = EditorView.updateListener.of((update)=>{
const updateListener = EditorView.updateListener.of((update) => { if(update.docChanged) {
if (update.docChanged) {
onChange(update.state.doc.toString()); onChange(update.state.doc.toString());
} }
}); });
const boldCommand = (view) => { const boldCommand = (view)=>{
const { from, to } = view.state.selection.main; const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to); const selected = view.state.doc.sliceString(from, to);
const text = `**${selected}**`; const text = `**${selected}**`;
view.dispatch({ view.dispatch({
changes: { from, to, insert: text }, changes : { from, to, insert: text },
selection: { anchor: from + text.length }, selection : { anchor: from + text.length },
}); });
return true; return true;
}; };
const italicCommand = (view) => { const italicCommand = (view)=>{
const { from, to } = view.state.selection.main; const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to); const selected = view.state.doc.sliceString(from, to);
const text = `*${selected}*`; const text = `*${selected}*`;
view.dispatch({ view.dispatch({
changes: { from, to, insert: text }, changes : { from, to, insert: text },
selection: { anchor: from + text.length }, selection : { anchor: from + text.length },
}); });
return true; return true;
}; };
const customKeymap = keymap.of([ const customKeymap = keymap.of([
{ key: "Mod-b", run: boldCommand }, { key: 'Mod-b', run: boldCommand },
{ key: "Mod-i", run: italicCommand }, { key: 'Mod-i', run: italicCommand },
]); ]);
const languageExtension = const languageExtension =
language === "css" ? css() : markdown({ base: markdownLanguage, codeLanguages: languages }); language === 'css' ? css() : markdown({ base: markdownLanguage, codeLanguages: languages });
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : []; const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : [];
@@ -196,43 +183,43 @@ const CodeEditor = forwardRef(
]; ];
}; };
useEffect(() => { useEffect(()=>{
if (!editorRef.current) return; if(!editorRef.current) return;
// create initial editor state // create initial editor state
const state = EditorState.create({ const state = EditorState.create({
doc: value, doc : value,
extensions: createExtensions({ onChange, language, editorTheme }), extensions : createExtensions({ onChange, language, editorTheme }),
}); });
viewRef.current = new EditorView({ viewRef.current = new EditorView({
state, state,
parent: editorRef.current, parent : editorRef.current,
}); });
// save initial state for current tab // save initial state for current tab
docsRef.current[tab] = state; docsRef.current[tab] = state;
return () => viewRef.current?.destroy(); return ()=>viewRef.current?.destroy();
}, []); }, []);
useEffect(() => { useEffect(()=>{
const view = viewRef.current; const view = viewRef.current;
if (!view) return; if(!view) return;
const prevTab = prevTabRef.current; const prevTab = prevTabRef.current;
if (prevTab !== tab) { if(prevTab !== tab) {
// save current state // save current state
docsRef.current[prevTab] = view.state; docsRef.current[prevTab] = view.state;
// restore or create // restore or create
let nextState = docsRef.current[tab]; let nextState = docsRef.current[tab];
if (!nextState) { if(!nextState) {
nextState = EditorState.create({ nextState = EditorState.create({
doc: value, doc : value,
extensions: createExtensions({ onChange, language, editorTheme }), extensions : createExtensions({ onChange, language, editorTheme }),
}); });
} }
@@ -241,76 +228,76 @@ const CodeEditor = forwardRef(
} }
}, [tab]); }, [tab]);
useEffect(() => { useEffect(()=>{
const view = viewRef.current; const view = viewRef.current;
if (!view) return; if(!view) return;
const current = view.state.doc.toString(); const current = view.state.doc.toString();
if (value !== current) { if(value !== current) {
view.dispatch({ view.dispatch({
changes: { from: 0, to: current.length, insert: value }, changes : { from: 0, to: current.length, insert: value },
}); });
} }
}, [value]); }, [value]);
useEffect(() => { //rebuild theme extension on theme change useEffect(()=>{
//rebuild theme extension on theme change
const view = viewRef.current; const view = viewRef.current;
if (!view) return; if(!view) return;
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : []; const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : [];
view.dispatch({ view.dispatch({
effects: themeCompartment.reconfigure(themeExtension), effects : themeCompartment.reconfigure(themeExtension),
}); });
}, [editorTheme]); }, [editorTheme]);
useImperativeHandle(ref, ()=>({
getValue : ()=>viewRef.current.state.doc.toString(),
useImperativeHandle(ref, () => ({ setValue : (text)=>{
getValue: () => viewRef.current.state.doc.toString(),
setValue: (text) => {
const view = viewRef.current; const view = viewRef.current;
view.dispatch({ view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: text }, changes : { from: 0, to: view.state.doc.length, insert: text },
}); });
}, },
injectText: (text) => { injectText : (text)=>{
const view = viewRef.current; const view = viewRef.current;
const { from, to } = view.state.selection.main; const { from, to } = view.state.selection.main;
view.dispatch({ view.dispatch({
changes: { from, to, insert: text }, changes : { from, to, insert: text },
selection: { anchor: from + text.length }, selection : { anchor: from + text.length },
}); });
view.focus(); view.focus();
}, },
getCursorPosition: () => viewRef.current.state.selection.main.head, getCursorPosition : ()=>viewRef.current.state.selection.main.head,
setCursorPosition: (pos) => { setCursorPosition : (pos)=>{
viewRef.current.dispatch({ selection: { anchor: pos } }); viewRef.current.dispatch({ selection: { anchor: pos } });
viewRef.current.focus(); viewRef.current.focus();
}, },
undo: () => undo(viewRef.current), undo : ()=>undo(viewRef.current),
redo: () => redo(viewRef.current), redo : ()=>redo(viewRef.current),
historySize: () => { historySize : ()=>{
const view = viewRef.current; const view = viewRef.current;
if (!view) return { done: 0, undone: 0 }; if(!view) return { done: 0, undone: 0 };
const h = view.state.field(historyField, false); const h = view.state.field(historyField, false);
if (!h) return { done: 0, undone: 0 }; if(!h) return { done: 0, undone: 0 };
return { done: h.done.length, undone: h.undone.length }; return { done: h.done.length, undone: h.undone.length };
}, },
focus: () => viewRef.current.focus(), focus : ()=>viewRef.current.focus(),
})); }));
return <div className="codeEditor" ref={editorRef} style={style} />; return <div className='codeEditor' ref={editorRef} style={style} />;
}, },
); );