Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update browser URL hash when user presses play button #129

Merged
merged 3 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"prettier-plugin-organize-imports": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"vscode-languageserver-protocol": "^3.17.5",
Expand Down Expand Up @@ -142,6 +143,5 @@
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
},
"packageManager": "[email protected]"
}
}
1 change: 1 addition & 0 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const buildmap = {
"src/pyodide-worker.ts",
"src/load-shinylive-sw.ts",
"src/run-python-blocks.ts",
"src/lzstring-worker.ts",
],
outdir: `${BUILD_DIR}/shinylive`,
// See note in esbuild.build() call above about why these are external.
Expand Down
9 changes: 8 additions & 1 deletion site_template/editor/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -18,6 +18,13 @@
allowCodeUrl: true,
allowGistUrl: true,
allowExampleUrl: true,
// This option causes shinylive to update the URL hash when the user
// clicks on the re-run button in the editor. It is false by default.
// It should be set to true only when the editor and viewer are used
// in a full-window configuration. If you are using the editor and
// viewer embedded in a larger page, it does not make sense to set
// this to true.
updateUrlHashOnRerun: true,
};

const appRoot = document.getElementById("root");
Expand Down
24 changes: 18 additions & 6 deletions site_template/examples/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -15,11 +15,23 @@
// reasons, if you enable any of these, then this site should be hosted on
// a separate domain or subdomain from other content. Otherwise the
// running of arbitrary code could be used, for example, to steal cookies.
runApp(appRoot, "examples-editor-terminal-viewer", {
allowCodeUrl: true,
allowGistUrl: true,
allowExampleUrl: true,
}, "{{APP_ENGINE}}");
runApp(
appRoot,
"examples-editor-terminal-viewer",
{
allowCodeUrl: true,
allowGistUrl: true,
allowExampleUrl: true,
// This option causes shinylive to update the URL hash when the user
// clicks on the re-run button in the editor. It is false by default.
// It should be set to true only when the editor and viewer are used
// in a full-window configuration. If you are using the editor and
// viewer embedded in a larger page, it does not make sense to set
// this to true.
updateUrlHashOnRerun: true,
},
"{{APP_ENGINE}}",
);
</script>
<link rel="stylesheet" href="../shinylive/style-resets.css" />
<link rel="stylesheet" href="../shinylive/shinylive.css" />
Expand Down
9 changes: 9 additions & 0 deletions src/Components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ type AppOptions = {

// In Viewer-only mode, should the header bar be shown?
showHeaderBar?: boolean;

// When the app is re-run from the Editor, should the URL hash be updated with
// the encoded version of the app?
updateUrlHashOnRerun?: boolean;
};

