2022-01-08 19:10:27 +00:00
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
2021-08-20 23:41:57 +01:00
|
|
|
import history from 'history/browser';
|
2022-01-08 19:10:27 +00:00
|
|
|
import Editor from '@monaco-editor/react';
|
|
|
|
|
import styled from 'styled-components';
|
|
|
|
|
import themes, { makeMonacoTheme } from '../style/themes';
|
|
|
|
|
|
|
|
|
|
export default function EditorTextArea({
|
|
|
|
|
forcedContent,
|
2022-01-08 19:19:16 +00:00
|
|
|
actualContent,
|
2022-01-08 19:10:27 +00:00
|
|
|
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);
|
2022-01-08 19:19:16 +00:00
|
|
|
usePlaceholderText(actualContent, editor, monaco);
|
2022-01-08 19:10:27 +00:00
|
|
|
|
|
|
|
|
function beforeMount(monaco) {
|
|
|
|
|
for (const [id, theme] of Object.entries(themes)) {
|
|
|
|
|
monaco.editor.defineTheme(id, makeMonacoTheme(theme));
|
|
|
|
|
}
|
2021-03-26 22:00:12 +00:00
|
|
|
}
|
|
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
function onMount(editor, monaco) {
|
|
|
|
|
setEditor(editor);
|
|
|
|
|
setMonaco(monaco);
|
|
|
|
|
editor.focus();
|
2021-03-28 17:39:16 +01:00
|
|
|
}
|
|
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
const onChange = useCallback(
|
|
|
|
|
value => {
|
|
|
|
|
setActualContent(value);
|
|
|
|
|
},
|
|
|
|
|
[setActualContent]
|
|
|
|
|
);
|
|
|
|
|
|
2021-03-26 22:00:12 +00:00
|
|
|
return (
|
2022-01-08 19:10:27 +00:00
|
|
|
<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}
|
2021-03-26 22:00:12 +00:00
|
|
|
/>
|
2022-01-08 19:10:27 +00:00
|
|
|
</EditorArea>
|
2021-04-02 13:05:15 +01:00
|
|
|
);
|
2021-03-26 22:00:12 +00:00
|
|
|
}
|
2021-03-27 11:49:46 +00:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
const EditorArea = styled.main`
|
|
|
|
|
margin-top: 2.5em;
|
|
|
|
|
height: 100%;
|
2021-08-20 22:56:15 +01:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
.line-numbers {
|
|
|
|
|
cursor: pointer !important;
|
2021-05-21 11:56:05 +01:00
|
|
|
}
|
|
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
.highlighted-line + div {
|
|
|
|
|
color: ${props => props.theme.editor.lineNumberHl};
|
|
|
|
|
background-color: ${props => props.theme.editor.lineNumberHlBackground};
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
2022-01-08 19:19:16 +00:00
|
|
|
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
position: absolute;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
font-weight: lighter;
|
|
|
|
|
|
|
|
|
|
&::after {
|
|
|
|
|
content: 'Paste (or type) some code...';
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-08 19:10:27 +00:00
|
|
|
`;
|
2021-05-21 11:56:05 +01:00
|
|
|
|
2022-01-08 19:19:16 +00:00
|
|
|
function usePlaceholderText(actualContent, editor, monaco) {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!editor || !monaco || actualContent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const decoration = {
|
|
|
|
|
range: new monaco.Range(1, 1, 1, 1),
|
|
|
|
|
options: {
|
|
|
|
|
after: {
|
|
|
|
|
content: '\u200B',
|
|
|
|
|
inlineClassName: 'placeholder-text',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const decorations = editor.deltaDecorations([], [decoration]);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
editor.deltaDecorations(decorations, []);
|
|
|
|
|
};
|
|
|
|
|
}, [editor, monaco, actualContent]);
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
function useLineNumberMagic(
|
|
|
|
|
editorAreaRef,
|
|
|
|
|
selected,
|
|
|
|
|
toggleSelected,
|
|
|
|
|
editor,
|
|
|
|
|
monaco
|
|
|
|
|
) {
|
|
|
|
|
// add an event listener for clicking on line numbers
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const node = editorAreaRef.current;
|
|
|
|
|
if (!node) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-27 11:49:46 +00:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
const handler = click => {
|
|
|
|
|
const target = click?.target;
|
|
|
|
|
if (target && target.classList.contains('line-numbers')) {
|
|
|
|
|
toggleSelected(parseInt(target.textContent), click);
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-03-27 11:49:46 +00:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
node.addEventListener('click', handler);
|
|
|
|
|
return () => node.removeEventListener('click', handler);
|
|
|
|
|
}, [editorAreaRef, toggleSelected]);
|
2021-03-27 11:49:46 +00:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
// apply a 'highlighed' decoration to the selected lines
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!editor || !monaco) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-21 11:56:05 +01:00
|
|
|
|
2022-01-08 19:19:16 +00:00
|
|
|
const newDecorations = [];
|
2022-01-08 19:10:27 +00:00
|
|
|
if (selected[0] !== -1) {
|
2022-01-08 19:19:16 +00:00
|
|
|
newDecorations.push({
|
2022-01-08 19:10:27 +00:00
|
|
|
range: new monaco.Range(selected[0], 1, selected[1], 1),
|
|
|
|
|
options: {
|
|
|
|
|
isWholeLine: true,
|
|
|
|
|
linesDecorationsClassName: 'highlighted-line',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2022-01-08 19:19:16 +00:00
|
|
|
const decorations = editor.deltaDecorations([], newDecorations);
|
2021-03-28 17:39:16 +01:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
return () => {
|
|
|
|
|
editor.deltaDecorations(decorations, []);
|
|
|
|
|
};
|
|
|
|
|
}, [editor, monaco, selected]);
|
|
|
|
|
}
|
2021-05-21 11:56:05 +01:00
|
|
|
|
|
|
|
|
function useSelectedLine() {
|
|
|
|
|
// extract highlighted lines from window hash
|
|
|
|
|
const [selected, setSelected] = useState(() => {
|
|
|
|
|
const hash = window.location.hash;
|
|
|
|
|
if (/^#L\d+(-\d+)?$/.test(hash)) {
|
|
|
|
|
const [start, end] = hash.substring(2).split('-').map(Number);
|
2022-01-08 19:10:27 +00:00
|
|
|
return [start, isNaN(end) ? start : end];
|
2021-05-21 11:56:05 +01:00
|
|
|
} else {
|
|
|
|
|
return [-1, -1];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// update window hash when a new line is highlighted
|
|
|
|
|
useEffect(() => {
|
2021-08-20 23:41:57 +01:00
|
|
|
let hash = '';
|
|
|
|
|
|
2021-05-21 11:56:05 +01:00
|
|
|
if (selected[0] !== -1) {
|
2022-01-08 19:10:27 +00:00
|
|
|
if (selected[1] !== selected[0]) {
|
2021-05-21 13:57:36 +01:00
|
|
|
const start = Math.min(...selected);
|
|
|
|
|
const end = Math.max(...selected);
|
2021-08-20 23:41:57 +01:00
|
|
|
hash = `#L${start}-${end}`;
|
2021-05-21 11:56:05 +01:00
|
|
|
} else {
|
2021-08-20 23:41:57 +01:00
|
|
|
hash = `#L${selected[0]}`;
|
2021-05-21 11:56:05 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-08-20 23:41:57 +01:00
|
|
|
|
|
|
|
|
history.replace({ hash });
|
2021-05-21 11:56:05 +01:00
|
|
|
}, [selected]);
|
|
|
|
|
|
|
|
|
|
// toggle the highlighting for a given line
|
2022-01-08 19:10:27 +00:00
|
|
|
function toggleSelected(lineNo, e) {
|
|
|
|
|
const shift = e.shiftKey;
|
|
|
|
|
if (selected[0] === lineNo && selected[1] === lineNo) {
|
2021-05-21 11:56:05 +01:00
|
|
|
setSelected([-1, -1]);
|
|
|
|
|
} else if (selected[0] === -1 || !shift) {
|
2022-01-08 19:10:27 +00:00
|
|
|
setSelected([lineNo, lineNo]);
|
2021-05-21 11:56:05 +01:00
|
|
|
} else {
|
|
|
|
|
setSelected([selected[0], lineNo]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
return [selected, toggleSelected];
|
2021-03-28 17:39:16 +01:00
|
|
|
}
|