2022-11-06 22:46:13 +00:00
|
|
|
import Editor, {
|
|
|
|
|
BeforeMount,
|
|
|
|
|
Monaco,
|
|
|
|
|
OnChange,
|
|
|
|
|
OnMount,
|
|
|
|
|
} from '@monaco-editor/react';
|
2021-08-20 23:41:57 +01:00
|
|
|
import history from 'history/browser';
|
2025-01-05 12:02:59 +00:00
|
|
|
import {
|
|
|
|
|
MutableRefObject,
|
|
|
|
|
RefObject,
|
|
|
|
|
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';
|
2024-11-27 00:29:27 +01:00
|
|
|
import { diffLanguage } from '../util/languages/diff';
|
2024-12-08 22:11:46 +00:00
|
|
|
import { logLanguage } from '../util/languages/log';
|
|
|
|
|
import { ResetFunction } from './Editor';
|
2022-11-06 22:46:13 +00:00
|
|
|
|
2024-08-24 11:05:21 +01:00
|
|
|
import { loader } from '@monaco-editor/react';
|
2024-12-08 22:11:46 +00:00
|
|
|
import * as monaco from 'monaco-editor';
|
2024-08-24 11:05:21 +01:00
|
|
|
|
2025-01-05 12:02:59 +00:00
|
|
|
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
|
|
|
|
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
|
|
|
|
|
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
|
|
|
|
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
|
|
|
|
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
|
|
|
|
|
|
|
|
|
self.MonacoEnvironment = {
|
|
|
|
|
getWorker(_: string, label: string): Promise<Worker> | Worker {
|
|
|
|
|
switch (label) {
|
|
|
|
|
case 'json':
|
|
|
|
|
return new jsonWorker();
|
|
|
|
|
case 'css':
|
|
|
|
|
case 'scss':
|
|
|
|
|
case 'less':
|
|
|
|
|
return new cssWorker();
|
|
|
|
|
case 'html':
|
|
|
|
|
case 'handlebars':
|
|
|
|
|
case 'razor':
|
|
|
|
|
return new htmlWorker();
|
|
|
|
|
case 'typescript':
|
|
|
|
|
case 'javascript':
|
|
|
|
|
return new tsWorker();
|
|
|
|
|
default:
|
|
|
|
|
return new editorWorker();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-08-24 11:05:21 +01:00
|
|
|
loader.config({ monaco });
|
|
|
|
|
|
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;
|
2025-01-05 12:02:59 +00:00
|
|
|
resetFunction: MutableRefObject<ResetFunction | null>;
|
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
|
|
|
}
|
2022-01-28 00:56:50 +00:00
|
|
|
|
2023-12-22 21:58:35 +00:00
|
|
|
monaco.languages.register({ id: 'log' });
|
|
|
|
|
monaco.languages.setMonarchTokensProvider('log', logLanguage);
|
2024-11-27 00:29:27 +01:00
|
|
|
monaco.languages.register({ id: 'diff' });
|
|
|
|
|
monaco.languages.setMonarchTokensProvider('diff', diffLanguage);
|
2023-12-22 21:58:35 +00:00
|
|
|
|
2022-01-28 00:56:50 +00:00
|
|
|
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) => {
|
2022-01-28 00:56:50 +00:00
|
|
|
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-01-28 00:56:50 +00:00
|
|
|
}
|
2022-11-06 22:46:13 +00:00
|
|
|
},
|
|
|
|
|
});
|
2022-01-28 00:56:50 +00:00
|
|
|
|
2022-01-08 19:10:27 +00:00
|
|
|
setEditor(editor);
|
|
|
|
|
setMonaco(monaco);
|
2022-01-28 00:56:50 +00:00
|
|
|
|
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%;
|
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 {
|
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(
|
2024-12-08 22:11:46 +00:00
|
|
|
editorAreaRef: RefObject<HTMLDivElement | null>,
|
2022-11-06 22:46:13 +00:00
|
|
|
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) => {
|
2024-08-24 11:05:21 +01:00
|
|
|
const element = document.elementFromPoint(click.x, click.y);
|
2022-11-06 22:46:13 +00:00
|
|
|
if (
|
2024-08-24 11:05:21 +01:00
|
|
|
element &&
|
|
|
|
|
element.classList.contains('line-numbers') &&
|
|
|
|
|
element.textContent
|
2022-11-06 22:46:13 +00:00
|
|
|
) {
|
2024-08-24 11:05:21 +01:00
|
|
|
toggleSelected(parseInt(element.textContent), click);
|
2022-01-08 19:10:27 +00:00
|
|
|
}
|
|
|
|
|
};
|
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
|
|
|
}
|