diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index c4189e21d..0c7740638 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -6,138 +6,153 @@ import { foldGutter, foldKeymap } from "@codemirror/language"; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine } from "@codemirror/view"; import { markdown } from "@codemirror/lang-markdown"; import { css } from "@codemirror/lang-css"; +import { oneDark } from "@codemirror/theme-one-dark"; -const CodeEditor = forwardRef(({ value = "", onChange = () => {} }, ref) => { - const editorRef = useRef(null); - const viewRef = useRef(null); +const CodeEditor = forwardRef( + ({ value = "", onChange = () => {}, language, editorTheme, tab, view, style, ...props }, ref) => { + const editorRef = useRef(null); + const viewRef = useRef(null); - // --- init editor --- - useEffect(() => { - if (!editorRef.current) return; + console.log(props); - const updateListener = EditorView.updateListener.of((update) => { - if (update.docChanged) { - onChange(update.state.doc.toString()); + // --- init editor --- + useEffect(() => { + if (!editorRef.current) return; + + 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 = () => { + switch (language) { + case "gfm": + return markdown({ codeLanguages: [] }); // GitHub-flavored Markdown + case "css": + return css(); + default: + return markdown(); + } + }; + + const state = EditorState.create({ + doc: value, + extensions: [ + history(), + keymap.of(defaultKeymap), + customKeymap, + updateListener, + languageExtension(), + highlightActiveLine(), + highlightActiveLineGutter(), + keymap.of(foldKeymap), + foldGutter(), + lineNumbers(), + oneDark, + ], + }); + + viewRef.current = new EditorView({ + state, + parent: editorRef.current, + }); + + return () => viewRef.current?.destroy(); + }, []); + + // --- sync external value --- + 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]); - const boldCommand = (view) => { - const { from, to } = view.state.selection.main; - const selected = view.state.doc.sliceString(from, to); - const text = `**${selected}**`; + // --- exposed API --- + useImperativeHandle(ref, () => ({ + getValue: () => viewRef.current.state.doc.toString(), - view.dispatch({ - changes: { from, to, insert: text }, - selection: { anchor: from + text.length }, - }); + setValue: (text) => { + const view = viewRef.current; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: text }, + }); + }, - return true; - }; + injectText: (text) => { + const view = viewRef.current; + const { from, to } = view.state.selection.main; - 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 }, + }); - view.dispatch({ - changes: { from, to, insert: text }, - selection: { anchor: from + text.length }, - }); + view.focus(); + }, - return true; - }; + getCursorPosition: () => viewRef.current.state.selection.main.head, - const customKeymap = keymap.of([ - { key: "Mod-b", run: boldCommand }, - { key: "Mod-i", run: italicCommand }, - ]); + setCursorPosition: (pos) => { + viewRef.current.dispatch({ selection: { anchor: pos } }); + viewRef.current.focus(); + }, - const state = EditorState.create({ - doc: value, - extensions: [ - history(), - keymap.of(defaultKeymap), - customKeymap, - updateListener, - markdown(), - css(), - highlightActiveLine(), - highlightActiveLineGutter(), - keymap.of(foldKeymap), - foldGutter(), - lineNumbers(), - - ], - }); + undo: () => undo(viewRef.current), + redo: () => redo(viewRef.current), - viewRef.current = new EditorView({ - state, - parent: editorRef.current, - }); + historySize: () => { + const view = viewRef.current; + if (!view) return { done: 0, undone: 0 }; - return () => viewRef.current?.destroy(); - }, []); + const h = view.state.field(historyField, false); + if (!h) return { done: 0, undone: 0 }; - // --- sync external value --- - useEffect(() => { - const view = viewRef.current; - if (!view) return; + return { done: h.done.length, undone: h.undone.length }; + }, - const current = view.state.doc.toString(); - if (value !== current) { - view.dispatch({ - changes: { from: 0, to: current.length, insert: value }, - }); - } - }, [value]); + focus: () => viewRef.current.focus(), + })); - // --- exposed API --- - useImperativeHandle(ref, () => ({ - getValue: () => viewRef.current.state.doc.toString(), + return
; + }, +); - 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; \ No newline at end of file +export default CodeEditor; diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 89d0c9497..be4e8b2f2 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -1,60 +1,81 @@ -@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'; - -//Icon fonts included so they can appear in emoji autosuggest dropdown -@import (less) '@themes/fonts/iconFonts/diceFont.less'; -@import (less) '@themes/fonts/iconFonts/elderberryInn.less'; -@import (less) '@themes/fonts/iconFonts/gameIcons.less'; -@import (less) '@themes/fonts/iconFonts/fontAwesome.less'; +// Icon fonts for emoji/autocomplete +@import (less) "@themes/fonts/iconFonts/diceFont.less"; +@import (less) "@themes/fonts/iconFonts/elderberryInn.less"; +@import (less) "@themes/fonts/iconFonts/gameIcons.less"; +@import (less) "@themes/fonts/iconFonts/fontAwesome.less"; @keyframes sourceMoveAnimation { - 50% { color : white;background-color : red;} - 100% { color : unset;background-color : unset;} + 50% { + color: white; + background-color: red; + } + 100% { + color: unset; + background-color: unset; + } } .codeEditor { - @media screen and (pointer : coarse) { - font-size : 16px; - } - .CodeMirror-foldmarker { - font-family : inherit; - font-weight : 600; - color : grey; - text-shadow : none; + font-family: monospace; + height: 100%; + + @media screen and (pointer: coarse) { + font-size: 16px; } - .CodeMirror-foldgutter { - cursor : pointer; - border-left : 1px solid #EEEEEE; - transition : background 0.1s; - &:hover { background : #DDDDDD; } + /* Line numbers and gutters */ + .cm-gutters { + background-color: #f0f0f0; + color: #555; + border-right: 1px solid #ddd; } - .sourceMoveFlash .CodeMirror-line { - animation-name : sourceMoveAnimation; - animation-duration : 0.4s; + /* Folding gutter */ + .cm-foldGutter { + cursor: pointer; + color: grey; + font-weight: 600; + transition: background 0.1s; + + &:hover { + background: #dddddd; + } } - .CodeMirror-search-field { - width:25em !important; - outline:1px inset #00000055 !important; + /* Active line */ + .cm-activeLine { + background-color: #f5f5f5; } + .cm-activeLineGutter { + background-color: #e0e0e0; + } + + /* Flash animation for source moves */ + .sourceMoveFlash .cm-line { + animation-name: sourceMoveAnimation; + animation-duration: 0.4s; + } + + /* Search input */ + .cm-searchField { + width: 25em !important; + outline: 1px inset #00000055 !important; + } + + /* Tab character visualization (optional) */ //.cm-tab { - // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; + // background: url(...) no-repeat right; //} - //.cm-trailingspace { - // .cm-space { - // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right; - // } + /* Trailing space visualization (optional) */ + //.cm-trailingSpace .cm-space { + // background: url(...) no-repeat right; //} } +/* Emoji preview styling */ .emojiPreview { - font-size : 1.5em; - line-height : 1.2em; -} \ No newline at end of file + font-size: 1.5em; + line-height: 1.2em; +} diff --git a/client/homebrew/editor/editor.less b/client/homebrew/editor/editor.less index 3851b50c5..9db6df26f 100644 --- a/client/homebrew/editor/editor.less +++ b/client/homebrew/editor/editor.less @@ -9,7 +9,7 @@ background:white; .codeEditor { height : calc(100% - 25px); - .CodeMirror { height : 100%; } + .cm-editor { height : 100%; } .pageLine, .snippetLine { background : #33333328; border-top : #333399 solid 1px; diff --git a/package-lock.json b/package-lock.json index 141c8a879..b372be43f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/language": "^6.12.2", "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^20.1.0", @@ -2173,6 +2174,18 @@ "@marijn/find-cluster-break": "^1.0.0" } }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@codemirror/view": { "version": "6.40.0", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", diff --git a/package.json b/package.json index c68c7feab..96a492b18 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/language": "^6.12.2", "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", "@dmsnell/diff-match-patch": "^1.1.0", "@googleapis/drive": "^20.1.0", diff --git a/themes/codeMirror/customEditorStyles.less b/themes/codeMirror/customEditorStyles.less index 8c48c1b43..b6e95d14b 100644 --- a/themes/codeMirror/customEditorStyles.less +++ b/themes/codeMirror/customEditorStyles.less @@ -1,4 +1,4 @@ -.editor .codeEditor .CodeMirror { +.editor .codeEditor .cm-editor { // Themes with dark backgrounds &.cm-s-3024-night, &.cm-s-abbott, diff --git a/themes/codeMirror/customThemes/darkbrewery.css b/themes/codeMirror/customThemes/darkbrewery.css index 6fba4001c..ec043c5ba 100644 --- a/themes/codeMirror/customThemes/darkbrewery.css +++ b/themes/codeMirror/customThemes/darkbrewery.css @@ -15,11 +15,11 @@ --highlight: #bcbcbc; color: #91A6AA; background: var(--bg); - .CodeMirror-scroll { - .CodeMirror-gutters { + .cm-scroller { + .cm-gutters { border-right: 1px solid #555; background: var(--bg); - .CodeMirror-gutter { + .cm-gutter { background-color: var(--bg); &.CodeMirror-foldgutter { cursor: pointer; @@ -31,9 +31,9 @@ } } } - .CodeMirror-lines { + .cm-content { /* Line numbers*/ - .CodeMirror-linenumber.CodeMirror-gutter-elt { + .CodeMirror-linenumber.cm-gutter-elt { background-color: var(--bg); color: #81969A; } diff --git a/themes/codeMirror/customThemes/darkvision.css b/themes/codeMirror/customThemes/darkvision.css index 4c74d105e..40a17ec2b 100644 --- a/themes/codeMirror/customThemes/darkvision.css +++ b/themes/codeMirror/customThemes/darkvision.css @@ -18,13 +18,13 @@ } /* Line number stuff */ - .CodeMirror-gutter-elt { + .cm-gutter-elt { color: #81969A; } .CodeMirror-linenumber { background-color: #0C0C0C; } - .CodeMirror-gutter { + .cm-gutter { background-color: #0C0C0C; }