From e497e42913db3d51da9d3d7b84df705e70d1c768 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 17 Nov 2025 16:15:44 +0000 Subject: [PATCH] TEST --- client/components/codeEditor/codeEditor.jsx | 33 ++- client/components/codeEditor/codeEditor.less | 12 +- client/components/codeEditor/codeEditorV6.jsx | 191 ++++++++++++++++++ client/homebrew/editor/editor.jsx | 44 +++- config/default.json | 3 +- package.json | 18 +- scripts/project.json | 36 ++-- server/app.js | 3 +- 8 files changed, 290 insertions(+), 50 deletions(-) create mode 100644 client/components/codeEditor/codeEditorV6.jsx diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index fb69b6dcf..a01d5a716 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -8,36 +8,36 @@ const autoCompleteEmoji = require('./autocompleteEmoji'); let CodeMirror; if(typeof window !== 'undefined'){ - CodeMirror = require('codemirror'); + CodeMirror = require('codemirror5'); //Language Modes - require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown - require('codemirror/mode/css/css.js'); - require('codemirror/mode/javascript/javascript.js'); + require('codemirror5/mode/gfm/gfm.js'); //Github flavoured markdown + require('codemirror5/mode/css/css.js'); + require('codemirror5/mode/javascript/javascript.js'); //Addons //Code folding - require('codemirror/addon/fold/foldcode.js'); - require('codemirror/addon/fold/foldgutter.js'); + require('codemirror5/addon/fold/foldcode.js'); + require('codemirror5/addon/fold/foldgutter.js'); //Search and replace - require('codemirror/addon/search/search.js'); - require('codemirror/addon/search/searchcursor.js'); - require('codemirror/addon/search/jump-to-line.js'); - require('codemirror/addon/search/match-highlighter.js'); - require('codemirror/addon/search/matchesonscrollbar.js'); - require('codemirror/addon/dialog/dialog.js'); + require('codemirror5/addon/search/search.js'); + require('codemirror5/addon/search/searchcursor.js'); + require('codemirror5/addon/search/jump-to-line.js'); + require('codemirror5/addon/search/match-highlighter.js'); + require('codemirror5/addon/search/matchesonscrollbar.js'); + require('codemirror5/addon/dialog/dialog.js'); //Trailing space highlighting // require('codemirror/addon/edit/trailingspace.js'); //Active line highlighting // require('codemirror/addon/selection/active-line.js'); //Scroll past last line - require('codemirror/addon/scroll/scrollpastend.js'); + require('codemirror5/addon/scroll/scrollpastend.js'); //Auto-closing //XML code folding is a requirement of the auto-closing tag feature and is not enabled - require('codemirror/addon/fold/xml-fold.js'); - require('codemirror/addon/edit/closetag.js'); + require('codemirror5/addon/fold/xml-fold.js'); + require('codemirror5/addon/edit/closetag.js'); //Autocompletion - require('codemirror/addon/hint/show-hint.js'); + require('codemirror5/addon/hint/show-hint.js'); const foldPagesCode = require('./fold-pages'); foldPagesCode.registerHomebreweryHelper(CodeMirror); @@ -462,4 +462,3 @@ const CodeEditor = createClass({ }); module.exports = CodeEditor; - diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index c8e60974b..a641e62f6 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -1,8 +1,8 @@ -@import (less) 'codemirror/lib/codemirror.css'; -@import (less) 'codemirror/addon/fold/foldgutter.css'; -@import (less) 'codemirror/addon/search/matchesonscrollbar.css'; -@import (less) 'codemirror/addon/dialog/dialog.css'; -@import (less) 'codemirror/addon/hint/show-hint.css'; +@import (less) 'codemirror5/lib/codemirror.css'; +@import (less) 'codemirror5/addon/fold/foldgutter.css'; +@import (less) 'codemirror5/addon/search/matchesonscrollbar.css'; +@import (less) 'codemirror5/addon/dialog/dialog.css'; +@import (less) 'codemirror5/addon/hint/show-hint.css'; //Icon fonts included so they can appear in emoji autosuggest dropdown @import (less) './themes/fonts/iconFonts/diceFont.less'; @@ -57,4 +57,4 @@ .emojiPreview { font-size : 1.5em; line-height : 1.2em; -} \ No newline at end of file +} diff --git a/client/components/codeEditor/codeEditorV6.jsx b/client/components/codeEditor/codeEditorV6.jsx new file mode 100644 index 000000000..368623d38 --- /dev/null +++ b/client/components/codeEditor/codeEditorV6.jsx @@ -0,0 +1,191 @@ +import './codeEditor.less'; +import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'; +import { Compartment, EditorSelection, EditorState } from '@codemirror/state'; +import { EditorView, keymap, highlightActiveLine, lineNumbers, highlightActiveLineGutter } from '@codemirror/view'; +import { history, historyKeymap, undo as historyUndo, redo as historyRedo, undoDepth, redoDepth } from '@codemirror/history'; +import { defaultKeymap, indentWithTab } from '@codemirror/commands'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; +import { defaultHighlightStyle } from '@codemirror/language'; +import { syntaxHighlighting } from '@codemirror/language'; +import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; +import { languages } from '@codemirror/language-data'; +import { javascript } from '@codemirror/lang-javascript'; +import { css } from '@codemirror/lang-css'; + +const baseExtensions = [ + lineNumbers(), + highlightActiveLineGutter(), + EditorView.lineWrapping, + history(), + keymap.of([ + indentWithTab, + ...closeBracketsKeymap, + ...searchKeymap, + ...historyKeymap, + ...defaultKeymap + ]), + closeBrackets(), + highlightSelectionMatches(), + highlightActiveLine(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }) +]; + +const languageExtension = (language)=>{ + switch(language){ + case 'css': + return css(); + case 'javascript': + return javascript({ jsx: true, typescript: true }); + case 'gfm': + default: + return markdown({ base: markdownLanguage, codeLanguages: languages }); + } +}; + +const CodeEditorV6 = React.forwardRef(({ + value = '', + onChange = ()=>{}, + language = 'gfm', + readOnly = false, + style = {}, + editorTheme, + onSelectionChange = ()=>{}, + onScroll = ()=>{} +}, ref)=>{ + const containerRef = useRef(null); + const viewRef = useRef(null); + const languageCompartment = useMemo(()=>new Compartment(), []); + const readOnlyCompartment = useMemo(()=>new Compartment(), []); + + useImperativeHandle(ref, ()=>({ + focus : ()=>viewRef.current?.focus(), + getView : ()=>viewRef.current, + getCM6View : ()=>viewRef.current, + getCursorPosition : ()=>{ + const view = viewRef.current; + if(!view) return { line: 0, ch: 0 }; + const { head } = view.state.selection.main; + const lineInfo = view.state.doc.lineAt(head); + return { line: lineInfo.number - 1, ch: head - lineInfo.from }; + }, + setCursorPosition : ({ line = 0, ch = 0 })=>{ + const view = viewRef.current; + if(!view) return; + const docLine = view.state.doc.line(Math.max(1, line + 1)); + const pos = Math.min(docLine.from + ch, docLine.to); + view.dispatch({ selection: EditorSelection.cursor(pos), scrollIntoView: true }); + }, + getTopVisibleLine : ()=>{ + const view = viewRef.current; + if(!view) return 0; + const top = view.scrollDOM.scrollTop; + const block = view.lineBlockAtHeight(top); + const lineInfo = view.state.doc.lineAt(block.from); + return lineInfo.number - 1; + }, + updateSize : ()=>viewRef.current?.requestMeasure(), + injectText : (text, overwrite=true)=>{ + const view = viewRef.current; + if(!view) return; + const { from, to } = view.state.selection.main; + const insertFrom = overwrite ? from : from; + const insertTo = overwrite ? to : from; + view.dispatch({ + changes : { from: insertFrom, to: insertTo, insert: text }, + selection : EditorSelection.cursor(insertFrom + text.length), + scrollIntoView : true + }); + view.focus(); + }, + undo : ()=>{ + const view = viewRef.current; + if(!view) return; + historyUndo(view); + }, + redo : ()=>{ + const view = viewRef.current; + if(!view) return; + historyRedo(view); + }, + historySize : ()=>{ + const view = viewRef.current; + if(!view) return { undo: 0, redo: 0 }; + return { + undo : undoDepth(view.state), + redo : redoDepth(view.state) + }; + }, + foldAllCode : ()=>{}, + unfoldAllCode : ()=>{} + }), []); + + useEffect(()=>{ + if(!containerRef.current) return; + + const initialExtensions = [ + ...baseExtensions, + languageCompartment.of(languageExtension(language)), + readOnlyCompartment.of(EditorState.readOnly.of(readOnly)), + EditorView.updateListener.of((update)=>{ + if(update.docChanged) onChange(update.state.doc.toString()); + if(update.selectionSet) onSelectionChange(update.view); + }) + ]; + + const state = EditorState.create({ + doc : value, + extensions : initialExtensions + }); + + const view = new EditorView({ + state, + parent : containerRef.current + }); + viewRef.current = view; + + const handleScroll = ()=>onScroll(view); + view.scrollDOM.addEventListener('scroll', handleScroll); + + return ()=>{ + view.scrollDOM.removeEventListener('scroll', handleScroll); + view.destroy(); + viewRef.current = null; + }; + }, []); + + useEffect(()=>{ + const view = viewRef.current; + if(!view) return; + view.dispatch({ + effects : languageCompartment.reconfigure(languageExtension(language)) + }); + }, [language, languageCompartment]); + + useEffect(()=>{ + const view = viewRef.current; + if(!view) return; + view.dispatch({ + effects : readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly)) + }); + }, [readOnly, readOnlyCompartment]); + + useEffect(()=>{ + const view = viewRef.current; + if(!view) return; + const currentValue = view.state.doc.toString(); + if(value === currentValue) return; + const transaction = view.state.update({ + changes : { from: 0, to: currentValue.length, insert: value } + }); + view.dispatch(transaction); + }, [value]); + + return ( +
+ ); +}); + +CodeEditorV6.displayName = 'CodeEditorV6'; + +export default CodeEditorV6; diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index a00c47403..df6147a2c 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -7,6 +7,9 @@ const dedent = require('dedent-tabs').default; import Markdown from '../../../shared/markdown.js'; const CodeEditor = require('client/components/codeEditor/codeEditor.jsx'); +import CodeEditorV6 from 'client/components/codeEditor/codeEditorV6.jsx'; + +const USE_CM6 = global?.config?.enable_CM6 === true; const SnippetBar = require('./snippetbar/snippetbar.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); @@ -62,8 +65,34 @@ const Editor = createClass({ }; }, - editor : React.createRef(null), - codeEditor : React.createRef(null), + editor : React.createRef(null), + codeEditor : React.createRef(null), + codeEditorV6 : React.createRef(null), + + getActiveEditor : function(){ + return USE_CM6 ? this.codeEditorV6.current : this.codeEditor.current; + }, + + focusActiveEditor : function(){ + const editor = this.getActiveEditor(); + if(!editor) return; + if(USE_CM6) editor.focus?.(); + else editor.codeMirror?.focus(); + }, + + handleCM6SelectionChange : function(view){ + if(!USE_CM6 || !view) return; + const { head } = view.state.selection.main; + const lineInfo = view.state.doc.lineAt(head); + this.updateCurrentCursorPage({ line: lineInfo.number - 1 }); + }, + + handleCM6Scroll : function(){ + if(!USE_CM6) return; + const topLine = this.codeEditorV6.current?.getTopVisibleLine?.(); + if(topLine == null) return; + this.updateCurrentViewPage(topLine); + }, isText : function() {return this.state.view == 'text';}, isStyle : function() {return this.state.view == 'style';}, @@ -76,8 +105,10 @@ const Editor = createClass({ document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys); - this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());}); - this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); + if(!USE_CM6) { + this.codeEditor.current?.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());}); + this.codeEditor.current?.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); + } const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); if(editorTheme) { @@ -90,7 +121,7 @@ const Editor = createClass({ componentDidUpdate : function(prevProps, prevState, snapshot) { - this.highlightCustomMarkdown(); + if(!USE_CM6) this.highlightCustomMarkdown(); if(prevProps.moveBrew !== this.props.moveBrew) this.brewJump(); @@ -121,7 +152,8 @@ const Editor = createClass({ }, updateCurrentCursorPage : function(cursor) { - const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1); + const cursorLine = cursor.line ?? cursor; + const lines = this.props.brew.text.split('\n').slice(1, cursorLine + 1); const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/; const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1); this.props.onCursorPageChange(currentPage); diff --git a/config/default.json b/config/default.json index 6be4ce7ce..b9624aa7b 100644 --- a/config/default.json +++ b/config/default.json @@ -7,5 +7,6 @@ "local_environments" : ["docker", "local"], "publicUrl" : "https://homebrewery.naturalcrit.com", "hb_images" : null, - "hb_fonts" : null + "hb_fonts" : null, + "enable_CM6" : true } diff --git a/package.json b/package.json index b4b86693d..767311bd0 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,23 @@ "@sanity/diff-match-patch": "^3.2.0", "body-parser": "^2.2.0", "classnames": "^2.5.1", - "codemirror": "^5.65.6", + "@codemirror/autocomplete": "^6.18.2", + "@codemirror/commands": "^6.7.2", + "@codemirror/fold": "^6.7.1", + "@codemirror/history": "^6.3.0", + "@codemirror/lang-css": "^6.3.0", + "@codemirror/lang-html": "^6.4.7", + "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-markdown": "^6.2.3", + "@codemirror/language": "^6.10.2", + "@codemirror/lint": "^6.8.0", + "@codemirror/search": "^6.5.6", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.2.1", + "@lezer/highlight": "^1.2.0", + "codemirror": "^6.0.1", + "codemirror5": "npm:codemirror@^5.65.19", "cookie-parser": "^1.4.7", "core-js": "^3.46.0", "cors": "^2.8.5", diff --git a/scripts/project.json b/scripts/project.json index 7385b674a..0beefbcec 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -8,24 +8,24 @@ "create-react-class", "lodash", "classnames", - "codemirror", - "codemirror/mode/gfm/gfm.js", - "codemirror/mode/css/css.js", - "codemirror/mode/javascript/javascript.js", - "codemirror/addon/fold/foldcode.js", - "codemirror/addon/fold/foldgutter.js", - "codemirror/addon/fold/xml-fold.js", - "codemirror/addon/scroll/scrollpastend.js", - "codemirror/addon/search/search.js", - "codemirror/addon/search/searchcursor.js", - "codemirror/addon/search/jump-to-line.js", - "codemirror/addon/search/match-highlighter.js", - "codemirror/addon/search/matchesonscrollbar.js", - "codemirror/addon/dialog/dialog.js", - "codemirror/addon/edit/closetag.js", - "codemirror/addon/edit/trailingspace.js", - "codemirror/addon/selection/active-line.js", - "codemirror/addon/hint/show-hint.js", + "codemirror5", + "codemirror5/mode/gfm/gfm.js", + "codemirror5/mode/css/css.js", + "codemirror5/mode/javascript/javascript.js", + "codemirror5/addon/fold/foldcode.js", + "codemirror5/addon/fold/foldgutter.js", + "codemirror5/addon/fold/xml-fold.js", + "codemirror5/addon/scroll/scrollpastend.js", + "codemirror5/addon/search/search.js", + "codemirror5/addon/search/searchcursor.js", + "codemirror5/addon/search/jump-to-line.js", + "codemirror5/addon/search/match-highlighter.js", + "codemirror5/addon/search/matchesonscrollbar.js", + "codemirror5/addon/dialog/dialog.js", + "codemirror5/addon/edit/closetag.js", + "codemirror5/addon/edit/trailingspace.js", + "codemirror5/addon/selection/active-line.js", + "codemirror5/addon/hint/show-hint.js", "moment", "superagent", "@sanity/diff-match-patch", diff --git a/server/app.js b/server/app.js index 1bdb5aac3..e07161ede 100644 --- a/server/app.js +++ b/server/app.js @@ -554,7 +554,8 @@ const renderPage = async (req, res)=>{ publicUrl : config.get('publicUrl') ?? '', baseUrl : `${req.protocol}://${req.get('host')}`, environment : nodeEnv, - deployment : config.get('heroku_app_name') ?? '' + deployment : config.get('heroku_app_name') ?? '', + enable_CM6 : config.get('enable_CM6') ?? false }; const props = { version : version,