export type ProxyHandle = PyodideProxyHandle | WebRProxyHandle;
Expand Down Expand Up @@ -353,6 +357,7 @@ export function App({
file.name === "app.R" ||
file.name === "server.R",
)}
updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun}
appEngine={appEngine}
/>
</React.Suspense>
Expand Down Expand Up @@ -400,6 +405,7 @@ export function App({
file.name === "app.R" ||
file.name === "server.R",
)}
updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun}
appEngine={appEngine}
/>
</React.Suspense>
Expand Down Expand Up @@ -433,6 +439,7 @@ export function App({
terminalMethods={terminalMethods}
utilityMethods={utilityMethods}
runOnLoad={false}
updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun}
appEngine={appEngine}
/>
</React.Suspense>
Expand All @@ -458,6 +465,7 @@ export function App({
lineNumbers={false}
showHeaderBar={false}
floatingButtons={true}
updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun}
appEngine={appEngine}
/>
</React.Suspense>
Expand Down Expand Up @@ -495,6 +503,7 @@ export function App({
terminalMethods={terminalMethods}
utilityMethods={utilityMethods}
viewerMethods={viewerMethods}
updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun}
appEngine={appEngine}
/>
</React.Suspense>
Expand Down
115 changes: 109 additions & 6 deletions src/Components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "balloon-css";
import type { Zippable } from "fflate";
import { zipSync } from "fflate";
import * as React from "react";
import toast, { Toaster } from "react-hot-toast";
import type * as LSP from "vscode-languageserver-protocol";
import * as fileio from "../fileio";
import { createUri } from "../language-server/client";
Expand All @@ -36,7 +37,15 @@ import { languageServerExtensions } from "./codeMirror/language-server/lsp-exten
import { useTabbedCodeMirror } from "./codeMirror/useTabbedCodeMirror";
import * as cmUtils from "./codeMirror/utils";
import type { FileContent } from "./filecontent";
import { editorUrlPrefix, fileContentsToUrlString } from "./share";
import {
editorUrlPrefix,
fileContentsToUrlString,
fileContentsToUrlStringInWebWorker,
} from "./share";

// If the file contents are larger than this value, then don't automatically
// update the URL hash when re-running the app.
const UPDATE_URL_SIZE_THRESHOLD = 250000;

export type EditorFile =
| {
Expand Down Expand Up @@ -77,6 +86,7 @@ export default function Editor({
lineNumbers = true,
showHeaderBar = true,
floatingButtons = false,
updateUrlHashOnRerun = false,
appEngine,
}: {
currentFilesFromApp: FileContent[];
Expand All @@ -93,6 +103,7 @@ export default function Editor({
lineNumbers?: boolean;
showHeaderBar?: boolean;
floatingButtons?: boolean;
updateUrlHashOnRerun?: boolean;
appEngine: AppEngine;
}) {
// In the future, instead of directly instantiating the PyrightClient, it
Expand All @@ -114,6 +125,33 @@ export default function Editor({
// the Viewer component.
const lspPathPrefix = `editor${editorInstanceId}/`;

// This tracks whether the files have changed since the the last time the user
// has run the app/code. This is used to determine whether to update the URL.
// It is different from `setFilesHaveChanged` which is passed in, because that
// tracks whether the files have changed since they were passed into the
// Editor component.
//
// If the Editor starts with a file, then you change it and re-run, then both
// the external `filesHaveChanged` and `filesHaveChangedSinceLastRun` will be
// true. But if you re-run it again without making changes, then
// `filesHaveChanged` will still be true, and `filesHaveChangedSinceLastRun`
// will be false.
const [filesHaveChangedSinceLastRun, setFilesHaveChangedSinceLastRun] =
React.useState<boolean>(false);

// This is a shortcut to indicate that the files have changed. See the comment
// for `setFilesHaveChangedSinceLastRun` to understand why this is needed.
const setFilesHaveChangedCombined = React.useCallback(
(value: boolean) => {
setFilesHaveChanged(value);
setFilesHaveChangedSinceLastRun(value);
},
[setFilesHaveChanged, setFilesHaveChangedSinceLastRun],
);

const [hasShownUrlTooLargeMessage, setHasShownUrlTooLargeMessage] =
React.useState<boolean>(false);

// Given a FileContent object, figure out which editor extensions to use.
// Use a memoized function to maintain referentially stablity.
const inferEditorExtensions = React.useCallback(
Expand All @@ -130,7 +168,7 @@ export default function Editor({
getLanguageExtension(language),
EditorView.updateListener.of((u: ViewUpdate) => {
if (u.docChanged) {
setFilesHaveChanged(true);
setFilesHaveChangedCombined(true);
}
}),
languageServerExtensions(lspClient, lspPathPrefix + file.name),
Expand All @@ -139,7 +177,7 @@ export default function Editor({
),
];
},
[lineNumbers, setFilesHaveChanged, lspClient, lspPathPrefix],
[lineNumbers, setFilesHaveChangedCombined, lspClient, lspPathPrefix],
);

const [cmView, setCmView] = React.useState<EditorView>();
Expand All @@ -148,7 +186,7 @@ export default function Editor({
currentFilesFromApp,
cmView,
inferEditorExtensions,
setFilesHaveChanged,
setFilesHaveChanged: setFilesHaveChangedCombined,
lspClient,
lspPathPrefix,
});
Expand Down Expand Up @@ -182,12 +220,43 @@ export default function Editor({
if (!viewerMethods || !viewerMethods.ready) return;

syncActiveFileState();
const fileContents = editorFilesToFileContents(files);

if (updateUrlHashOnRerun && filesHaveChangedSinceLastRun) {
const filesSize = fileContentsSize(fileContents);

if (
!hasShownUrlTooLargeMessage &&
filesSize > UPDATE_URL_SIZE_THRESHOLD
) {
toast(
"Auto-updating the app link is disabled because the app is very large. " +
"If you want the sharing URL, click the Share button.",
);
setHasShownUrlTooLargeMessage(true);
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateBrowserUrlHash(fileContents);
}
}

setFilesHaveChangedCombined(false);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
await viewerMethods.stopApp();
await viewerMethods.runApp(editorFilesToFileContents(files));
await viewerMethods.runApp(fileContents);
})();
}, [viewerMethods, syncActiveFileState, files]);
}, [
viewerMethods,
syncActiveFileState,
updateUrlHashOnRerun,
filesHaveChangedSinceLastRun,
setFilesHaveChangedCombined,
hasShownUrlTooLargeMessage,
setHasShownUrlTooLargeMessage,
files,
]);

// Run the entire current file in the terminal.
const runAllCode = React.useCallback(() => {
Expand Down Expand Up @@ -558,6 +627,13 @@ export default function Editor({
</div>
) : null}
<div className="editor-container" ref={cmDivRef}></div>
<Toaster
toastOptions={{
duration: 5000,
position: "top-center",
style: { fontFamily: "var(--font-face)" },
}}
/>
{floatingButtons ? (
<div className="floating-buttons">{runButton}</div>
) : null}
Expand Down Expand Up @@ -640,6 +716,22 @@ function editorFilesToFflateZippable(files: EditorFile[]): Zippable {
return res;
}

// Get the size in bytes of the contents of a FileContent array. Note that this
// isn't exactly the size in bytes -- for text files, it counts the number of
// characters, but some could be multi-byte (and the size could vary depending
// on the encoding). But it's close enough for our purposes.
function fileContentsSize(files: FileContent[]): number {
let size = 0;
for (const file of files) {
if (file.type === "binary") {
size += file.content.length;
} else {
size += file.content.length;
}
}
return size;
}

// =============================================================================
// Misc utility functions
// =============================================================================
Expand Down Expand Up @@ -714,3 +806,14 @@ function keyBindings({
},
];
}
/**
* Update the browser URL hash with the current contents of the Editor.
*/
async function updateBrowserUrlHash(
fileContents: FileContent[],
): Promise<void> {
const encodedFileContents =
await fileContentsToUrlStringInWebWorker(fileContents);
const hash = "#code=" + encodedFileContents;
history.replaceState(null, "", hash);
}
Loading