Files
lucko-paste/src/components/EditorTextArea.tsx

310 lines
7.6 KiB
TypeScript
Raw Normal View History

2022-11-06 22:46:13 +00:00
import Editor, {
BeforeMount,
Monaco,
OnChange,
OnMount,
} from '@monaco-editor/react';
import history from 'history/browser';
2023-01-08 20:33:17 +00:00
import React, {
MutableRefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
2022-01-08 19:10:27 +00:00
import styled from 'styled-components';
2023-05-08 21:18:58 +01:00
import themes, { Theme } from '../style/themes';
2022-11-06 22:46:13 +00:00
import type { editor } from 'monaco-editor';
2023-01-08 20:33:17 +00:00
import { ResetFunction } from './Editor';
2023-12-22 21:58:35 +00:00
import { logLanguage } from '../util/log-language';
2022-11-06 22:46:13 +00:00
export interface EditorTextAreaProps {
forcedContent: string;
actualContent: string;
setActualContent: (value: string) => void;
theme: Theme;
language: string;
fontSize: number;
readOnly: boolean;
2023-01-08 20:33:17 +00:00
resetFunction: MutableRefObject<ResetFunction | undefined>;
2022-11-06 22:46:13 +00:00
}
2022-01-08 19:10:27 +00:00
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,
2022-01-27 23:26:16 +00:00
readOnly,
2023-01-08 20:33:17 +00:00
resetFunction,
2022-11-06 22:46:13 +00:00
}: EditorTextAreaProps) {
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const [monaco, setMonaco] = useState<Monaco>();
2022-01-08 19:10:27 +00:00
const [selected, toggleSelected] = useSelectedLine();
2022-11-06 22:46:13 +00:00
const editorAreaRef = useRef<HTMLDivElement>(null);
2022-01-08 22:59:01 +00:00
useLineNumberMagic(
editorAreaRef,
selected,
toggleSelected,
forcedContent,
editor,
monaco
);
2022-11-06 22:46:13 +00:00
2022-01-08 19:19:16 +00:00
usePlaceholderText(actualContent, editor, monaco);
2022-01-08 19:10:27 +00:00
2022-11-06 22:46:13 +00:00
const beforeMount: BeforeMount = monaco => {
2023-05-08 21:18:58 +01:00
for (const theme of Object.values(themes) as Theme[]) {
monaco.editor.defineTheme(theme.id, theme.editor);
2022-01-08 19:10:27 +00:00
}
2023-12-22 21:58:35 +00:00
monaco.languages.register({ id: 'log' });
monaco.languages.setMonarchTokensProvider('log', logLanguage);
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
});
2022-11-06 22:46:13 +00:00
};
2021-03-26 22:00:12 +00:00
2022-11-06 22:46:13 +00:00
const onMount: OnMount = (editor, monaco) => {
editor.addAction({
id: 'search',
label: 'Search with Google',
contextMenuGroupId: '9_cutcopypaste',
contextMenuOrder: 5,
2022-11-06 22:46:13 +00:00
run: editor => {
const selection = editor.getSelection();
if (selection && !selection.isEmpty()) {
const model = editor.getModel();
if (model) {
const query = model.getValueInRange(selection);
window.open('https://www.google.com/search?q=' + query, '_blank');
}
}
2022-11-06 22:46:13 +00:00
},
});
2022-01-08 19:10:27 +00:00
setEditor(editor);
setMonaco(monaco);
2023-01-08 20:33:17 +00:00
resetFunction.current = () => {
editor.setValue('');
editor.focus();
};
2022-01-08 19:10:27 +00:00
editor.focus();
2022-11-06 22:46:13 +00:00
};
2021-03-28 17:39:16 +01:00
2022-11-06 22:46:13 +00:00
const onChange: OnChange = useCallback(
2022-01-08 19:10:27 +00:00
value => {
2022-11-06 22:46:13 +00:00
setActualContent(value as string);
2022-01-08 19:10:27 +00:00
},
[setActualContent]
);
2022-01-09 20:37:39 +00:00
// detect indentation whenever new forced content is set
useEffect(() => {
if (!editor) {
return;
}
2022-11-06 22:46:13 +00:00
const model = editor.getModel();
if (!model) {
return;
}
model.detectIndentation(true, 2);
}, [editor, forcedContent]);
2022-01-09 20:37:39 +00:00
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,
2022-11-06 22:46:13 +00:00
wordWrap: 'on',
2022-01-08 19:10:27 +00:00
renderLineHighlight: 'none',
2022-11-06 22:46:13 +00:00
renderValidationDecorations: 'off',
2022-01-27 23:26:16 +00:00
readOnly,
2022-01-27 23:58:38 +00:00
domReadOnly: readOnly,
2022-01-08 19:10:27 +00:00
}}
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%;
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 {
2023-05-08 21:18:58 +01:00
color: ${props => props.theme.highlightedLine.color};
background-color: ${props => props.theme.highlightedLine.backgroundColor};
2022-01-08 19:10:27 +00:00
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-11-06 22:46:13 +00:00
function usePlaceholderText(
actualContent: string,
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined
) {
2022-01-08 19:19:16 +00:00
useEffect(() => {
if (!editor || !monaco || actualContent) {
return;
}
const decoration = {
2022-11-06 22:46:13 +00:00
range: new monaco.Range(1, 0, 1, 1),
2022-01-08 19:19:16 +00:00
options: {
after: {
content: '\u200B',
inlineClassName: 'placeholder-text',
},
},
};
const decorations = editor.deltaDecorations([], [decoration]);
return () => {
editor.deltaDecorations(decorations, []);
};
}, [editor, monaco, actualContent]);
}
2022-11-06 22:46:13 +00:00
type SelectedLine = [number, number];
type ToggleSelectedFunction = (lineNo: number, e: MouseEvent) => void;
function useSelectedLine(): [SelectedLine, ToggleSelectedFunction] {
// extract highlighted lines from window hash
const [selected, setSelected] = useState<[number, number]>(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? start : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
let hash = '';
if (selected[0] !== -1) {
if (selected[1] !== selected[0]) {
const start = Math.min(...selected);
const end = Math.max(...selected);
hash = `#L${start}-${end}`;
} else {
hash = `#L${selected[0]}`;
}
}
history.replace({ hash });
}, [selected]);
// toggle the highlighting for a given line
const toggleSelected = useCallback(
(lineNo: number, e: MouseEvent) => {
const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, lineNo]);
} else {
setSelected([selected[0], lineNo]);
}
},
[selected, setSelected]
);
return [selected, toggleSelected];
}
2022-01-08 19:10:27 +00:00
function useLineNumberMagic(
2022-11-06 22:46:13 +00:00
editorAreaRef: React.RefObject<HTMLDivElement>,
selected: SelectedLine,
toggleSelected: ToggleSelectedFunction,
forcedContent: string,
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined
2022-01-08 19:10:27 +00:00
) {
// 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-11-06 22:46:13 +00:00
const handler = (click: MouseEvent) => {
const target = click?.target as HTMLElement;
if (
target &&
target.classList.contains('line-numbers') &&
target.textContent
) {
2022-01-08 19:10:27 +00:00
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
2022-01-08 22:01:01 +00:00
// and scroll them into view if not already
2022-01-08 19:10:27 +00:00
useEffect(() => {
2022-01-08 22:01:01 +00:00
if (!editor || !monaco || selected[0] === -1) {
2022-01-08 19:10:27 +00:00
return;
}
2021-05-21 11:56:05 +01:00
2022-01-08 22:01:01 +00:00
// apply a decoration
2022-01-08 22:59:01 +00:00
const newDecorations = [
{
range: new monaco.Range(selected[0], 1, selected[1], 1),
options: {
isWholeLine: true,
linesDecorationsClassName: 'highlighted-line',
},
2022-01-08 22:01:01 +00:00
},
2022-01-08 22:59:01 +00:00
];
2022-01-08 19:19:16 +00:00
const decorations = editor.deltaDecorations([], newDecorations);
2021-03-28 17:39:16 +01:00
2022-01-08 22:01:01 +00:00
// scroll the selected line into view
if (selected[0] === selected[1]) {
editor.revealLineInCenterIfOutsideViewport(selected[0]);
} else {
editor.revealLinesInCenterIfOutsideViewport(selected[0], selected[1]);
}
// cleanup by removing the decorations
2022-01-08 19:10:27 +00:00
return () => {
editor.deltaDecorations(decorations, []);
};
2022-01-08 22:01:01 +00:00
}, [editor, monaco, selected, forcedContent]);
2022-01-08 19:10:27 +00:00
}