From 9d7f4f954e01226de818234047d00a997e5b6cff Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 13 Sep 2020 17:17:40 -0600 Subject: [PATCH] Replace Ace editor with CodeMirror --- package.json | 4 +- .../scripts/components/elements/AceEditor.tsx | 84 ------ .../components/elements/CodemirrorEditor.tsx | 241 ++++++++++++++++++ .../server/files/FileEditContainer.tsx | 15 +- resources/scripts/modes.d.ts | 12 +- resources/scripts/modes.js | 63 +++-- yarn.lock | 24 ++ 7 files changed, 325 insertions(+), 118 deletions(-) delete mode 100644 resources/scripts/components/elements/AceEditor.tsx create mode 100644 resources/scripts/components/elements/CodemirrorEditor.tsx diff --git a/package.json b/package.json index e939af813..15e96c4bd 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,8 @@ "@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/react-fontawesome": "0.1.4", "axios": "^0.19.2", - "ayu-ace": "^2.0.4", - "brace": "^0.11.1", "chart.js": "^2.8.0", + "codemirror": "^5.57.0", "date-fns": "^2.14.0", "debounce": "^1.2.0", "deepmerge": "^4.2.2", @@ -57,6 +56,7 @@ "@babel/preset-typescript": "^7.7.4", "@babel/runtime": "^7.7.5", "@types/chart.js": "^2.8.5", + "@types/codemirror": "^0.0.98", "@types/debounce": "^1.2.0", "@types/events": "^3.0.0", "@types/node": "^12.6.9", diff --git a/resources/scripts/components/elements/AceEditor.tsx b/resources/scripts/components/elements/AceEditor.tsx deleted file mode 100644 index 47fba4edb..000000000 --- a/resources/scripts/components/elements/AceEditor.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import ace, { Editor } from 'brace'; -import styled from 'styled-components/macro'; -import tw from 'twin.macro'; -import modes from '@/modes'; - -// @ts-ignore -require('brace/ext/modelist'); -require('ayu-ace/mirage'); - -const EditorContainer = styled.div` - min-height: 16rem; - height: calc(100vh - 20rem); - ${tw`relative`}; - - #editor { - ${tw`rounded h-full`}; - } -`; - -Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`)); -const modelist = ace.acequire('ace/ext/modelist'); - -export interface Props { - style?: React.CSSProperties; - initialContent?: string; - mode: string; - filename?: string; - onModeChanged: (mode: string) => void; - fetchContent: (callback: () => Promise) => void; - onContentSaved: () => void; -} - -export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { - const [ editor, setEditor ] = useState(); - const ref = useCallback(node => { - if (node) setEditor(ace.edit('editor')); - }, []); - - useEffect(() => { - if (modelist && filename) { - onModeChanged(modelist.getModeForPath(filename).mode.replace(/^ace\/mode\//, '')); - } - }, [ filename ]); - - useEffect(() => { - editor && editor.session.setMode(`ace/mode/${mode}`); - }, [ editor, mode ]); - - useEffect(() => { - editor && editor.session.setValue(initialContent || ''); - }, [ editor, initialContent ]); - - useEffect(() => { - if (!editor) { - fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); - return; - } - - editor.setTheme('ace/theme/ayu-mirage'); - - editor.$blockScrolling = Infinity; - editor.container.style.lineHeight = '1.375rem'; - editor.container.style.fontWeight = '500'; - editor.renderer.updateFontSize(); - editor.renderer.setShowPrintMargin(false); - editor.session.setTabSize(4); - editor.session.setUseSoftTabs(true); - - editor.commands.addCommand({ - name: 'Save', - bindKey: { win: 'Ctrl-s', mac: 'Command-s' }, - exec: () => onContentSaved(), - }); - - fetchContent(() => Promise.resolve(editor.session.getValue())); - }, [ editor, fetchContent, onContentSaved ]); - - return ( - -
- - ); -}; diff --git a/resources/scripts/components/elements/CodemirrorEditor.tsx b/resources/scripts/components/elements/CodemirrorEditor.tsx new file mode 100644 index 000000000..efe3f1571 --- /dev/null +++ b/resources/scripts/components/elements/CodemirrorEditor.tsx @@ -0,0 +1,241 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import CodeMirror from 'codemirror'; +import styled from 'styled-components/macro'; +import tw from 'twin.macro'; +import modes, { Mode } from '@/modes'; + +require('codemirror/lib/codemirror.css'); + +// Themes +require('codemirror/theme/ayu-mirage.css'); + +// Addons +require('codemirror/addon/edit/closebrackets'); +require('codemirror/addon/edit/closetag'); +require('codemirror/addon/edit/matchbrackets'); +require('codemirror/addon/edit/matchtags'); +require('codemirror/addon/edit/trailingspace'); + +require('codemirror/addon/fold/foldcode'); +require('codemirror/addon/fold/foldgutter.css'); +require('codemirror/addon/fold/foldgutter'); +require('codemirror/addon/fold/brace-fold'); +require('codemirror/addon/fold/comment-fold'); +require('codemirror/addon/fold/indent-fold'); +require('codemirror/addon/fold/markdown-fold'); +require('codemirror/addon/fold/xml-fold'); + +require('codemirror/addon/hint/css-hint'); +require('codemirror/addon/hint/html-hint'); +require('codemirror/addon/hint/javascript-hint'); +require('codemirror/addon/hint/show-hint.css'); +require('codemirror/addon/hint/show-hint'); +require('codemirror/addon/hint/sql-hint'); +require('codemirror/addon/hint/xml-hint'); + +require('codemirror/addon/mode/simple'); + +require('codemirror/addon/dialog/dialog.css'); +require('codemirror/addon/dialog/dialog'); + +require('codemirror/addon/scroll/annotatescrollbar'); +require('codemirror/addon/scroll/scrollpastend'); +require('codemirror/addon/scroll/simplescrollbars.css'); +require('codemirror/addon/scroll/simplescrollbars'); + +require('codemirror/addon/search/jump-to-line'); +require('codemirror/addon/search/match-highlighter'); +require('codemirror/addon/search/matchesonscrollbar.css'); +require('codemirror/addon/search/matchesonscrollbar'); +require('codemirror/addon/search/search'); +require('codemirror/addon/search/searchcursor'); + +// Modes +require('codemirror/mode/brainfuck/brainfuck'); +require('codemirror/mode/clike/clike'); +require('codemirror/mode/css/css'); +require('codemirror/mode/dart/dart'); +require('codemirror/mode/diff/diff'); +require('codemirror/mode/dockerfile/dockerfile'); +require('codemirror/mode/erlang/erlang'); +require('codemirror/mode/gfm/gfm'); +require('codemirror/mode/go/go'); +require('codemirror/mode/handlebars/handlebars'); +require('codemirror/mode/htmlembedded/htmlembedded'); +require('codemirror/mode/htmlmixed/htmlmixed'); +require('codemirror/mode/http/http'); +require('codemirror/mode/javascript/javascript'); +require('codemirror/mode/jsx/jsx'); +require('codemirror/mode/julia/julia'); +require('codemirror/mode/lua/lua'); +require('codemirror/mode/markdown/markdown'); +require('codemirror/mode/nginx/nginx'); +require('codemirror/mode/perl/perl'); +require('codemirror/mode/php/php'); +require('codemirror/mode/properties/properties'); +require('codemirror/mode/protobuf/protobuf'); +require('codemirror/mode/pug/pug'); +require('codemirror/mode/python/python'); +require('codemirror/mode/rpm/rpm'); +require('codemirror/mode/ruby/ruby'); +require('codemirror/mode/rust/rust'); +require('codemirror/mode/sass/sass'); +require('codemirror/mode/shell/shell'); +require('codemirror/mode/smarty/smarty'); +require('codemirror/mode/sql/sql'); +require('codemirror/mode/swift/swift'); +require('codemirror/mode/toml/toml'); +require('codemirror/mode/twig/twig'); +require('codemirror/mode/vue/vue'); +require('codemirror/mode/xml/xml'); +require('codemirror/mode/yaml/yaml'); + +const EditorContainer = styled.div` + min-height: 16rem; + height: calc(100vh - 20rem); + ${tw`relative`}; + + > div { + ${tw`rounded h-full`}; + } + + .CodeMirror { + font-size: 12px; + line-height: 1.375rem; + } + + .CodeMirror-linenumber { + padding: 1px 12px 0 12px !important; + } + + .CodeMirror-foldmarker { + color: #CBCCC6; + text-shadow: none; + margin-left: 0.25rem; + margin-right: 0.25rem; + } +`; + +export interface Props { + style?: React.CSSProperties; + initialContent?: string; + mode: string; + filename?: string; + onModeChanged: (mode: string) => void; + fetchContent: (callback: () => Promise) => void; + onContentSaved: () => void; +} + +export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { + const [ editor, setEditor ] = useState(); + + const ref = useCallback((node) => { + if (!node) { + return; + } + + const e = CodeMirror.fromTextArea(node, { + mode: 'text/plain', + theme: 'ayu-mirage', + + indentUnit: 4, + smartIndent: true, + tabSize: 4, + indentWithTabs: true, + + lineWrapping: true, + lineNumbers: true, + + foldGutter: true, + fixedGutter: true, + + scrollbarStyle: 'overlay', + coverGutterNextToScrollbar: false, + + readOnly: false, + + showCursorWhenSelecting: false, + + autofocus: false, + + spellcheck: true, + autocorrect: false, + autocapitalize: false, + lint: false, + + // This property is actually used, the d.ts file for CodeMirror is incorrect. + // @ts-ignore + autoCloseBrackets: true, + matchBrackets: true, + + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], + }); + + setEditor(e); + }, []); + + useEffect(() => { + if (filename === undefined) { + return; + } + + const findModeByFilename = (filename: string): Mode|undefined => { + for (let i = 0; i < modes.length; i++) { + const info = modes[i]; + + if (info.file && info.file.test(filename)) { + return info; + } + } + + const dot = filename.lastIndexOf('.'); + const ext = dot > -1 && filename.substring(dot + 1, filename.length); + + if (ext) { + for (let i = 0; i < modes.length; i++) { + const info = modes[i]; + + if (info.ext) { + for (let j = 0; j < info.ext.length; j++) { + if (info.ext[j] === ext) { + return info; + } + } + } + } + } + + return undefined; + }; + + onModeChanged(findModeByFilename(filename)?.mime || 'text/plain'); + }, [ filename ]); + + useEffect(() => { + editor && editor.setOption('mode', mode); + }, [ editor, mode ]); + + useEffect(() => { + editor && editor.setValue(initialContent || ''); + }, [ editor, initialContent ]); + + useEffect(() => { + if (!editor) { + fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); + return; + } + + editor.addKeyMap({ + 'Ctrl-S': () => onContentSaved(), + 'Cmd-S': () => onContentSaved(), + }); + + fetchContent(() => Promise.resolve(editor.getValue())); + }, [ editor, fetchContent, onContentSaved ]); + + return ( + +