|
| 1 | +import React, { useCallback, useEffect, useState } from 'react'; |
| 2 | +import CodeMirror from 'codemirror'; |
| 3 | +import styled from 'styled-components/macro'; |
| 4 | +import tw from 'twin.macro'; |
| 5 | +import modes, { Mode } from '@/modes'; |
| 6 | + |
| 7 | +require('codemirror/lib/codemirror.css'); |
| 8 | + |
| 9 | +// Themes |
| 10 | +require('codemirror/theme/ayu-mirage.css'); |
| 11 | + |
| 12 | +// Addons |
| 13 | +require('codemirror/addon/edit/closebrackets'); |
| 14 | +require('codemirror/addon/edit/closetag'); |
| 15 | +require('codemirror/addon/edit/matchbrackets'); |
| 16 | +require('codemirror/addon/edit/matchtags'); |
| 17 | +require('codemirror/addon/edit/trailingspace'); |
| 18 | + |
| 19 | +require('codemirror/addon/fold/foldcode'); |
| 20 | +require('codemirror/addon/fold/foldgutter.css'); |
| 21 | +require('codemirror/addon/fold/foldgutter'); |
| 22 | +require('codemirror/addon/fold/brace-fold'); |
| 23 | +require('codemirror/addon/fold/comment-fold'); |
| 24 | +require('codemirror/addon/fold/indent-fold'); |
| 25 | +require('codemirror/addon/fold/markdown-fold'); |
| 26 | +require('codemirror/addon/fold/xml-fold'); |
| 27 | + |
| 28 | +require('codemirror/addon/hint/css-hint'); |
| 29 | +require('codemirror/addon/hint/html-hint'); |
| 30 | +require('codemirror/addon/hint/javascript-hint'); |
| 31 | +require('codemirror/addon/hint/show-hint.css'); |
| 32 | +require('codemirror/addon/hint/show-hint'); |
| 33 | +require('codemirror/addon/hint/sql-hint'); |
| 34 | +require('codemirror/addon/hint/xml-hint'); |
| 35 | + |
| 36 | +require('codemirror/addon/mode/simple'); |
| 37 | + |
| 38 | +require('codemirror/addon/dialog/dialog.css'); |
| 39 | +require('codemirror/addon/dialog/dialog'); |
| 40 | + |
| 41 | +require('codemirror/addon/scroll/annotatescrollbar'); |
| 42 | +require('codemirror/addon/scroll/scrollpastend'); |
| 43 | +require('codemirror/addon/scroll/simplescrollbars.css'); |
| 44 | +require('codemirror/addon/scroll/simplescrollbars'); |
| 45 | + |
| 46 | +require('codemirror/addon/search/jump-to-line'); |
| 47 | +require('codemirror/addon/search/match-highlighter'); |
| 48 | +require('codemirror/addon/search/matchesonscrollbar.css'); |
| 49 | +require('codemirror/addon/search/matchesonscrollbar'); |
| 50 | +require('codemirror/addon/search/search'); |
| 51 | +require('codemirror/addon/search/searchcursor'); |
| 52 | + |
| 53 | +// Modes |
| 54 | +require('codemirror/mode/brainfuck/brainfuck'); |
| 55 | +require('codemirror/mode/clike/clike'); |
| 56 | +require('codemirror/mode/css/css'); |
| 57 | +require('codemirror/mode/dart/dart'); |
| 58 | +require('codemirror/mode/diff/diff'); |
| 59 | +require('codemirror/mode/dockerfile/dockerfile'); |
| 60 | +require('codemirror/mode/erlang/erlang'); |
| 61 | +require('codemirror/mode/gfm/gfm'); |
| 62 | +require('codemirror/mode/go/go'); |
| 63 | +require('codemirror/mode/handlebars/handlebars'); |
| 64 | +require('codemirror/mode/htmlembedded/htmlembedded'); |
| 65 | +require('codemirror/mode/htmlmixed/htmlmixed'); |
| 66 | +require('codemirror/mode/http/http'); |
| 67 | +require('codemirror/mode/javascript/javascript'); |
| 68 | +require('codemirror/mode/jsx/jsx'); |
| 69 | +require('codemirror/mode/julia/julia'); |
| 70 | +require('codemirror/mode/lua/lua'); |
| 71 | +require('codemirror/mode/markdown/markdown'); |
| 72 | +require('codemirror/mode/nginx/nginx'); |
| 73 | +require('codemirror/mode/perl/perl'); |
| 74 | +require('codemirror/mode/php/php'); |
| 75 | +require('codemirror/mode/properties/properties'); |
| 76 | +require('codemirror/mode/protobuf/protobuf'); |
| 77 | +require('codemirror/mode/pug/pug'); |
| 78 | +require('codemirror/mode/python/python'); |
| 79 | +require('codemirror/mode/rpm/rpm'); |
| 80 | +require('codemirror/mode/ruby/ruby'); |
| 81 | +require('codemirror/mode/rust/rust'); |
| 82 | +require('codemirror/mode/sass/sass'); |
| 83 | +require('codemirror/mode/shell/shell'); |
| 84 | +require('codemirror/mode/smarty/smarty'); |
| 85 | +require('codemirror/mode/sql/sql'); |
| 86 | +require('codemirror/mode/swift/swift'); |
| 87 | +require('codemirror/mode/toml/toml'); |
| 88 | +require('codemirror/mode/twig/twig'); |
| 89 | +require('codemirror/mode/vue/vue'); |
| 90 | +require('codemirror/mode/xml/xml'); |
| 91 | +require('codemirror/mode/yaml/yaml'); |
| 92 | + |
| 93 | +const EditorContainer = styled.div` |
| 94 | + min-height: 16rem; |
| 95 | + height: calc(100vh - 20rem); |
| 96 | + ${tw`relative`}; |
| 97 | +
|
| 98 | + > div { |
| 99 | + ${tw`rounded h-full`}; |
| 100 | + } |
| 101 | +
|
| 102 | + .CodeMirror { |
| 103 | + font-size: 12px; |
| 104 | + line-height: 1.375rem; |
| 105 | + } |
| 106 | +
|
| 107 | + .CodeMirror-linenumber { |
| 108 | + padding: 1px 12px 0 12px !important; |
| 109 | + } |
| 110 | +
|
| 111 | + .CodeMirror-foldmarker { |
| 112 | + color: #CBCCC6; |
| 113 | + text-shadow: none; |
| 114 | + margin-left: 0.25rem; |
| 115 | + margin-right: 0.25rem; |
| 116 | + } |
| 117 | +`; |
| 118 | + |
| 119 | +export interface Props { |
| 120 | + style?: React.CSSProperties; |
| 121 | + initialContent?: string; |
| 122 | + mode: string; |
| 123 | + filename?: string; |
| 124 | + onModeChanged: (mode: string) => void; |
| 125 | + fetchContent: (callback: () => Promise<string>) => void; |
| 126 | + onContentSaved: () => void; |
| 127 | +} |
| 128 | + |
| 129 | +export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { |
| 130 | + const [ editor, setEditor ] = useState<CodeMirror.Editor>(); |
| 131 | + |
| 132 | + const ref = useCallback((node) => { |
| 133 | + if (!node) { |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + const e = CodeMirror.fromTextArea(node, { |
| 138 | + mode: 'text/plain', |
| 139 | + theme: 'ayu-mirage', |
| 140 | + |
| 141 | + indentUnit: 4, |
| 142 | + smartIndent: true, |
| 143 | + tabSize: 4, |
| 144 | + indentWithTabs: true, |
| 145 | + |
| 146 | + lineWrapping: true, |
| 147 | + lineNumbers: true, |
| 148 | + |
| 149 | + foldGutter: true, |
| 150 | + fixedGutter: true, |
| 151 | + |
| 152 | + scrollbarStyle: 'overlay', |
| 153 | + coverGutterNextToScrollbar: false, |
| 154 | + |
| 155 | + readOnly: false, |
| 156 | + |
| 157 | + showCursorWhenSelecting: false, |
| 158 | + |
| 159 | + autofocus: false, |
| 160 | + |
| 161 | + spellcheck: true, |
| 162 | + autocorrect: false, |
| 163 | + autocapitalize: false, |
| 164 | + lint: false, |
| 165 | + |
| 166 | + // This property is actually used, the d.ts file for CodeMirror is incorrect. |
| 167 | + // @ts-ignore |
| 168 | + autoCloseBrackets: true, |
| 169 | + matchBrackets: true, |
| 170 | + |
| 171 | + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], |
| 172 | + }); |
| 173 | + |
| 174 | + setEditor(e); |
| 175 | + }, []); |
| 176 | + |
| 177 | + useEffect(() => { |
| 178 | + if (filename === undefined) { |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + const findModeByFilename = (filename: string): Mode|undefined => { |
| 183 | + for (let i = 0; i < modes.length; i++) { |
| 184 | + const info = modes[i]; |
| 185 | + |
| 186 | + if (info.file && info.file.test(filename)) { |
| 187 | + return info; |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + const dot = filename.lastIndexOf('.'); |
| 192 | + const ext = dot > -1 && filename.substring(dot + 1, filename.length); |
| 193 | + |
| 194 | + if (ext) { |
| 195 | + for (let i = 0; i < modes.length; i++) { |
| 196 | + const info = modes[i]; |
| 197 | + |
| 198 | + if (info.ext) { |
| 199 | + for (let j = 0; j < info.ext.length; j++) { |
| 200 | + if (info.ext[j] === ext) { |
| 201 | + return info; |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + return undefined; |
| 209 | + }; |
| 210 | + |
| 211 | + onModeChanged(findModeByFilename(filename)?.mime || 'text/plain'); |
| 212 | + }, [ filename ]); |
| 213 | + |
| 214 | + useEffect(() => { |
| 215 | + editor && editor.setOption('mode', mode); |
| 216 | + }, [ editor, mode ]); |
| 217 | + |
| 218 | + useEffect(() => { |
| 219 | + editor && editor.setValue(initialContent || ''); |
| 220 | + }, [ editor, initialContent ]); |
| 221 | + |
| 222 | + useEffect(() => { |
| 223 | + if (!editor) { |
| 224 | + fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); |
| 225 | + return; |
| 226 | + } |
| 227 | + |
| 228 | + editor.addKeyMap({ |
| 229 | + 'Ctrl-S': () => onContentSaved(), |
| 230 | + 'Cmd-S': () => onContentSaved(), |
| 231 | + }); |
| 232 | + |
| 233 | + fetchContent(() => Promise.resolve(editor.getValue())); |
| 234 | + }, [ editor, fetchContent, onContentSaved ]); |
| 235 | + |
| 236 | + return ( |
| 237 | + <EditorContainer style={style}> |
| 238 | + <textarea ref={ref}/> |
| 239 | + </EditorContainer> |
| 240 | + ); |
| 241 | +}; |
0 commit comments