Update to use the Monaco editor (#7)
This commit is contained in:
@@ -1,114 +1,127 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ReactEditor from 'react-simple-code-editor';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import history from 'history/browser';
|
||||
import EditorPrismStyle from './EditorPrismStyle';
|
||||
import { getHighlighter } from '../util/highlighting';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import styled from 'styled-components';
|
||||
import themes, { makeMonacoTheme } from '../style/themes';
|
||||
|
||||
export default function EditorTextArea({ code, setCode, language, fontSize }) {
|
||||
const [isSelected, isSelectionMiddle, toggleSelected] = useSelectedLine();
|
||||
const highlight = getHighlighter(language);
|
||||
export default function EditorTextArea({
|
||||
forcedContent,
|
||||
setActualContent,
|
||||
theme,
|
||||
language,
|
||||
fontSize,
|
||||
}) {
|
||||
const [editor, setEditor] = useState();
|
||||
const [monaco, setMonaco] = useState();
|
||||
const [selected, toggleSelected] = useSelectedLine();
|
||||
const editorAreaRef = useRef();
|
||||
useLineNumberMagic(editorAreaRef, selected, toggleSelected, editor, monaco);
|
||||
|
||||
function highlightWithLineNumbers(input, grammar) {
|
||||
return highlight(input, grammar)
|
||||
.split(/\r?\n/)
|
||||
.map((line, i) => (
|
||||
<span key={i}>
|
||||
<LineNumber
|
||||
lineNo={i + 1}
|
||||
selected={isSelected(i + 1)}
|
||||
shouldScroll={isSelectionMiddle(i + 1)}
|
||||
toggleSelected={toggleSelected}
|
||||
/>
|
||||
<span dangerouslySetInnerHTML={{ __html: line }} />
|
||||
</span>
|
||||
))
|
||||
.reduce((acc, curr, idx) => {
|
||||
if (idx !== 0) {
|
||||
acc.push('\n');
|
||||
}
|
||||
acc.push(curr);
|
||||
return acc;
|
||||
}, []);
|
||||
function beforeMount(monaco) {
|
||||
for (const [id, theme] of Object.entries(themes)) {
|
||||
monaco.editor.defineTheme(id, makeMonacoTheme(theme));
|
||||
}
|
||||
}
|
||||
|
||||
const autoBracketState = useState(null);
|
||||
const editorRef = useRef();
|
||||
function keydown(e) {
|
||||
handleKeydown(e, editorRef.current, autoBracketState);
|
||||
function onMount(editor, monaco) {
|
||||
setEditor(editor);
|
||||
setMonaco(monaco);
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
const onChange = useCallback(
|
||||
value => {
|
||||
setActualContent(value);
|
||||
},
|
||||
[setActualContent]
|
||||
);
|
||||
|
||||
return (
|
||||
<EditorPrismStyle>
|
||||
<StyledReactEditor
|
||||
ref={editorRef}
|
||||
value={code}
|
||||
onValueChange={setCode}
|
||||
highlight={highlightWithLineNumbers}
|
||||
placeholder={'Paste (or type) some code...'}
|
||||
padding={10}
|
||||
size={fontSize}
|
||||
textareaId="code-area"
|
||||
autoFocus={true}
|
||||
onKeyDown={keydown}
|
||||
<EditorArea ref={editorAreaRef}>
|
||||
<Editor
|
||||
theme={theme.id}
|
||||
language={language === 'plain' ? 'plaintext' : language}
|
||||
options={{
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: fontSize,
|
||||
fontLigatures: true,
|
||||
wordWrap: true,
|
||||
renderLineHighlight: 'none',
|
||||
detectIndentation: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
beforeMount={beforeMount}
|
||||
onMount={onMount}
|
||||
onChange={onChange}
|
||||
value={forcedContent}
|
||||
/>
|
||||
</EditorPrismStyle>
|
||||
</EditorArea>
|
||||
);
|
||||
}
|
||||
|
||||
const LineNumber = ({ lineNo, selected, shouldScroll, toggleSelected }) => {
|
||||
const autoScroll = useAutoScroll(shouldScroll);
|
||||
const EditorArea = styled.main`
|
||||
margin-top: 2.5em;
|
||||
height: 100%;
|
||||
|
||||
function click(e) {
|
||||
toggleSelected(lineNo, e.shiftKey);
|
||||
.line-numbers {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
return selected ? (
|
||||
<HighlightedLineNumber ref={autoScroll} onClick={click}>
|
||||
{lineNo}
|
||||
</HighlightedLineNumber>
|
||||
) : (
|
||||
<PlainLineNumber ref={autoScroll} onClick={click}>
|
||||
{lineNo}
|
||||
</PlainLineNumber>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledReactEditor = styled(ReactEditor)`
|
||||
counter-reset: line;
|
||||
font-size: ${props => props.size}px;
|
||||
outline: 0;
|
||||
min-height: calc(100vh - 2rem);
|
||||
|
||||
#code-area {
|
||||
outline: none;
|
||||
padding-left: 60px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding-left: 60px !important;
|
||||
.highlighted-line + div {
|
||||
color: ${props => props.theme.editor.lineNumberHl};
|
||||
background-color: ${props => props.theme.editor.lineNumberHlBackground};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
const PlainLineNumber = styled.span`
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
color: ${props => props.theme.editor.lineNumber};
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
font-weight: 100;
|
||||
user-select: none;
|
||||
function useLineNumberMagic(
|
||||
editorAreaRef,
|
||||
selected,
|
||||
toggleSelected,
|
||||
editor,
|
||||
monaco
|
||||
) {
|
||||
// add an event listener for clicking on line numbers
|
||||
useEffect(() => {
|
||||
const node = editorAreaRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// override parent <pre>
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const handler = click => {
|
||||
const target = click?.target;
|
||||
if (target && target.classList.contains('line-numbers')) {
|
||||
toggleSelected(parseInt(target.textContent), click);
|
||||
}
|
||||
};
|
||||
|
||||
const HighlightedLineNumber = styled(PlainLineNumber)`
|
||||
color: ${props => props.theme.editor.lineNumberHl};
|
||||
background-color: ${props => props.theme.editor.lineNumberHlBackground};
|
||||
font-weight: bold;
|
||||
`;
|
||||
node.addEventListener('click', handler);
|
||||
return () => node.removeEventListener('click', handler);
|
||||
}, [editorAreaRef, toggleSelected]);
|
||||
|
||||
// apply a 'highlighed' decoration to the selected lines
|
||||
useEffect(() => {
|
||||
if (!editor || !monaco) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = [];
|
||||
if (selected[0] !== -1) {
|
||||
range.push({
|
||||
range: new monaco.Range(selected[0], 1, selected[1], 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: 'highlighted-line',
|
||||
},
|
||||
});
|
||||
}
|
||||
const decorations = editor.deltaDecorations([], range);
|
||||
|
||||
return () => {
|
||||
editor.deltaDecorations(decorations, []);
|
||||
};
|
||||
}, [editor, monaco, selected]);
|
||||
}
|
||||
|
||||
function useSelectedLine() {
|
||||
// extract highlighted lines from window hash
|
||||
@@ -116,7 +129,7 @@ function useSelectedLine() {
|
||||
const hash = window.location.hash;
|
||||
if (/^#L\d+(-\d+)?$/.test(hash)) {
|
||||
const [start, end] = hash.substring(2).split('-').map(Number);
|
||||
return [start, isNaN(end) ? -1 : end];
|
||||
return [start, isNaN(end) ? start : end];
|
||||
} else {
|
||||
return [-1, -1];
|
||||
}
|
||||
@@ -127,7 +140,7 @@ function useSelectedLine() {
|
||||
let hash = '';
|
||||
|
||||
if (selected[0] !== -1) {
|
||||
if (selected[1] !== -1) {
|
||||
if (selected[1] !== selected[0]) {
|
||||
const start = Math.min(...selected);
|
||||
const end = Math.max(...selected);
|
||||
hash = `#L${start}-${end}`;
|
||||
@@ -140,171 +153,16 @@ function useSelectedLine() {
|
||||
}, [selected]);
|
||||
|
||||
// toggle the highlighting for a given line
|
||||
function toggleSelected(lineNo, shift) {
|
||||
if (selected[0] === lineNo && selected[1] === -1) {
|
||||
function toggleSelected(lineNo, e) {
|
||||
const shift = e.shiftKey;
|
||||
if (selected[0] === lineNo && selected[1] === lineNo) {
|
||||
setSelected([-1, -1]);
|
||||
} else if (selected[0] === -1 || !shift) {
|
||||
setSelected([lineNo, -1]);
|
||||
setSelected([lineNo, lineNo]);
|
||||
} else {
|
||||
setSelected([selected[0], lineNo]);
|
||||
}
|
||||
}
|
||||
|
||||
// should a line be highlighted in the viewer?
|
||||
function isSelected(lineNo) {
|
||||
if (selected[0] === -1) {
|
||||
return false;
|
||||
}
|
||||
if (selected[1] === -1) {
|
||||
return selected[0] === lineNo;
|
||||
}
|
||||
|
||||
return lineNo >= Math.min(...selected) && lineNo <= Math.max(...selected);
|
||||
}
|
||||
|
||||
// is a line in the middle of the selection
|
||||
function isSelectionMiddle(lineNo) {
|
||||
if (selected[0] === -1) {
|
||||
return false;
|
||||
}
|
||||
if (selected[1] === -1) {
|
||||
return selected[0] === lineNo;
|
||||
}
|
||||
|
||||
return (
|
||||
lineNo === Math.floor((Math.min(...selected) + Math.max(...selected)) / 2)
|
||||
);
|
||||
}
|
||||
|
||||
return [isSelected, isSelectionMiddle, toggleSelected];
|
||||
}
|
||||
|
||||
function useAutoScroll(shouldScroll) {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// only attempt to autoscroll if this is the first render.
|
||||
if (!firstRender) {
|
||||
return;
|
||||
}
|
||||
setFirstRender(false);
|
||||
|
||||
if (shouldScroll) {
|
||||
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, [shouldScroll, firstRender]);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
const KEYCODE_ENTER = 13;
|
||||
const KEYCODE_PARENS = 57;
|
||||
const KEYCODE_PARENS_CLOSE = 48;
|
||||
const KEYCODE_BRACKETS = 219;
|
||||
const KEYCODE_BRACKETS_CLOSE = 221;
|
||||
const KEYCODE_QUOTE = 222;
|
||||
const KEYCODE_BACK_QUOTE = 192;
|
||||
|
||||
function getPair({ keyCode, shiftKey }) {
|
||||
if (keyCode === KEYCODE_PARENS && shiftKey) {
|
||||
return ['(', ')'];
|
||||
} else if (keyCode === KEYCODE_BRACKETS) {
|
||||
if (shiftKey) {
|
||||
return ['{', '}'];
|
||||
} else {
|
||||
return ['[', ']'];
|
||||
}
|
||||
} else if (keyCode === KEYCODE_QUOTE) {
|
||||
if (shiftKey) {
|
||||
return ['"', '"'];
|
||||
} else {
|
||||
return ["'", "'"];
|
||||
}
|
||||
} else if (keyCode === KEYCODE_BACK_QUOTE && !shiftKey) {
|
||||
return ['`', '`'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleKeydown(e, editor, [autoBracket, setAutoBracket]) {
|
||||
const { value, selectionStart, selectionEnd } = e.target;
|
||||
if (selectionStart !== selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user types a closing bracket explictly, just jump to after the automatically added one
|
||||
if (
|
||||
selectionStart !== 0 &&
|
||||
autoBracket === e.key &&
|
||||
(e.keyCode === KEYCODE_BRACKETS_CLOSE || e.keyCode === KEYCODE_PARENS_CLOSE)
|
||||
) {
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value: value,
|
||||
selectionStart: selectionStart + 1,
|
||||
selectionEnd: selectionStart + 1,
|
||||
});
|
||||
setAutoBracket(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// reset auto brackets
|
||||
setAutoBracket(null);
|
||||
|
||||
// When entering an open bracket/quote, add the closing one
|
||||
const pair = getPair(e);
|
||||
if (pair) {
|
||||
// don't add double apostrophes if it looks like a sentence
|
||||
if (
|
||||
e.keyCode === KEYCODE_QUOTE &&
|
||||
!e.shiftKey &&
|
||||
selectionStart !== 0 &&
|
||||
/[a-zA-Z]/.test(value.charAt(selectionStart - 1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value:
|
||||
value.substring(0, selectionStart) +
|
||||
pair[0] +
|
||||
pair[1] +
|
||||
value.substring(selectionEnd),
|
||||
selectionStart: selectionStart + 1,
|
||||
selectionEnd: selectionStart + 1,
|
||||
});
|
||||
setAutoBracket(pair[1]);
|
||||
}
|
||||
|
||||
// When pressing enter immediately after an open bracket, automatically add a newline plus extra indent
|
||||
if (
|
||||
e.keyCode === KEYCODE_ENTER &&
|
||||
selectionEnd !== 0 &&
|
||||
value[selectionEnd - 1] === '{'
|
||||
) {
|
||||
const line = editor._getLines(value, selectionStart).pop();
|
||||
const matches = line.match(/^\s+/);
|
||||
const existingIndent = matches ? matches[0] : '';
|
||||
|
||||
const indent = ' ';
|
||||
const updatedValue =
|
||||
value.substring(0, selectionStart) +
|
||||
'\n' +
|
||||
existingIndent +
|
||||
indent +
|
||||
'\n' +
|
||||
existingIndent +
|
||||
value.substring(selectionEnd);
|
||||
const updatedSelection =
|
||||
selectionStart + 1 /* newline */ + existingIndent.length + indent.length;
|
||||
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value: updatedValue,
|
||||
selectionStart: updatedSelection,
|
||||
selectionEnd: updatedSelection,
|
||||
});
|
||||
}
|
||||
return [selected, toggleSelected];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user