diff --git a/src/components/code-editor/index.jsx b/src/components/code-editor/index.jsx
index 8a051def2..68403c0ba 100644
--- a/src/components/code-editor/index.jsx
+++ b/src/components/code-editor/index.jsx
@@ -1,7 +1,7 @@
-import { useState, useRef, useEffect } from 'preact/hooks';
+import { useRef, useEffect } from 'preact/hooks';
import { EditorView } from 'codemirror';
import { lineNumbers, keymap, highlightActiveLineGutter, highlightActiveLine } from '@codemirror/view';
-import { EditorState } from '@codemirror/state';
+import { EditorState, Transaction } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { syntaxHighlighting, HighlightStyle, indentUnit, bracketMatching } from '@codemirror/language';
@@ -26,21 +26,39 @@ const highlightStyle = HighlightStyle.define([
{ tag: tags.invalid, class: 'cm-invalid' }
]);
+/**
+ * @param {object} props
+ * @param {string} props.editorCode
+ * @param {(value: string) => void} props.onInput
+ * @param {string} props.slug
+ * @param {string} [props.class]
+ */
export default function CodeEditor(props) {
const editorParent = useRef(null);
+ /** @type {{ current: EditorView | null }} */
const editor = useRef(null);
- // eslint-disable-next-line no-unused-vars
- const [_, setEditor] = useState(null);
+
+ const routeHasChanged = useRef(false);
+
+ useEffect(() => {
+ if (props.slug || !editor.current) routeHasChanged.current = true;
+ }, [props.slug]);
useEffect(() => {
- console.log('editor code:\n', props.value);
- if (editor.current && !props.baseExampleSlug) return;
- if (editor.current) editor.current.destroy();
+ if (routeHasChanged.current === false) return;
+ routeHasChanged.current = false;
+
+ if (editor.current) {
+ editor.current.dispatch({
+ changes: { from: 0, to: editor.current.state.doc.length, insert: props.editorCode }
+ });
+ return;
+ }
const theme = EditorView.theme({}, { dark: true });
const state = EditorState.create({
- doc: props.value,
+ doc: props.editorCode,
extensions: [
lineNumbers(),
highlightActiveLine(),
@@ -54,8 +72,9 @@ export default function CodeEditor(props) {
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
[theme, syntaxHighlighting(highlightStyle, { fallback: true })],
EditorView.updateListener.of(update => {
- if (update.docChanged) {
- if (props.onInput) props.onInput({ value: update.state.doc.toString() });
+ // Ignores changes from swapping out the editor code programmatically
+ if (isViewUpdateFromUserInput(update)) {
+ props.onInput(update.state.doc.toString());
}
})
]
@@ -65,16 +84,23 @@ export default function CodeEditor(props) {
state,
parent: editorParent.current
});
-
- setEditor(editor.current);
- }, [props.baseExampleSlug]);
+ }, [props.editorCode]);
useEffect(() => (
() => {
- editor.current.destroy();
- setEditor(null);
+ if (editor.current) editor.current.destroy();
}
), []);
return
;
}
+
+/** @param {import('@codemirror/view').ViewUpdate} viewUpdate */
+function isViewUpdateFromUserInput(viewUpdate) {
+ if (viewUpdate.docChanged) {
+ for (const transaction of viewUpdate.transactions) {
+ if (transaction.annotation(Transaction.userEvent)) return true;
+ }
+ }
+ return false;
+}
diff --git a/src/components/controllers/repl-page.jsx b/src/components/controllers/repl-page.jsx
index 11854998e..6fc75d0c1 100644
--- a/src/components/controllers/repl-page.jsx
+++ b/src/components/controllers/repl-page.jsx
@@ -1,4 +1,4 @@
-import { useRoute } from 'preact-iso';
+import { useLocation, useRoute } from 'preact-iso';
import { Repl } from './repl';
import { useExample } from './repl/examples';
import { useContent, useResource } from '../../lib/use-resource';
@@ -15,7 +15,7 @@ export default function ReplPage() {
useTitle(meta.title);
useDescription(meta.description);
- const [code, slug] = initialCode(query);
+ const [code] = initialCode(query);
return (
@@ -28,7 +28,7 @@ export default function ReplPage() {
display: none !important;
}
`}
-
+
);
}
@@ -39,6 +39,7 @@ export default function ReplPage() {
* ?code -> ?example -> localStorage -> simple counter example
*/
function initialCode(query) {
+ const { route } = useLocation();
let code, slug;
if (query.code) {
try {
@@ -48,13 +49,9 @@ function initialCode(query) {
code = useExample([query.example]);
if (code) {
slug = query.example;
- history.replaceState(
- null,
- null,
- `/repl?example=${encodeURIComponent(slug)}`
- );
+ route(`/repl?example=${encodeURIComponent(slug)}`, true);
}
- else history.replaceState(null, null, '/repl');
+ else route('/repl', true);
}
if (!code) {
@@ -63,11 +60,7 @@ function initialCode(query) {
} else {
slug = 'counter';
if (typeof window !== 'undefined') {
- history.replaceState(
- null,
- null,
- `/repl?example=${encodeURIComponent(slug)}`
- );
+ route(`/repl?example=${encodeURIComponent(slug)}`, true);
}
code = useExample([slug]);
}
diff --git a/src/components/controllers/repl/index.jsx b/src/components/controllers/repl/index.jsx
index 95c812798..a5e6d8a89 100644
--- a/src/components/controllers/repl/index.jsx
+++ b/src/components/controllers/repl/index.jsx
@@ -1,7 +1,8 @@
import { useState } from 'preact/hooks';
+import { useLocation, useRoute } from 'preact-iso';
import { Splitter } from '../../splitter';
-import { EXAMPLES, getExample, loadExample } from './examples';
import { ErrorOverlay } from './error-overlay';
+import { EXAMPLES, getExample, loadExample } from './examples';
import { useStoredValue } from '../../../lib/localstorage';
import { useResource } from '../../../lib/use-resource';
import { parseStackTrace } from './errors';
@@ -13,32 +14,29 @@ import REPL_CSS from './examples.css?raw';
* @param {string} props.code
* @param {string} [props.slug]
*/
-export function Repl({ code, slug }) {
+export function Repl({ code }) {
+ const { route } = useLocation();
+ const { query } = useRoute();
const [editorCode, setEditorCode] = useStoredValue('preact-www-repl-code', code);
- const [exampleSlug, setExampleSlug] = useState(slug || '');
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
- // TODO: CodeMirror v5 cannot load in Node, and loading only the runner
- // causes some bad jumping/pop-in. For the moment, this is the best option
+ // TODO: Needs some work for prerendering to not cause pop-in
if (typeof window === 'undefined') return null;
+ /**
+ * @type {{ Runner: import('./runner').default, CodeEditor: import('../../code-editor').default }}
+ */
const { Runner, CodeEditor } = useResource(() => Promise.all([
import('../../code-editor'),
import('./runner')
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
- const applyExample = (e) => {
- const slug = e.target.value;
+ const applyExample = (slug) => {
loadExample(getExample(slug).url)
.then(code => {
setEditorCode(code);
- setExampleSlug(slug);
- history.replaceState(
- null,
- null,
- `/repl?example=${encodeURIComponent(slug)}`
- );
+ route(`/repl?example=${encodeURIComponent(slug)}`, true);
});
};
@@ -47,25 +45,16 @@ export function Repl({ code, slug }) {
// Clears the example & code query params when a user
// begins to modify the code
- if (!exampleSlug || !location.search) return;
- const example = getExample(exampleSlug);
- if (example) {
- loadExample(example.url).then(exampleCode => {
- if (exampleCode !== value) {
- setExampleSlug('');
- history.replaceState(null, null, '/repl');
- }
- });
+ if (query.example || query.code) {
+ route('/repl', true);
}
};
const share = () => {
- if (!exampleSlug) {
- history.replaceState(
- null,
- null,
- `/repl?code=${encodeURIComponent(btoa(editorCode))}`
- );
+ // No reason to share semi-sketchy btoa'd code if there's
+ // a perfectly good example we can use instead
+ if (!query.example) {
+ route(`/repl?code=${encodeURIComponent(btoa(editorCode))}`, true);
}
try {
@@ -94,13 +83,13 @@ export function Repl({ code, slug }) {