Update to use the Monaco editor (#7)

This commit is contained in:
lucko
2022-01-08 19:10:27 +00:00
committed by GitHub
parent a0b2db024b
commit ed64391a51
18 changed files with 3714 additions and 6792 deletions

4
.github/banner.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,33 +1,47 @@
<h1 align="center">📋 paste</h1> <p align="center">
<img src=".github/banner.svg">
</p>
**paste is a simple web app for writing & sharing code.** It's my own take on conventional pastebin sites like _pastebin.com_ or _hastebin_. **paste is a simple web app for writing & sharing code.** It's my own take on conventional pastebin sites like _pastebin.com_ or _hastebin_.
The frontend _(this repository)_ is written using the React framework. The backend data storage is handled by a separate web service called [bytebin](https://github.com/lucko/bytebin). The frontend _(this repository)_ is written using the React framework. The backend data storage is handled by a separate web service called [bytebin](https://github.com/lucko/bytebin).
The user-interface is quite simple; it supports syntax highlighting, automatic indentation, many supported languages, themes, zooming in/out, linking to specific lines or sections, and more! The user-interface is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/), the engine behind the popular Visual Studio Code text editor. It's quite simple; it supports syntax highlighting, automatic indentation, many supported languages, themes, zooming in/out, linking to specific lines or sections, and more!
<p align="center"> ## pastes.dev
<img src="https://i.imgur.com/03rBijj.gif">
</p>
## Usage I host a public instance at [pastes.dev](https://pastes.dev). Please feel free to use it to share code/configs/whatever!
I host a public instance of paste at [paste.lucko.me](https://paste.lucko.me). Please feel free to use it to share code/configs/whatever! Please note that the following (very-non-legally worded) [terms of service](https://github.com/lucko/bytebin#public-instances) apply.
If you come across any content which is illegal or infringes on copyright, please [get in touch](https://lucko.me/contact) and let me know so I can remove it.
However please note that the (very-non-legally worded) [terms of service](https://github.com/lucko/bytebin#public-instances) for my public bytebin instance apply here too. If you come across any content which is illegal or infringes on copyright, please [get in touch](https://lucko.me/contact) and let me know so I can remove it. Uploaded content is retained for 90 days then deleted.
Uploaded content is retained for 30 days then deleted. ### pastes.dev API
### Host your own * To **read** content, send a HTTP `GET` request to `https://api.pastes.dev/<key>`.
* Replace `<key>` with the id of the paste.
* The content is returned in the response body.
* The `Content-Type` header is `text/<language>`, where language is the id of the language the paste was saved with.
* To **upload** content, send a HTTP `POST` request to `https://api.pastes.dev/post`.
* Include the content in the request body.
* Specify the language with the `Content-Type: text/<language>` header, and please provide a `User-Agent` header too.
* The paste "key" is returned in the `Location` header, or in the response body as a JSON object in the format `{"key": "<key>"}`.
If you want to host your own paste, first you need to compile it: The API is powered by the [bytebin](https://github.com/lucko/bytebin) service, so more information about how it works can be found there.
## Host your own
It's quite simple to host your own version.
```bash ```bash
git clone https://github.com/lucko/paste git clone https://github.com/lucko/paste
cd paste
yarn install yarn install
# Outputs html/css/js files to /build
yarn build yarn build
# or run the following instead of 'yarn build' to start a webserver for testing/development # Start a webserver for testing/development
yarn start yarn start
``` ```
@@ -41,11 +55,3 @@ docker compose up -d
``` ```
You should then (hopefully!) be able to access the application at `http://localhost:8080/`. You should then (hopefully!) be able to access the application at `http://localhost:8080/`.
## API
paste uses [bytebin](https://github.com/lucko/bytebin) for data storage.
As a result, you can use the [bytebin API](https://github.com/lucko/bytebin#api-usage) to submit/read content programatically.
To set the language of a paste, use the `Content-Type` header with value `text/<language>` (e.g. `Content-Type: text/yaml` for a _.yml_ file).

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.3.1",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
@@ -11,11 +12,9 @@
"history": "^5.0.0", "history": "^5.0.0",
"local-storage": "^2.0.0", "local-storage": "^2.0.0",
"pako": "^2.0.3", "pako": "^2.0.3",
"prismjs": "^1.23.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "5.0.0",
"react-simple-code-editor": "^0.11.0",
"styled-components": "^5.2.1" "styled-components": "^5.2.1"
}, },
"scripts": { "scripts": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/assets/logo256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,27 +2,26 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>paste</title> <title>pastes</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#aaddff" /> <meta name="theme-color" content="#d2a8ff" />
<meta name="description" content="lucko's simple pastebin." /> <meta name="description" content="a simple pastebin." />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="paste" /> <meta name="twitter:title" content="pastes" />
<meta name="twitter:description" content="lucko's simple pastebin." /> <meta name="twitter:description" content="a simple pastebin." />
<meta name="twitter:image" content="%PUBLIC_URL%/assets/logo180.png" /> <meta name="twitter:image" content="%PUBLIC_URL%/assets/logo256.png" />
<meta property="og:title" content="paste" /> <meta property="og:title" content="pastes" />
<meta property="og:description" content="lucko's simple pastebin." /> <meta property="og:description" content="a simple pastebin." />
<meta property="og:type" content="product" /> <meta property="og:type" content="product" />
<meta property="og:image" content="%PUBLIC_URL%/assets/logo180.png" /> <meta property="og:image" content="%PUBLIC_URL%/assets/logo256.png" />
<meta property="og:url" content="https://paste.lucko.me/" /> <meta property="og:url" content="%PUBLIC_URL%" />
<meta property="og:site_name" content="paste" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link href="%PUBLIC_URL%/assets/logo512.png" rel="shortcut icon" sizes="512x512" type="image/png"> <link href="%PUBLIC_URL%/assets/logo512.png" rel="shortcut icon" sizes="512x512" type="image/png">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo256.png" />
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import Editor from './components/Editor'; import Editor from './components/Editor';
import parseContentType from 'content-type-parser'; import parseContentType from 'content-type-parser';
import { languageIds } from './util/highlighting'; import { languageIds } from './util/highlighting';
@@ -22,7 +22,7 @@ async function loadFromBytebin(id) {
resp.headers.get('content-type') resp.headers.get('content-type')
); );
document.title = 'paste | ' + id; document.title = 'pastes | ' + id;
return { ok: true, content, type }; return { ok: true, content, type };
} else { } else {
return { ok: false }; return { ok: false };
@@ -49,13 +49,19 @@ const LOADED = Symbol();
export default function App() { export default function App() {
const [pasteId] = useState(getPasteIdFromUrl); const [pasteId] = useState(getPasteIdFromUrl);
const [state, setState] = useState(INITIAL); const [state, setState] = useState(INITIAL);
const [content, setContent] = useState(''); const [forcedContent, setForcedContent] = useState('');
const [actualContent, setActualContent] = useState('');
const [contentType, setContentType] = useState(); const [contentType, setContentType] = useState();
const setContent = useCallback((content) => {
setActualContent(content);
setForcedContent(content);
}, [setActualContent, setForcedContent]);
useEffect(() => { useEffect(() => {
if (pasteId && state === INITIAL) { if (pasteId && state === INITIAL) {
setState(LOADING); setState(LOADING);
setContent('Loading...'); setForcedContent('Loading...');
loadFromBytebin(pasteId).then(({ ok, content, type }) => { loadFromBytebin(pasteId).then(({ ok, content, type }) => {
if (ok) { if (ok) {
setContent(content); setContent(content);
@@ -72,8 +78,10 @@ export default function App() {
return ( return (
<Editor <Editor
content={content} forcedContent={forcedContent}
setContent={setContent} setForcedContent={setContent}
actualContent={actualContent}
setActualContent={setActualContent}
contentType={contentType} contentType={contentType}
/> />
); );

View File

@@ -6,8 +6,15 @@ import EditorControls from './EditorControls';
import EditorTextArea from './EditorTextArea'; import EditorTextArea from './EditorTextArea';
import themes from '../style/themes'; import themes from '../style/themes';
export default function Editor({ content, setContent, contentType }) { export default function Editor({
forcedContent,
setForcedContent,
actualContent,
setActualContent,
contentType,
}) {
const [language, setLanguage] = useState('plain'); const [language, setLanguage] = useState('plain');
const [theme, setTheme] = usePreference( const [theme, setTheme] = usePreference(
'theme', 'theme',
'dark', 'dark',
@@ -37,8 +44,8 @@ export default function Editor({ content, setContent, contentType }) {
<ThemeProvider theme={themes[theme]}> <ThemeProvider theme={themes[theme]}>
<EditorGlobalStyle /> <EditorGlobalStyle />
<EditorControls <EditorControls
code={content} actualContent={actualContent}
setCode={setContent} setForcedContent={setForcedContent}
language={language} language={language}
setLanguage={setLanguage} setLanguage={setLanguage}
theme={theme} theme={theme}
@@ -46,8 +53,9 @@ export default function Editor({ content, setContent, contentType }) {
zoom={zoom} zoom={zoom}
/> />
<EditorTextArea <EditorTextArea
code={content} forcedContent={forcedContent}
setCode={setContent} setActualContent={setActualContent}
theme={themes[theme]}
language={language} language={language}
fontSize={fontSize} fontSize={fontSize}
/> />

View File

@@ -10,8 +10,8 @@ import themes from '../style/themes';
import { postUrl } from '../util/constants'; import { postUrl } from '../util/constants';
export default function EditorControls({ export default function EditorControls({
code, actualContent,
setCode, setForcedContent,
language, language,
setLanguage, setLanguage,
theme, theme,
@@ -23,23 +23,25 @@ export default function EditorControls({
useEffect(() => { useEffect(() => {
setRecentlySaved(false); setRecentlySaved(false);
}, [code, language]); }, [actualContent, language]);
const save = useCallback(() => { const save = useCallback(() => {
if (!code || recentlySaved) { if (!actualContent || recentlySaved) {
return; return;
} }
setSaving(true); setSaving(true);
saveToBytebin(code, language).then(pasteId => { saveToBytebin(actualContent, language).then(pasteId => {
setSaving(false); setSaving(false);
setRecentlySaved(true); setRecentlySaved(true);
if (pasteId) {
history.replace({ history.replace({
pathname: pasteId, pathname: pasteId,
}); });
copy(window.location.href); copy(window.location.href);
document.title = 'paste | ' + pasteId; document.title = 'paste | ' + pasteId;
}
}); });
}, [code, language, recentlySaved]); }, [actualContent, language, recentlySaved]);
useEffect(() => { useEffect(() => {
const listener = e => { const listener = e => {
@@ -61,7 +63,7 @@ export default function EditorControls({
}, [save, zoom]); }, [save, zoom]);
function reset() { function reset() {
setCode(''); setForcedContent('');
setLanguage('plain'); setLanguage('plain');
history.replace({ history.replace({
pathname: '/', pathname: '/',
@@ -109,6 +111,7 @@ export default function EditorControls({
const Header = styled.header` const Header = styled.header`
position: fixed; position: fixed;
top: 0;
z-index: 2; z-index: 2;
width: 100%; width: 100%;
height: 2em; height: 2em;

View File

@@ -1,178 +0,0 @@
import styled from 'styled-components';
export default function EditorPrismStyle({ children }) {
return <Main>{children}</Main>;
}
const Main = styled.main`
padding-top: 2em;
color: ${props => props.theme.editor.primary};
background: ${props => props.theme.editor.background};
code[class*='language-'],
pre[class*='language-'] {
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
text-shadow: none;
background: ${props => props.theme.editor.selection};
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
text-shadow: none;
background: ${props => props.theme.editor.selection};
}
@media print {
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: ${props => props.theme.editor.comment};
}
.token.punctuation {
color: ${props => props.theme.editor.punctuation};
}
.token.annotation {
color: ${props => props.theme.editor.annotation};
}
.token.namespace {
color: ${props => props.theme.editor.namespace};
}
.token.property,
.token.tag {
color: ${props => props.theme.editor.property};
}
.token.tag .punctuation {
color: ${props => props.theme.editor.primary};
}
.token.script > .token.punctuation {
color: ${props => props.theme.editor.punctuation};
}
.token.tag .attr-value {
color: ${props => props.theme.editor.selector};
}
.token.tag .script {
color: ${props => props.theme.editor.primary};
}
.token.boolean {
color: ${props => props.theme.editor.keyword};
}
.token.constant {
color: ${props => props.theme.editor.constant};
}
.token.number,
.token.symbol,
.token.deleted {
color: ${props => props.theme.editor.number};
}
.token.attr-name {
color: ${props => props.theme.editor.function};
}
.token.selector,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: ${props => props.theme.editor.selector};
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: ${props => props.theme.editor.operator};
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: ${props => props.theme.editor.keyword};
}
.token.comment .token.keyword {
color: ${props => props.theme.editor.primary};
}
.token.comment .token.tag,
.token.comment .token.tag > .token.punctuation {
color: ${props => props.theme.editor.commentTag};
}
.token.function {
color: ${props => props.theme.editor.function};
}
.token.class-name {
color: ${props => props.theme.editor.className};
}
.token.regex,
.token.important,
.token.variable {
color: ${props => props.theme.editor.variable};
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
`;

View File

@@ -1,122 +1,135 @@
import { useState, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import ReactEditor from 'react-simple-code-editor';
import history from 'history/browser'; import history from 'history/browser';
import EditorPrismStyle from './EditorPrismStyle'; import Editor from '@monaco-editor/react';
import { getHighlighter } from '../util/highlighting'; import styled from 'styled-components';
import themes, { makeMonacoTheme } from '../style/themes';
export default function EditorTextArea({ code, setCode, language, fontSize }) { export default function EditorTextArea({
const [isSelected, isSelectionMiddle, toggleSelected] = useSelectedLine(); forcedContent,
const highlight = getHighlighter(language); 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) { function beforeMount(monaco) {
return highlight(input, grammar) for (const [id, theme] of Object.entries(themes)) {
.split(/\r?\n/) monaco.editor.defineTheme(id, makeMonacoTheme(theme));
.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;
}, []);
} }
const autoBracketState = useState(null); function onMount(editor, monaco) {
const editorRef = useRef(); setEditor(editor);
function keydown(e) { setMonaco(monaco);
handleKeydown(e, editorRef.current, autoBracketState); editor.focus();
} }
const onChange = useCallback(
value => {
setActualContent(value);
},
[setActualContent]
);
return ( return (
<EditorPrismStyle> <EditorArea ref={editorAreaRef}>
<StyledReactEditor <Editor
ref={editorRef} theme={theme.id}
value={code} language={language === 'plain' ? 'plaintext' : language}
onValueChange={setCode} options={{
highlight={highlightWithLineNumbers} fontFamily: 'JetBrains Mono',
placeholder={'Paste (or type) some code...'} fontSize: fontSize,
padding={10} fontLigatures: true,
size={fontSize} wordWrap: true,
textareaId="code-area" renderLineHighlight: 'none',
autoFocus={true} detectIndentation: true,
onKeyDown={keydown} tabSize: 2,
}}
beforeMount={beforeMount}
onMount={onMount}
onChange={onChange}
value={forcedContent}
/> />
</EditorPrismStyle> </EditorArea>
); );
} }
const LineNumber = ({ lineNo, selected, shouldScroll, toggleSelected }) => { const EditorArea = styled.main`
const autoScroll = useAutoScroll(shouldScroll); margin-top: 2.5em;
height: 100%;
function click(e) { .line-numbers {
toggleSelected(lineNo, e.shiftKey); cursor: pointer !important;
} }
return selected ? ( .highlighted-line + div {
<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;
}
`;
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;
// override parent <pre>
pointer-events: auto;
cursor: pointer;
`;
const HighlightedLineNumber = styled(PlainLineNumber)`
color: ${props => props.theme.editor.lineNumberHl}; color: ${props => props.theme.editor.lineNumberHl};
background-color: ${props => props.theme.editor.lineNumberHlBackground}; background-color: ${props => props.theme.editor.lineNumberHlBackground};
font-weight: bold; font-weight: bold;
}
`; `;
function useLineNumberMagic(
editorAreaRef,
selected,
toggleSelected,
editor,
monaco
) {
// add an event listener for clicking on line numbers
useEffect(() => {
const node = editorAreaRef.current;
if (!node) {
return;
}
const handler = click => {
const target = click?.target;
if (target && target.classList.contains('line-numbers')) {
toggleSelected(parseInt(target.textContent), click);
}
};
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() { function useSelectedLine() {
// extract highlighted lines from window hash // extract highlighted lines from window hash
const [selected, setSelected] = useState(() => { const [selected, setSelected] = useState(() => {
const hash = window.location.hash; const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) { if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number); const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? -1 : end]; return [start, isNaN(end) ? start : end];
} else { } else {
return [-1, -1]; return [-1, -1];
} }
@@ -127,7 +140,7 @@ function useSelectedLine() {
let hash = ''; let hash = '';
if (selected[0] !== -1) { if (selected[0] !== -1) {
if (selected[1] !== -1) { if (selected[1] !== selected[0]) {
const start = Math.min(...selected); const start = Math.min(...selected);
const end = Math.max(...selected); const end = Math.max(...selected);
hash = `#L${start}-${end}`; hash = `#L${start}-${end}`;
@@ -140,171 +153,16 @@ function useSelectedLine() {
}, [selected]); }, [selected]);
// toggle the highlighting for a given line // toggle the highlighting for a given line
function toggleSelected(lineNo, shift) { function toggleSelected(lineNo, e) {
if (selected[0] === lineNo && selected[1] === -1) { const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]); setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) { } else if (selected[0] === -1 || !shift) {
setSelected([lineNo, -1]); setSelected([lineNo, lineNo]);
} else { } else {
setSelected([selected[0], lineNo]); setSelected([selected[0], lineNo]);
} }
} }
// should a line be highlighted in the viewer? return [selected, toggleSelected];
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,
});
}
} }

View File

@@ -5,8 +5,20 @@ body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
height: 100%;
width: 100%;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
#root {
width: 100vw;
height: 100%;
min-height: 100%;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: hidden;
}

View File

@@ -1,34 +1,36 @@
const themes = { const themes = {
light: { light: {
id: 'light',
primary: '#aaddff', primary: '#aaddff',
secondary: '#022550', secondary: '#022550',
highlight: '#36368c', highlight: '#36368c',
lightOrDark: 'light', lightOrDark: 'light',
editor: { editor: {
background: 'none', background: '#ffffff',
lineNumber: '#cccccc', lineNumber: '#cccccc',
lineNumberHl: 'black', lineNumberHl: '#000000',
lineNumberHlBackground: '#e0f6ff', lineNumberHlBackground: '#e0f6ff',
primary: 'black', primary: '#000000',
selection: '#b3d4fc', selection: '#b3d4fc',
comment: 'slategray', comment: '#708090',
commentTag: '#A082BD', commentTag: '#A082BD',
punctuation: '#999', punctuation: '#999999',
annotation: '#999', annotation: '#999999',
namespace: 'slategray', namespace: '#708090',
property: '#e90', property: '#ee9900',
constant: '#905', constant: '#990055',
number: '#905', number: '#990055',
selector: '#690', selector: '#669900',
operator: '#9a6e3a', operator: '#9a6e3a',
keyword: '#07a', keyword: '#0077aa',
function: '#DD4A68', function: '#DD4A68',
className: '#DD4A68', className: '#DD4A68',
variable: '#e90', variable: '#ee9900',
}, },
}, },
blue: { blue: {
id: 'blue',
primary: '#022550', primary: '#022550',
secondary: '#aaddff', secondary: '#aaddff',
highlight: '#77c8f9', highlight: '#77c8f9',
@@ -37,7 +39,7 @@ const themes = {
editor: { editor: {
background: '#041f29', background: '#041f29',
lineNumber: '#81969A', lineNumber: '#81969A',
lineNumberHl: '#fff', lineNumberHl: '#ffffff',
lineNumberHlBackground: '#0e303e', lineNumberHlBackground: '#0e303e',
primary: '#E0E2E4', primary: '#E0E2E4',
selection: '#E0E2E4', selection: '#E0E2E4',
@@ -58,6 +60,7 @@ const themes = {
}, },
}, },
dark: { dark: {
id: 'dark',
primary: '#c9d1d9', // fg.default primary: '#c9d1d9', // fg.default
secondary: '#010409', // canvas.inset secondary: '#010409', // canvas.inset
highlight: '#161b22', // canvas.overlay highlight: '#161b22', // canvas.overlay
@@ -70,7 +73,6 @@ const themes = {
lineNumberHlBackground: '#161b22', // canvas.overlay lineNumberHlBackground: '#161b22', // canvas.overlay
primary: '#c9d1d9', // fg.default primary: '#c9d1d9', // fg.default
selection: '#c9d1d9', // fg.default selection: '#c9d1d9', // fg.default
comment: '#8b949e', comment: '#8b949e',
commentTag: '#79c0ff', commentTag: '#79c0ff',
punctuation: '#d2a8ff', punctuation: '#d2a8ff',
@@ -90,3 +92,89 @@ const themes = {
}; };
export default themes; export default themes;
export function makeMonacoTheme(theme) {
return {
base: theme.lightOrDark === 'light' ? 'vs' : 'vs-dark',
inherit: true,
rules: [
{
token: '', // minimap
foreground: theme.editor.primary.substring(1),
background: theme.editor.background.substring(1),
},
{
token: 'string',
foreground: theme.editor.selector.substring(1),
},
{
token: 'keyword',
foreground: theme.editor.keyword.substring(1),
},
{
token: 'constant',
foreground: theme.editor.constant.substring(1),
},
{
token: 'number',
foreground: theme.editor.number.substring(1),
},
{
token: 'annotation',
foreground: theme.editor.annotation.substring(1),
},
{
token: 'variable',
foreground: theme.editor.variable.substring(1),
},
{
token: 'operator',
foreground: theme.editor.operator.substring(1),
},
{
token: 'operators',
foreground: theme.editor.operator.substring(1),
},
{
token: 'punctuation',
foreground: theme.editor.operator.substring(1),
},
{
token: 'delimiter',
foreground: theme.editor.punctuation.substring(1),
},
{
token: 'delimiter.square',
foreground: theme.editor.punctuation.substring(1),
},
{
token: 'delimiter.bracket',
foreground: theme.editor.punctuation.substring(1),
},
{
token: 'delimiter.parenthesis',
foreground: theme.editor.punctuation.substring(1),
},
{
token: 'identifier',
foreground: theme.editor.function.substring(1),
},
{
token: 'type',
foreground: theme.editor.className.substring(1),
},
{
token: 'comment',
foreground: theme.editor.comment.substring(1),
},
{
token: 'identifier.java',
foreground: theme.editor.primary.substring(1),
},
],
colors: {
'editor.background': theme.editor.background,
'editor.foreground': theme.editor.primary,
},
};
}

View File

@@ -1,66 +1,24 @@
import {
highlight,
languages as prismLanguages,
} from 'prismjs/components/prism-core';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-diff';
import 'prismjs/components/prism-docker';
import 'prismjs/components/prism-go';
import 'prismjs/components/prism-groovy';
import 'prismjs/components/prism-haskell';
import 'prismjs/components/prism-java';
import 'prismjs/components/prism-javadoclike';
import 'prismjs/components/prism-javadoc';
import 'prismjs/components/prism-javastacktrace';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-kotlin';
import 'prismjs/components/prism-log';
import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-php';
import 'prismjs/components/prism-markup-templating';
import 'prismjs/components/prism-properties';
import 'prismjs/components/prism-protobuf';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-jsx';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-tsx';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-rust';
import 'prismjs/components/prism-sql';
import 'prismjs/components/prism-toml';
import 'prismjs/components/prism-yaml';
export const languages = { export const languages = {
config: ['yaml', 'json', 'toml', 'properties'], config: ['yaml', 'json', 'xml', 'ini'],
logs: ['log', 'javastacktrace'],
code: [ code: [
'java', 'java',
'javascript', 'javascript',
'typescript', 'typescript',
'python', 'python',
'kotlin', 'kotlin',
'clike', 'cpp',
'bash', 'csharp',
'shell',
'ruby', 'ruby',
'rust', 'rust',
'sql', 'sql',
'go', 'go',
'groovy',
'haskell',
], ],
web: ['markup', 'css', 'php', 'jsx', 'tsx'], web: ['html', 'css', 'php'],
misc: ['plain', 'docker', 'diff', 'markdown', 'protobuf'], misc: ['plain', 'dockerfile', 'markdown'],
}; };
export const languageIds = Object.values(languages).flat(1); // missing following the rewrite: toml, properties, log, javastacktrace, groovy, haskell, protobuf
// would be good to add these back with custom language definitions
export function getHighlighter(language) { export const languageIds = Object.values(languages).flat(1);
const grammar = prismLanguages[language] || {};
return input => highlight(input, grammar);
}

9631
yarn.lock

File diff suppressed because it is too large Load Diff