jli  Linuxx86_641.10.3v1.10.30b4590a5507d3f3046e5bafc007cacbbfc9b310b\ePlutoUÁWk%&)/9 ,/opt/julia/packages/Pluto/GVuR6/src/Pluto.jlәA!1աpJ!2AFuzzyCompletions#8YsJ DRelocatableFolders5/opt/julia/packages/Pluto/GVuR6/frontend/Desktop.d.tsAA5/opt/julia/packages/Pluto/GVuR6/frontend/alegreya.cssAA3/opt/julia/packages/Pluto/GVuR6/frontend/binder.cssA7/opt/julia/packages/Pluto/GVuR6/frontend/dark_color.cssEA3/opt/julia/packages/Pluto/GVuR6/frontend/editor.cssEA4/opt/julia/packages/Pluto/GVuR6/frontend/editor.htmlEA2/opt/julia/packages/Pluto/GVuR6/frontend/editor.jsEA2/opt/julia/packages/Pluto/GVuR6/frontend/error.cssEA6/opt/julia/packages/Pluto/GVuR6/frontend/error.jl.htmlEA:/opt/julia/packages/Pluto/GVuR6/frontend/featured-card.cssEA</opt/julia/packages/Pluto/GVuR6/frontend/featured_sources.jsEA4/opt/julia/packages/Pluto/GVuR6/frontend/hide-ui.cssEA8/opt/julia/packages/Pluto/GVuR6/frontend/highlightjs.cssEA2/opt/julia/packages/Pluto/GVuR6/frontend/index.cssfA3/opt/julia/packages/Pluto/GVuR6/frontend/index.htmlfA1/opt/julia/packages/Pluto/GVuR6/frontend/index.jsfA6/opt/julia/packages/Pluto/GVuR6/frontend/juliamono.cssfA8/opt/julia/packages/Pluto/GVuR6/frontend/light_color.cssfA5/opt/julia/packages/Pluto/GVuR6/frontend/package.jsonfA5/opt/julia/packages/Pluto/GVuR6/frontend/treeview.cssfA5/opt/julia/packages/Pluto/GVuR6/frontend/vollkorn.cssfA=/opt/julia/packages/Pluto/GVuR6/frontend/warn_old_browsers.jsfA4/opt/julia/packages/Pluto/GVuR6/frontend/welcome.cssfAA/opt/julia/packages/Pluto/GVuR6/frontend/common/AudioRecording.jsA9/opt/julia/packages/Pluto/GVuR6/frontend/common/Binder.jsA7/opt/julia/packages/Pluto/GVuR6/frontend/common/Bond.jsA=/opt/julia/packages/Pluto/GVuR6/frontend/common/ClassTable.jsA>/opt/julia/packages/Pluto/GVuR6/frontend/common/Environment.jsA;/opt/julia/packages/Pluto/GVuR6/frontend/common/Feedback.jsAF/opt/julia/packages/Pluto/GVuR6/frontend/common/InstallTimeEstimate.jsAD/opt/julia/packages/Pluto/GVuR6/frontend/common/KeyboardShortcuts.jsA:/opt/julia/packages/Pluto/GVuR6/frontend/common/Logging.jsA:/opt/julia/packages/Pluto/GVuR6/frontend/common/MsgPack.jsAC/opt/julia/packages/Pluto/GVuR6/frontend/common/NewUpdateMessage.jsAN/opt/julia/packages/Pluto/GVuR6/frontend/common/NodejsCompatibilityPolyfill.jsAJ/opt/julia/packages/Pluto/GVuR6/frontend/common/NotebookLocationFromURL.jsAB/opt/julia/packages/Pluto/GVuR6/frontend/common/PlutoConnection.jsAJ/opt/julia/packages/Pluto/GVuR6/frontend/common/PlutoConnectionSendFn.d.tsA?/opt/julia/packages/Pluto/GVuR6/frontend/common/PlutoContext.jsA</opt/julia/packages/Pluto/GVuR6/frontend/common/PlutoHash.jsA;/opt/julia/packages/Pluto/GVuR6/frontend/common/Polyfill.jsA@/opt/julia/packages/Pluto/GVuR6/frontend/common/ProcessStatus.jsA;/opt/julia/packages/Pluto/GVuR6/frontend/common/RunLocal.jsA@/opt/julia/packages/Pluto/GVuR6/frontend/common/Serialization.jsAG/opt/julia/packages/Pluto/GVuR6/frontend/common/SetupCellEnvironment.jsA?/opt/julia/packages/Pluto/GVuR6/frontend/common/SetupMathJax.jsAE/opt/julia/packages/Pluto/GVuR6/frontend/common/SliderServerClient.jsA;/opt/julia/packages/Pluto/GVuR6/frontend/common/URLTools.jsA?/opt/julia/packages/Pluto/GVuR6/frontend/common/UnicodeTools.jsA=/opt/julia/packages/Pluto/GVuR6/frontend/common/clock sync.jsAC/opt/julia/packages/Pluto/GVuR6/frontend/common/open_pluto_popup.jsAF/opt/julia/packages/Pluto/GVuR6/frontend/common/parse_launch_params.jsA</opt/julia/packages/Pluto/GVuR6/frontend/common/useDialog.jsAC/opt/julia/packages/Pluto/GVuR6/frontend/common/useEventListener.jsAB/opt/julia/packages/Pluto/GVuR6/frontend/components/AudioPlayer.jsAG/opt/julia/packages/Pluto/GVuR6/frontend/components/BottomRightPanel.jsA;/opt/julia/packages/Pluto/GVuR6/frontend/components/Cell.jsA@/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput.jsSAA/opt/julia/packages/Pluto/GVuR6/frontend/components/CellOutput.jsSAJ/opt/julia/packages/Pluto/GVuR6/frontend/components/DiscreteProgressBar.jsSA@/opt/julia/packages/Pluto/GVuR6/frontend/components/DropRuler.jsSAF/opt/julia/packages/Pluto/GVuR6/frontend/components/EditOrRunButton.jsSA=/opt/julia/packages/Pluto/GVuR6/frontend/components/Editor.jsSAC/opt/julia/packages/Pluto/GVuR6/frontend/components/ErrorMessage.jsSAC/opt/julia/packages/Pluto/GVuR6/frontend/components/ExportBanner.jsSAD/opt/julia/packages/Pluto/GVuR6/frontend/components/FetchProgress.jsSAA/opt/julia/packages/Pluto/GVuR6/frontend/components/FilePicker.jsSAG/opt/julia/packages/Pluto/GVuR6/frontend/components/FrontmatterInput.jsSAB/opt/julia/packages/Pluto/GVuR6/frontend/components/LiveDocsTab.jsSA;/opt/julia/packages/Pluto/GVuR6/frontend/components/Logs.jsSAD/opt/julia/packages/Pluto/GVuR6/frontend/components/NonCellOutput.jsSA?/opt/julia/packages/Pluto/GVuR6/frontend/components/Notebook.jsSAE/opt/julia/packages/Pluto/GVuR6/frontend/components/NotifyWhenDone.jsSAC/opt/julia/packages/Pluto/GVuR6/frontend/components/PasteHandler.jsSAD/opt/julia/packages/Pluto/GVuR6/frontend/components/PkgStatusMark.jsSAF/opt/julia/packages/Pluto/GVuR6/frontend/components/PkgTerminalView.jsSA</opt/julia/packages/Pluto/GVuR6/frontend/components/Popup.jsSA?/opt/julia/packages/Pluto/GVuR6/frontend/components/Preamble.jsSAA/opt/julia/packages/Pluto/GVuR6/frontend/components/ProcessTab.jsSAB/opt/julia/packages/Pluto/GVuR6/frontend/components/ProgressBar.jsSAB/opt/julia/packages/Pluto/GVuR6/frontend/components/RecordingUI.jsSA>/opt/julia/packages/Pluto/GVuR6/frontend/components/RunArea.jsEAD/opt/julia/packages/Pluto/GVuR6/frontend/components/SafePreviewUI.jsEA?/opt/julia/packages/Pluto/GVuR6/frontend/components/Scroller.jsEAD/opt/julia/packages/Pluto/GVuR6/frontend/components/SelectionArea.jsEAD/opt/julia/packages/Pluto/GVuR6/frontend/components/SlideControls.jsEA?/opt/julia/packages/Pluto/GVuR6/frontend/components/TreeView.jsEAA/opt/julia/packages/Pluto/GVuR6/frontend/components/UndoDelete.jsEAS/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/LiveDocsFromCursor.jsAL/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/ReactWidget.jsAV/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/awesome_line_wrapping.jsAU/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/block_matcher_plugin.jsAU/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/cell_movement_plugin.jsAV/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/comment_mixed_parsers.jsAT/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/debug_syntax_plugin.jsAX/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/go_to_definition_plugin.jsAO/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/highlight_line.jsAO/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/lezer_template.jsAM/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/mixedParsers.jsSAN/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/mod_d_command.jsSAR/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/pkg_bubble_plugin.jsSAS/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/pluto_autocomplete.jsSAS/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/pluto_paste_plugin.jsSAV/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/scopestate_statefield.jsSAP/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tab_help_plugin.jsSAN/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tests/.gitignoreSAM/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tests/README.mdSAS/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tests/import_map.jsonSAV/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tests/scopestate.test.jsSAY/opt/julia/packages/Pluto/GVuR6/frontend/components/CellInput/tests/scopestate_helpers.jsSAQ/opt/julia/packages/Pluto/GVuR6/frontend/components/Editor/LaunchBackendButton.jsSAg/opt/julia/packages/Pluto/GVuR6/frontend/components/HackySideStuff/HijackExternalLinksToOpenInNewTab.jsSAG/opt/julia/packages/Pluto/GVuR6/frontend/components/welcome/Featured.jsEAK/opt/julia/packages/Pluto/GVuR6/frontend/components/welcome/FeaturedCard.jsEAC/opt/julia/packages/Pluto/GVuR6/frontend/components/welcome/Open.jsEAE/opt/julia/packages/Pluto/GVuR6/frontend/components/welcome/Recent.jsEAF/opt/julia/packages/Pluto/GVuR6/frontend/components/welcome/Welcome.jsEA>/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon-16x16.pngEA>/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon-32x32.pngEA>/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon-96x96.pngEA8/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon.icoEA8/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon.pngEA8/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon.svgEAD/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon_unsaturated.svgEAG/opt/julia/packages/Pluto/GVuR6/frontend/img/favicon_unsaturated_bg.svgEA5/opt/julia/packages/Pluto/GVuR6/frontend/img/logo.svgEAC/opt/julia/packages/Pluto/GVuR6/frontend/img/logo_white_contour.svgEA:/opt/julia/packages/Pluto/GVuR6/frontend/imports/AnsiUp.jsEAJ/opt/julia/packages/Pluto/GVuR6/frontend/imports/CodemirrorPlutoSetup.d.tsEAH/opt/julia/packages/Pluto/GVuR6/frontend/imports/CodemirrorPlutoSetup.jsfA?/opt/julia/packages/Pluto/GVuR6/frontend/imports/DOMPurify.d.tsfA=/opt/julia/packages/Pluto/GVuR6/frontend/imports/DOMPurify.jsfA</opt/julia/packages/Pluto/GVuR6/frontend/imports/Preact.d.tsfA:/opt/julia/packages/Pluto/GVuR6/frontend/imports/Preact.jsfAG/opt/julia/packages/Pluto/GVuR6/frontend/imports/PreactCustomElement.jsfAA/opt/julia/packages/Pluto/GVuR6/frontend/imports/highlightjs.d.tsfA?/opt/julia/packages/Pluto/GVuR6/frontend/imports/highlightjs.jsfA;/opt/julia/packages/Pluto/GVuR6/frontend/imports/immer.d.tsfA9/opt/julia/packages/Pluto/GVuR6/frontend/imports/immer.jsfA</opt/julia/packages/Pluto/GVuR6/frontend/imports/lodash.d.tsfA:/opt/julia/packages/Pluto/GVuR6/frontend/imports/lodash.jsfAB/opt/julia/packages/Pluto/GVuR6/frontend/imports/msgpack-lite.d.tsfA@/opt/julia/packages/Pluto/GVuR6/frontend/imports/msgpack-lite.jsfAW/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Black.woff.a3efb88f6f.efe3f25b.woffAY/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Black.woff2.a3efb88f6f.1f333c9f.woff2A]/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-BlackItalic.woff.a3efb88f6f.f6e2e726.woffA_/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-BlackItalic.woff2.a3efb88f6f.89710fff.woff2AV/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Bold.woff.a3efb88f6f.d92ed350.woffAX/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Bold.woff2.a3efb88f6f.e8bd971d.woff2A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-BoldItalic.woff.a3efb88f6f.39f68e5f.woffA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-BoldItalic.woff2.a3efb88f6f.cd6e580e.woff2A[/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraBold.woff.a3efb88f6f.cd51bf91.woffA]/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraBold.woff2.a3efb88f6f.ffc04ff0.woff2Aa/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraBoldItalic.woff.a3efb88f6f.122106d3.woffAc/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraBoldItalic.woff2.a3efb88f6f.de4f3979.woff2A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraLight.woff.a3efb88f6f.018d6d24.woffA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraLight.woff2.a3efb88f6f.735b820a.woff2Ab/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraLightItalic.woff.a3efb88f6f.c051c5b4.woffAd/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ExtraLightItalic.woff2.a3efb88f6f.75944d4e.woff2AX/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Italic.woff.a3efb88f6f.d9effa31.woffAZ/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Italic.woff2.a3efb88f6f.85eee0c1.woff2AW/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Light.woff.a3efb88f6f.0cf765a1.woffx AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Light.woff2.a3efb88f6f.f86a8a97.woff2x A]/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-LightItalic.woff.a3efb88f6f.6c91af65.woffx A_/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-LightItalic.woff2.a3efb88f6f.cb22faff.woff2x AX/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Medium.woff.a3efb88f6f.cb391d89.woffx AZ/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Medium.woff2.a3efb88f6f.4f0ea984.woff2x A^/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-MediumItalic.woff.a3efb88f6f.1f636c25.woffx A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-MediumItalic.woff2.a3efb88f6f.2b734b7a.woff2x AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Regular.woff.a3efb88f6f.d9325c13.woffx A[/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Regular.woff2.a3efb88f6f.c52a047f.woff2x AZ/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-SemiBold.woff.a3efb88f6f.c6670c13.woffx A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-SemiBold.woff2.a3efb88f6f.08366574.woff2x A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-SemiBoldItalic.woff.a3efb88f6f.13394743.woffx Ab/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-SemiBoldItalic.woff2.a3efb88f6f.5c7db07d.woff2x AV/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Thin.woff.a3efb88f6f.832f7e9d.woffx AX/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-Thin.woff2.a3efb88f6f.099a7dc1.woff2x A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ThinItalic.woff.a3efb88f6f.9a9e646f.woffx A^/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-ThinItalic.woff2.a3efb88f6f.f5b32cfe.woff2x A^/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-italic.var.woff2.a3efb88f6f.e62a3bb2.woff2x A]/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter-roman.var.woff2.a3efb88f6f.8120837c.woff2x AW/opt/julia/packages/Pluto/GVuR6/frontend-dist/Inter.var.woff2.a3efb88f6f.ca82f9aa.woff2x AK/opt/julia/packages/Pluto/GVuR6/frontend-dist/JuliaMono-Bold.24b57d33.woff2LAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/JuliaMono-BoldLatin.7ec5c608.woff2LAN/opt/julia/packages/Pluto/GVuR6/frontend-dist/JuliaMono-Regular.e0106c6f.woff2AT/opt/julia/packages/Pluto/GVuR6/frontend-dist/JuliaMono-RegularItalic.3159f647.woff2AS/opt/julia/packages/Pluto/GVuR6/frontend-dist/JuliaMono-RegularLatin.26c56b70.woff2AK/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-Black.0ebdfbf5.woff2AQ/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-BlackItalic.4095acfe.woff2AJ/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-Bold.6e1feb70.woff2AP/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-BoldItalic.a48ab300.woff2AN/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-SemiBold.8072eb6c.woff2AT/opt/julia/packages/Pluto/GVuR6/frontend-dist/Vollkorn-SemiBoldItalic.5c3cd265.woff2AF/opt/julia/packages/Pluto/GVuR6/frontend-dist/add-outline.e3c93c35.svgAM/opt/julia/packages/Pluto/GVuR6/frontend-dist/arrow-back-outline.9ae1bed8.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/arrow-forward-outline.f5f68f5c.svgAT/opt/julia/packages/Pluto/GVuR6/frontend-dist/arrow-redo-circle-outline.dfa899f4.svgAM/opt/julia/packages/Pluto/GVuR6/frontend-dist/arrow-undo-outline.d8c99108.svgAR/opt/julia/packages/Pluto/GVuR6/frontend-dist/arrow-up-circle-outline.3f146ffe.svgAF/opt/julia/packages/Pluto/GVuR6/frontend-dist/ban-outline.c97da9b4.svgAM/opt/julia/packages/Pluto/GVuR6/frontend-dist/caret-down-outline.4253a2fe.svgAW/opt/julia/packages/Pluto/GVuR6/frontend-dist/caret-forward-circle-outline.d0bf2b34.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/caret-forward-outline.348a84b6.svgAS/opt/julia/packages/Pluto/GVuR6/frontend-dist/chatbox-ellipses-outline.4334a3a4.svgAL/opt/julia/packages/Pluto/GVuR6/frontend-dist/checkmark-outline.4fb8c646.svgAL/opt/julia/packages/Pluto/GVuR6/frontend-dist/checkmark-outline.538ded54.svgAO/opt/julia/packages/Pluto/GVuR6/frontend-dist/chevron-down-outline.1efaf1cc.svgAY/opt/julia/packages/Pluto/GVuR6/frontend-dist/chevron-forward-circle-outline.babf2a99.svgAO/opt/julia/packages/Pluto/GVuR6/frontend-dist/close-circle-outline.4baeedb9.svgAG/opt/julia/packages/Pluto/GVuR6/frontend-dist/close-circle.d0f6eac6.svgAH/opt/julia/packages/Pluto/GVuR6/frontend-dist/close-outline.9c22a232.svgAQ/opt/julia/packages/Pluto/GVuR6/frontend-dist/cloud-download-outline.8c1ff9bb.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/cloud-offline-outline.ac2eade9.svgAG/opt/julia/packages/Pluto/GVuR6/frontend-dist/copy-outline.0f561529.svgAG/opt/julia/packages/Pluto/GVuR6/frontend-dist/copy-outline.6d5a7927.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/document-lock-outline.69d176a4.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/document-text-outline.8d1e2333.svgAK/opt/julia/packages/Pluto/GVuR6/frontend-dist/download-outline.523a74d8.svgAH/opt/julia/packages/Pluto/GVuR6/frontend-dist/easel-outline.9b064e1f.svgAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.52bd66ba.cssA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.5fc14923.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.6386bd9d.jsAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.658c03ff.cssAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.6c40dd9a.cssA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.8a3292da.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.90ede145.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.912b6bfb.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.9f9dc874.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.b8733d72.jsAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.d0a5b1f0.cssAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.db21a487.cssAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.e82e08bd.cssA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.fe612591.js8A9/opt/julia/packages/Pluto/GVuR6/frontend-dist/editor.html8A]/opt/julia/packages/Pluto/GVuR6/frontend-dist/ellipsis-horizontal-circle-outline.6279ed30.svg8AV/opt/julia/packages/Pluto/GVuR6/frontend-dist/ellipsis-horizontal-outline.abb6e818.svg8AN/opt/julia/packages/Pluto/GVuR6/frontend-dist/ellipsis-horizontal.c9a6cc29.svg8AL/opt/julia/packages/Pluto/GVuR6/frontend-dist/ellipsis-vertical.a30e7430.svg8A;/opt/julia/packages/Pluto/GVuR6/frontend-dist/error.jl.html8AJ/opt/julia/packages/Pluto/GVuR6/frontend-dist/eye-off-outline.a37e03db.svg8AF/opt/julia/packages/Pluto/GVuR6/frontend-dist/eye-outline.6e6e0f7c.svg8AH/opt/julia/packages/Pluto/GVuR6/frontend-dist/favicon-16x16.347d2855.png8AH/opt/julia/packages/Pluto/GVuR6/frontend-dist/favicon-32x32.8789add4.png8AH/opt/julia/packages/Pluto/GVuR6/frontend-dist/favicon-96x96.48689391.png8AN/opt/julia/packages/Pluto/GVuR6/frontend-dist/favicon_unsaturated.d1387b25.svg8AF/opt/julia/packages/Pluto/GVuR6/frontend-dist/firebase-app.15ba8989.js8AL/opt/julia/packages/Pluto/GVuR6/frontend-dist/firebase-firestore.f72d0b8a.js8AN/opt/julia/packages/Pluto/GVuR6/frontend-dist/help-circle-outline.a023036a.svgA?/opt/julia/packages/Pluto/GVuR6/frontend-dist/index.6587449d.jsA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/index.7e5e6cea.cssA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/index.dbc7264b.cssA8/opt/julia/packages/Pluto/GVuR6/frontend-dist/index.htmlAU/opt/julia/packages/Pluto/GVuR6/frontend-dist/information-circle-outline.008b2bb9.svgAO/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-all-400-italic.c29c8c6c.woffAO/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-all-400-normal.a1a68bdf.woffAR/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-latin-400-italic.6edbc86c.woff2AR/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-latin-400-normal.77db3602.woff2AV/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-latin-ext-400-italic.336aaf51.woff2AV/opt/julia/packages/Pluto/GVuR6/frontend-dist/lato-latin-ext-400-normal.e1ce8ad3.woff2A?/opt/julia/packages/Pluto/GVuR6/frontend-dist/logo.004c1d7c.svgAJ/opt/julia/packages/Pluto/GVuR6/frontend-dist/mic-off-outline.251c22b7.svgAF/opt/julia/packages/Pluto/GVuR6/frontend-dist/mic-outline.e10eafe2.svgAL/opt/julia/packages/Pluto/GVuR6/frontend-dist/newspaper-outline.e481c39c.svgAP/opt/julia/packages/Pluto/GVuR6/frontend-dist/notifications-outline.aa91b431.svgAB/opt/julia/packages/Pluto/GVuR6/frontend-dist/parcel-manifest.jsonA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/pulse.27a877a7.svgAR/opt/julia/packages/Pluto/GVuR6/frontend-dist/radio-button-on-outline.778acac1.svgAV/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-all-400-italic.31a14f53.woffAV/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-all-400-normal.364ec368.woffAV/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-all-500-normal.cc559149.woffAV/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-all-700-normal.393c796d.woffA\/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-400-italic.e399ed93.woff2A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-400-normal.638e826e.woff2A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-500-normal.8ed3add8.woff2A\/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-700-normal.5e6410cf.woff2A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-ext-400-italic.db052448.woff2A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-ext-400-normal.dcb520ee.woff2A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-ext-500-normal.d362a132.woff2A`/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-cyrillic-ext-700-normal.5c7aabac.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-greek-400-italic.6f7e0b2f.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-greek-400-normal.dd843e41.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-greek-500-normal.dd57b097.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-greek-700-normal.b101f80c.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-400-italic.ef82d48f.woff2AY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-400-normal.cf1eee5f.woff2JXAY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-500-normal.98ad1d4e.woff2JXAY/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-700-normal.447ac127.woff2JXA]/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-ext-400-italic.aaa9a959.woff2JXA]/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-ext-400-normal.d9409874.woff2JXA]/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-ext-500-normal.415f7d14.woff2JXA]/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-latin-ext-700-normal.c497b002.woff2JXA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-vietnamese-400-italic.d8e0a32a.woff2JXA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-vietnamese-400-normal.306e7635.woff2JXA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-vietnamese-500-normal.1320bb60.woff2JXA^/opt/julia/packages/Pluto/GVuR6/frontend-dist/roboto-mono-vietnamese-700-normal.edb1435b.woff2JXAA/opt/julia/packages/Pluto/GVuR6/frontend-dist/search.1ca5b3b3.svgJXAH/opt/julia/packages/Pluto/GVuR6/frontend-dist/share-outline.50164ded.svgJXAN/opt/julia/packages/Pluto/GVuR6/frontend-dist/stop-circle-outline.6623356d.svgJXAN/opt/julia/packages/Pluto/GVuR6/frontend-dist/sync-circle-outline.65b15d76.svgJXAG/opt/julia/packages/Pluto/GVuR6/frontend-dist/sync-outline.9a1bd27b.svgJXAK/opt/julia/packages/Pluto/GVuR6/frontend-dist/terminal-outline.79a23031.svgJXAC/opt/julia/packages/Pluto/GVuR6/frontend-dist/terminal.6b804248.svgJXAG/opt/julia/packages/Pluto/GVuR6/frontend-dist/time-outline.7c1877f0.svgJXA@/opt/julia/packages/Pluto/GVuR6/frontend-dist/vmsg.56bb9389.wasmJXAJ/opt/julia/packages/Pluto/GVuR6/frontend-dist/warning-outline.d84ed9e8.svgJXA;/opt/julia/packages/Pluto/GVuR6/sample/Basic mathematics.jlJXA//opt/julia/packages/Pluto/GVuR6/sample/Basic.jlJXA9/opt/julia/packages/Pluto/GVuR6/sample/Getting started.jlJXA7/opt/julia/packages/Pluto/GVuR6/sample/Interactivity.jlJXA4/opt/julia/packages/Pluto/GVuR6/sample/JavaScript.jlJXA./opt/julia/packages/Pluto/GVuR6/sample/LICENSEJXA2/opt/julia/packages/Pluto/GVuR6/sample/Markdown.jlJXA2/opt/julia/packages/Pluto/GVuR6/sample/Plots.jl.jlJXA4/opt/julia/packages/Pluto/GVuR6/sample/PlutoUI.jl.jlJXA8/opt/julia/packages/Pluto/GVuR6/sample/Tower of Hanoi.jlJXA@/opt/julia/packages/Pluto/GVuR6/sample/notebook_with_metadata.jlJXAA/opt/julia/packages/Pluto/GVuR6/sample/old_notebook_with_using.jlJXA//opt/julia/packages/Pluto/GVuR6/sample/test1.jlJXA</opt/julia/packages/Pluto/GVuR6/sample/test_embed_display.jlJXA?/opt/julia/packages/Pluto/GVuR6/sample/test_go_to_definition.jlJXA6/opt/julia/packages/Pluto/GVuR6/sample/test_logging.jlәA9/opt/julia/packages/Pluto/GVuR6/sample/test_pkg_bubble.jlәA4/opt/julia/packages/Pluto/GVuR6/src/runner/Loader.jlәAC/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/Project.tomlәAI/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/PlutoRunner.jlәAH/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/precompile.jlәAi߯rRZDPkgScratchspaces.jlScratch,/opt/julia/packages/Pluto/GVuR6/Project.tomlAA(kikkerkratluskerPlutoDependencyExplorer#Climate Justice!ExpressionExplorer</opt/julia/packages/Pluto/GVuR6/src/notebook/path helpers.jlәA_䇽UlD*Base646/opt/julia/packages/Pluto/GVuR6/src/notebook/Export.jlәA!:͒["KHypertextLiteralԱ6;V.OG'\URIs4/opt/julia/packages/Pluto/GVuR6/src/Configuration.jlәAm.aJRConfigurations Configuration8/opt/julia/packages/Pluto/GVuR6/src/evaluation/Tokens.jlәA;/opt/julia/packages/Pluto/GVuR6/src/evaluation/Throttled.jlәAI/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/PlutoRunner.jlәAz`sZPn7Markdown PlutoRunner!@'Z萠WL ~InteractiveUtils PlutoRunner_䇽UlD*Base64 PlutoRunner!1աpJ!2AFuzzyCompletions PlutoRunnerrz9[viqUUIDs PlutoRunnerj2 EY8pDates PlutoRunnerhUXM=T{VLogging PlutoRunneruavV͠?REPL PlutoRunnerrz9[viqUUIDs PlutoRunnerUseEffectCleanupsH/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/precompile.jlәA PlutoRunner *gnV@jjPrecompileTools PlutoRunner9/opt/julia/packages/Pluto/GVuR6/src/packages/PkgCompat.jlәAuavV͠?REPL PkgCompati߯rRZDPkg PkgCompat"QΝtH'RegistryInstances PkgCompat7/opt/julia/packages/Pluto/GVuR6/src/webserver/Status.jl\A4/opt/julia/packages/Pluto/GVuR6/src/notebook/Cell.jlәArz9[viqUUIDs8/opt/julia/packages/Pluto/GVuR6/src/notebook/Notebook.jlәAv3TOI`&TOMLB/opt/julia/packages/Pluto/GVuR6/src/notebook/saving and loading.jlәA;/opt/julia/packages/Pluto/GVuR6/src/notebook/frontmatter.jlәA6/opt/julia/packages/Pluto/GVuR6/src/notebook/Events.jlәAUP5>HTTP8/opt/julia/packages/Pluto/GVuR6/src/webserver/Session.jl\A;/opt/julia/packages/Pluto/GVuR6/src/webserver/PutUpdates.jl\A5/opt/julia/packages/Pluto/GVuR6/src/analysis/Parse.jlәAz`sZPn7Markdown</opt/julia/packages/Pluto/GVuR6/src/analysis/is_just_text.jlәA?/opt/julia/packages/Pluto/GVuR6/src/analysis/DependencyCache.jlәA</opt/julia/packages/Pluto/GVuR6/src/analysis/MoreAnalysis.jlәAB/opt/julia/packages/Pluto/GVuR6/src/evaluation/WorkspaceManager.jlәArz9[viqUUIDsWorkspaceManager;N2MB16MaltWorkspaceManager?/opt/julia/packages/Pluto/GVuR6/src/evaluation/MacroAnalysis.jlәA:/opt/julia/packages/Pluto/GVuR6/src/packages/IOListener.jlәA=/opt/julia/packages/Pluto/GVuR6/src/packages/ANSIEmulation.jlәA ANSIEmulationz`sZPn7Markdown ANSIEmulation!@'Z萠WL ~InteractiveUtils ANSIEmulationC/opt/julia/packages/Pluto/GVuR6/src/packages/precompile_isolated.jlәA8/opt/julia/packages/Pluto/GVuR6/src/packages/Packages.jlәAhUXM=T{VLogging6l7vSzԗLoggingExtras8/opt/julia/packages/Pluto/GVuR6/src/packages/PkgUtils.jlәA,zXzsy`{FileWatchingPkgUtilsi߯rRZDPkgPkgUtilsz`sZPn7MarkdownPkgUtils5/opt/julia/packages/Pluto/GVuR6/src/evaluation/Run.jlәAuavV͠?REPL:/opt/julia/packages/Pluto/GVuR6/src/evaluation/RunBonds.jlәA9/opt/julia/packages/Pluto/GVuR6/src/webserver/data_url.jl\A DownloadCoolz`sZPn7Markdown DownloadCool!@'Z萠WL ~InteractiveUtils DownloadCoolUP5>HTTP DownloadCool_䇽UlD*Base64 DownloadCoolax$,J $:Downloads DownloadCool8/opt/julia/packages/Pluto/GVuR6/src/webserver/MsgPack.jl\Aj2 EY8pDatesqK#rS"NMsgPack?/opt/julia/packages/Pluto/GVuR6/src/webserver/SessionActions.jl\A,zXzsy`{FileWatchingSessionActionsUP5>HTTPSessionActionsrz9[viqUUIDsSessionActions7/opt/julia/packages/Pluto/GVuR6/src/webserver/Static.jl\Aemail-is-c00l.nlMIMEs?/opt/julia/packages/Pluto/GVuR6/src/webserver/Authentication.jlәA7/opt/julia/packages/Pluto/GVuR6/src/webserver/Router.jl\A8/opt/julia/packages/Pluto/GVuR6/src/webserver/Dynamic.jlәA:/opt/julia/packages/Pluto/GVuR6/src/webserver/Firebasey.jlәA Firebaseyz`sZPn7Markdown Firebasey!@'Z萠WL ~InteractiveUtils Firebasey?/opt/julia/packages/Pluto/GVuR6/src/webserver/FirebaseyUtils.jl\AFirebaseyUtilsz`sZPn7MarkdownFirebaseyUtils!@'Z萠WL ~InteractiveUtilsFirebaseyUtils:/opt/julia/packages/Pluto/GVuR6/src/webserver/REPLTools.jl\A;N2MB16Malt:/opt/julia/packages/Pluto/GVuR6/src/webserver/WebServer.jl\Aސݗ1V$ bdSockets1/opt/julia/packages/Pluto/GVuR6/src/precompile.jlәA *gnV@jjPrecompileTools%T|&FTΑPrecompileSignaturesprecompile_workloadCoremуJ5Basemу]J5MainmуJ5ArgToolsBń x(mуF K5 Artifactsmr-V3|mу K5Base64UlD*_mу> K5CRC32c\y.jmуj K5 FileWatchingXzsy`{,zmуh& K5LibdluVW59˗,mу-" K5LoggingT{VhUXM=mуrU" K5MmapP~:xg,Omу|' K5NetworkOptionsC0YW,mуʠ, K5SHAQ<$!<%mу1 K5 Serialization [)*k1mу-G K5Sockets1V$ bdސݗmуYBY K5UnicodeP>I>Nrmуeszo K5 LinearAlgebraSm7̏mуuux K5 OpenBLAS_jll[(Śb6EcQ FmуDux K5libblastrampoline_jllLSۆ }lxӠmу^} K5MarkdownZPn7z`smу/Ed~ K5Printfg^cX׸QDmу;h K5Random_ɢ?\Ymу? K5TarOi>աmу!t, K5DatesEY8pj2 mуX K5FuturebS;3{I xVMmуsD K5InteractiveUtilsWL ~@'ZmуVg K5LibGit2Z[&RPTv3EКRmу8J K5 LibGit2_jll YXg}]$mуD K5 MbedTLS_jllAX 3ȡ_mу- K5 LibSSH2_jlloTZk)߆7 LoggingExtrasvSzԗ6l7b6MbedTLSAQ)smsp G205BitFlags_dΣ5oi~~QcV6 JLLWrappersK<;+i2<7 OpenSSL_jllP.#V"\"6TranscodingStreams(P;BŦ&߸Y07TestExtRm()Dy~W 97Zlib_jll?QXZwzTחNUO5 CodecZlibZ\xfK1Sj=8SimpleBufferStreamKTzw8P"\(-6HTTPP5>U638 Distributedo[\(  pW1ѳ X6MaltMB16;N2q虨7MsgPackS"NqK#ru -8 6MIMEs-c00l.nlemail-ishCRU6PrecompileSignaturesFTΑT|&.̞ )6s v) _@: Aa ! 0generic,/opt/julia/packages/Pluto/GVuR6/src/Pluto.jl""" Start a notebook server using: ```julia julia> Pluto.run() ``` Have a look at the FAQ: https://github.com/fonsp/Pluto.jl/wiki """ module Pluto if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@max_methods")) @eval Base.Experimental.@max_methods 1 end import FuzzyCompletions import RelocatableFolders: @path const ROOT_DIR = normpath(joinpath(@__DIR__, "..")) const FRONTEND_DIR = @path(joinpath(ROOT_DIR, "frontend")) const FRONTEND_DIST_DIR = let dir = joinpath(ROOT_DIR, "frontend-dist") isdir(dir) ? @path(dir) : FRONTEND_DIR end const frontend_dist_exists = FRONTEND_DIR !== FRONTEND_DIST_DIR const SAMPLE_DIR = @path(joinpath(ROOT_DIR, "sample")) const RUNNER_DIR = @path(joinpath(ROOT_DIR, "src", "runner")) function project_relative_path(root, xs...) root == joinpath("src", "runner") ? joinpath(RUNNER_DIR, xs...) : root == "frontend-dist" && frontend_dist_exists ? joinpath(FRONTEND_DIST_DIR, xs...) : root == "frontend" ? joinpath(FRONTEND_DIR, xs...) : root == "sample" ? joinpath(SAMPLE_DIR, xs...) : normpath(joinpath(pkgdir(Pluto), root, xs...)) end import Pkg import Scratch include_dependency("../Project.toml") const PLUTO_VERSION = VersionNumber(Pkg.TOML.parsefile(joinpath(ROOT_DIR, "Project.toml"))["version"]) const PLUTO_VERSION_STR = "v$(string(PLUTO_VERSION))" const JULIA_VERSION_STR = "v$(string(VERSION))" import PlutoDependencyExplorer: PlutoDependencyExplorer, TopologicalOrder, NotebookTopology, ExprAnalysisCache, ImmutableVector, ExpressionExplorerExtras, topological_order, all_cells, disjoint, where_assigned, where_referenced using ExpressionExplorer include("./notebook/path helpers.jl") include("./notebook/Export.jl") include("./Configuration.jl") include("./evaluation/Tokens.jl") include("./evaluation/Throttled.jl") include("./runner/PlutoRunner/src/PlutoRunner.jl") include("./packages/PkgCompat.jl") include("./webserver/Status.jl") include("./notebook/Cell.jl") include("./notebook/Notebook.jl") include("./notebook/saving and loading.jl") include("./notebook/frontmatter.jl") include("./notebook/Events.jl") include("./webserver/Session.jl") include("./webserver/PutUpdates.jl") include("./analysis/Parse.jl") include("./analysis/is_just_text.jl") include("./analysis/DependencyCache.jl") include("./analysis/MoreAnalysis.jl") include("./evaluation/WorkspaceManager.jl") include("./evaluation/MacroAnalysis.jl") include("./packages/IOListener.jl") include("./packages/precompile_isolated.jl") include("./packages/Packages.jl") include("./packages/PkgUtils.jl") include("./evaluation/Run.jl") include("./evaluation/RunBonds.jl") module DownloadCool include("./webserver/data_url.jl") end include("./webserver/MsgPack.jl") include("./webserver/SessionActions.jl") include("./webserver/Static.jl") include("./webserver/Authentication.jl") include("./webserver/Router.jl") include("./webserver/Dynamic.jl") include("./webserver/REPLTools.jl") include("./webserver/WebServer.jl") const reset_notebook_environment = PkgUtils.reset_notebook_environment const update_notebook_environment = PkgUtils.update_notebook_environment const activate_notebook_environment = PkgUtils.activate_notebook_environment export reset_notebook_environment export update_notebook_environment export activate_notebook_environment include("./precompile.jl") const pluto_boot_environment_path = Ref{String}() function __init__() pluto_boot_environment_name = "pluto-boot-environment-$(VERSION)-$(PLUTO_VERSION)" pluto_boot_environment_path[] = Scratch.@get_scratch!(pluto_boot_environment_name) # Print a welcome banner if (get(ENV, "JULIA_PLUTO_SHOW_BANNER", "1") != "0" && get(ENV, "CI", "🍄") != "true" && isinteractive()) # Print the banner only once per version, if there isn't # yet a file for this version in banner_shown scratch space. # (Using the Pluto version as the filename enables later # version-specific "what's new" messages.) fn = joinpath(Scratch.@get_scratch!("banner_shown"), PLUTO_VERSION_STR) if !isfile(fn) @info """ Welcome to Pluto $(PLUTO_VERSION_STR) 🎈 Start a notebook server using: julia> Pluto.run() Have a look at the FAQ: https://github.com/fonsp/Pluto.jl/wiki """ # create empty file to indicate that we've shown the banner write(fn, ""); end end end end </opt/julia/packages/Pluto/GVuR6/src/notebook/path helpers.jl8import Base64: base64decode # from https://github.com/JuliaLang/julia/pull/36425 function detectwsl() Sys.islinux() && isfile("/proc/sys/kernel/osrelease") && occursin(r"Microsoft|WSL"i, read("/proc/sys/kernel/osrelease", String)) end """ maybe_convert_path_to_wsl(path) Return the WSL path if the system is using the Windows Subsystem for Linux (WSL) and return `path` otherwise. WSL mounts the windows drive to /mnt/ and provides a utility tool to convert windows paths into WSL paths. This function will try to use this tool to automagically convert paths pasted from windows (with the right click -> copy as path functionality) into paths Pluto can understand. Example: $(raw"C:\Users\pankg\OneDrive\Desktop\pluto\bakery_pnl_ready2.jl") → "/mnt/c/Users/pankg/OneDrive/Desktop/pluto/bakery_pnl_ready2.jl" but "/mnt/c/Users/pankg/OneDrive/Desktop/pluto/bakery_pnl_ready2.jl" stays the same """ function maybe_convert_path_to_wsl(path) try isfile(path) && return path if detectwsl() # wslpath utility prints path to stderr if it fails to convert # (it used to fail for WSL-valid paths) !isnothing(match(r"^/mnt/\w+/", path)) && return path return readchomp(pipeline(`wslpath -u $(path)`; stderr=devnull)) end catch e return path end return path end const adjectives = [ "groundbreaking" "revolutionary" "important" "novel" "fun" "interesting" "fascinating" "exciting" "surprising" "remarkable" "wonderful" "stunning" "mini" "small" "tiny" "cute" "friendly" "wild" ] const nouns = [ "discovery" "experiment" "story" "journal" "notebook" "revelation" "computation" "creation" "analysis" "invention" "blueprint" "report" "science" "magic" "program" "notes" "lecture" "theory" "proof" "conjecture" ] """ Generate a filename like `"Cute discovery"`. Does not end with `.jl`. """ function cutename() titlecase(rand(adjectives)) * " " * rand(nouns) end function new_notebooks_directory() try path = get( ENV, "JULIA_PLUTO_NEW_NOTEBOOKS_DIR", joinpath(first(DEPOT_PATH), "pluto_notebooks") ) if !isdir(path) mkdir(path) end path catch homedir() end end """ Standard Pluto file extensions, including `.jl` and `.pluto.jl`. Pluto can open files with any extension, but the default extensions are used when searching for notebooks, or when trying to create a nice filename for something else, like the backup file. """ const pluto_file_extensions = [ ".pluto.jl", ".Pluto.jl", ".nb.jl", ".jl", ".plutojl", ".pluto", ".nbjl", ".pljl", ".pluto.jl.txt", # MacOS can create these .txt files sometimes ".jl.txt", ] endswith_pluto_file_extension(s) = any(endswith(s, e) for e in pluto_file_extensions) """ Extract the Julia notebook file contents from a Pluto-exported HTML file. """ function embedded_notebookfile(html_contents::AbstractString)::String if !occursin("", html_contents) throw(ArgumentError("Pass the contents of a Pluto-exported HTML file as argument.")) end m = match(r"pluto_notebookfile.*\"data\:.*base64\,(.*)\"", html_contents) if m === nothing throw(ArgumentError("Notebook does not have an embedded notebook file.")) else String(base64decode(m.captures[1])) end end """ Does the path end with a pluto file extension (like `.jl` or `.pluto.jl`) and does the first line say `### A Pluto.jl notebook ###`? """ is_pluto_notebook(path::String) = endswith_pluto_file_extension(path) && readline(path) == "### A Pluto.jl notebook ###" function without_pluto_file_extension(s) for e in pluto_file_extensions if endswith(s, e) return s[1:prevind(s, ncodeunits(s), ncodeunits(e))] end end s end """ Return `base` * `suffix` if the file does not exist yet. If it does, return `base * sep * string(n) * suffix`, where `n` is the smallest natural number such that the file is new. (no 0 is not a natural number you snake) """ function numbered_until_new(base::AbstractString; sep::AbstractString=" ", suffix::AbstractString=".jl", create_file::Bool=true, skip_original::Bool=false) chosen = base * suffix n = 1 while (n == 1 && skip_original) || isfile(chosen) chosen = base * sep * string(n) * suffix n += 1 end if create_file touch(chosen) end chosen end backup_filename(path) = numbered_until_new(without_pluto_file_extension(path); sep=" backup ", suffix=".jl", create_file=false, skip_original=true) "Like `cp` except we create the file manually (to fix permission issues). (It's not plagiarism if you use this function to copy homework.)" function readwrite(from::AbstractString, to::AbstractString) write(to, read(from, String)) end function tryexpanduser(path) try expanduser(path) catch ex path end end const tamepath = abspath ∘ tryexpanduser "Block until reading the file two times in a row gave the same result." function wait_until_file_unchanged(filename::String, timeout::Real, last_contents::String="")::Nothing new_contents = try read(filename, String) catch "" end @info "Waiting for file to stabilize..."# last_contents new_contents if last_contents == new_contents # yayyy return else sleep(timeout) wait_until_file_unchanged(filename, timeout, new_contents) end end6/opt/julia/packages/Pluto/GVuR6/src/notebook/Export.jl$import Pkg using Base64 using HypertextLiteral import URIs const default_binder_url = "https://mybinder.org/v2/gh/fonsp/pluto-on-binder/v$(string(PLUTO_VERSION))" const cdn_version_override = nothing # const cdn_version_override = "2a48ae2" if cdn_version_override !== nothing @warn "Reminder to fonsi: Using a development version of Pluto for CDN assets. The binder button might not work. You should not see this on a released version of Pluto." cdn_version_override end cdnified_editor_html(; kwargs...) = cdnified_html("editor.html"; kwargs...) function cdnified_html(filename::AbstractString; version::Union{Nothing,VersionNumber,AbstractString}=nothing, pluto_cdn_root::Union{Nothing,AbstractString}=nothing, ) should_use_bundled_cdn = version ∈ (nothing, PLUTO_VERSION) && pluto_cdn_root === nothing something( if should_use_bundled_cdn try original = read(project_relative_path("frontend-dist", filename), String) cdn_root = "https://cdn.jsdelivr.net/gh/fonsp/Pluto.jl@$(string(PLUTO_VERSION))/frontend-dist/" @debug "Using CDN for Pluto assets:" cdn_root replace_with_cdn(original) do url # Because parcel creates filenames with a hash in them, we can check if the file exists locally to make sure that everything is in order. @assert isfile(project_relative_path("frontend-dist", url)) "Could not find the file $(project_relative_path("frontend-dist", url)) locally, that's a bad sign." URIs.resolvereference(cdn_root, url) |> string end catch e @warn "Could not use bundled CDN version of $(filename). You should only see this message if you are using a fork of Pluto." exception=(e,catch_backtrace()) maxlog=1 nothing end end, let original = read(project_relative_path("frontend", filename), String) cdn_root = something(pluto_cdn_root, "https://cdn.jsdelivr.net/gh/fonsp/Pluto.jl@$(something(cdn_version_override, string(something(version, PLUTO_VERSION))))/frontend/") @debug "Using CDN for Pluto assets:" cdn_root replace_with_cdn(original) do url URIs.resolvereference(cdn_root, url) |> string end end ) end const _insertion_meta = """""" const _insertion_parameters = """""" const _insertion_preload = """""" inserted_html(original_contents::AbstractString; meta::AbstractString="", parameters::AbstractString="", preload::AbstractString="", ) = replace_at_least_once( replace_at_least_once( replace_at_least_once(original_contents, _insertion_meta => """ $(meta) $(_insertion_meta) """ ), _insertion_parameters => """ $(parameters) $(_insertion_parameters) """ ), _insertion_preload => """ $(preload) $(_insertion_preload) """ ) function prefetch_statefile_html(statefile_js::AbstractString) if length(statefile_js) < 300 && startswith(statefile_js, '"') && endswith(statefile_js, '"') && !startswith(statefile_js, "\"data:") """\n\n""" else "" end end """ See [PlutoSliderServer.jl](https://github.com/JuliaPluto/PlutoSliderServer.jl) if you are interested in exporting notebooks programatically. """ function generate_html(; version::Union{Nothing,VersionNumber,AbstractString}=nothing, pluto_cdn_root::Union{Nothing,AbstractString}=nothing, notebookfile_js::AbstractString="undefined", statefile_js::AbstractString="undefined", slider_server_url_js::AbstractString="undefined", binder_url_js::AbstractString=repr(default_binder_url), disable_ui::Bool=true, preamble_html_js::AbstractString="undefined", notebook_id_js::AbstractString="undefined", isolated_cell_ids_js::AbstractString="undefined", header_html::AbstractString="", )::String cdnified = cdnified_editor_html(; version, pluto_cdn_root) length(statefile_js) > 32000000 && @error "Statefile embedded in HTML is very large. The file can be opened with Chrome and Safari, but probably not with Firefox. If you are using PlutoSliderServer to generate this file, then we recommend the setting `baked_statefile=false`. If you are not using PlutoSliderServer, then consider reducing the size of figures and output in the notebook." length(statefile_js) parameters = """ """ preload = prefetch_statefile_html(statefile_js) inserted_html(cdnified; meta=header_html, parameters, preload) end function replace_at_least_once(s, pair) from, to = pair @assert occursin(from, s) replace(s, pair) end function generate_html(notebook; kwargs...)::String state = notebook_to_js(notebook) notebookfile_js = let notebookfile64 = base64encode() do io save_notebook(io, notebook) end "\"data:text/julia;charset=utf-8;base64,$(notebookfile64)\"" end statefile_js = let statefile64 = base64encode() do io pack(io, state) end "\"data:;base64,$(statefile64)\"" end fm = frontmatter(notebook) header_html = isempty(fm) ? "" : frontmatter_html(fm) # avoid loading HypertextLiteral if there is no frontmatter # We don't set `notebook_id_js` because this is generated by the server, the option is only there for funky setups. generate_html(; statefile_js, notebookfile_js, header_html, kwargs...) end const frontmatter_writers = ( ("title", x -> @htl(""" $(x) """)), ("description", x -> @htl(""" """)), ("tags", x -> x isa Vector ? @htl("$(( @htl(""" """) for t in x ))") : nothing), ) const _og_properties = ("title", "type", "description", "image", "url", "audio", "video", "site_name", "locale", "locale:alternate", "determiner") const _default_frontmatter = Dict{String, Any}( "type" => "article", # Note: these defaults are skipped when there is no frontmatter at all. ) function frontmatter_html(frontmatter::Dict{String,Any}; default_frontmatter::Dict{String,Any}=_default_frontmatter)::String d = merge(default_frontmatter, frontmatter) repr(MIME"text/html"(), @htl("""$(( f(d[key]) for (key, f) in frontmatter_writers if haskey(d, key) ))$(( @htl(""" """) for (key, val) in d if key in _og_properties ))""")) end replace_substring(s::String, sub::SubString, newval::AbstractString) = *( SubString(s, 1, prevind(s, sub.offset + 1, 1)), newval, SubString(s, nextind(s, sub.offset + sub.ncodeunits)) ) const dont_cdnify = ("new","open","shutdown","move","notebooklist","notebookfile","statefile","notebookexport","notebookupload") const source_pattern = r"\s(?:src|href)=\"(.+?)\"" function replace_with_cdn(cdnify::Function, s::String, idx::Integer=1) next_match = match(source_pattern, s, idx) if next_match === nothing s else url = only(next_match.captures) if occursin("//", url) || url ∈ dont_cdnify # skip this one replace_with_cdn(cdnify, s, nextind(s, next_match.offset)) else replace_with_cdn(cdnify, replace_substring( s, url, cdnify(url) )) end end end """ Generate a custom index.html that is designed to display a custom set of featured notebooks, without the file UI or Pluto logo. This is to be used by [PlutoSliderServer.jl](https://github.com/JuliaPluto/PlutoSliderServer.jl) to show a fancy index page. """ function generate_index_html(; version::Union{Nothing,VersionNumber,AbstractString}=nothing, pluto_cdn_root::Union{Nothing,AbstractString}=nothing, featured_direct_html_links::Bool=false, featured_sources_js::AbstractString="undefined", ) cdnified = cdnified_html("index.html"; version, pluto_cdn_root) meta = """ """ parameters = """ """ preload = prefetch_statefile_html(featured_sources_js) inserted_html(cdnified; meta, parameters, preload) end 4/opt/julia/packages/Pluto/GVuR6/src/Configuration.jlP""" The full list of keyword arguments that can be passed to [`Pluto.run`](@ref) (or [`Pluto.Configuration.from_flat_kwargs`](@ref)) is divided into four categories. Take a look at the documentation for: - [`Pluto.Configuration.CompilerOptions`](@ref) defines the command line arguments for notebook `julia` processes. - [`Pluto.Configuration.ServerOptions`](@ref) configures the HTTP server. - [`Pluto.Configuration.SecurityOptions`](@ref) configures the authentication options for Pluto's HTTP server. Change with caution. - [`Pluto.Configuration.EvaluationOptions`](@ref) is used internally during Pluto's testing. Note that Pluto is designed to be _zero-configuration_, and most users should not (have to) change these settings. Most 'customization' can be achieved using Julia's wide range of packages! That being said, the available settings are useful if you are using Pluto in a special environment, such as docker, mybinder, etc. """ module Configuration using Configurations # https://github.com/Roger-luo/Configurations.jl import ..Pluto: tamepath safepwd() = try pwd() catch e @warn "pwd() failure" exception=(e, catch_backtrace()) homedir() end # Using a ref to avoid fixing the pwd() output during the compilation phase. We don't want this value to be baked into the sysimage, because it depends on the `pwd()`. We do want to cache it, because the pwd might change while Pluto is running. const pwd_ref = Ref{String}() function notebook_path_suggestion() pwd_val = if isassigned(pwd_ref) pwd_ref[] else safepwd() end preferred_dir = startswith(Sys.BINDIR, pwd_val) ? homedir() : pwd_val # so that it ends with / or \ string(joinpath(preferred_dir, "")) end function __init__() pwd_ref[] = safepwd() end const ROOT_URL_DEFAULT = nothing const BASE_URL_DEFAULT = "/" const HOST_DEFAULT = "127.0.0.1" const PORT_DEFAULT = nothing const PORT_HINT_DEFAULT = 1234 const LAUNCH_BROWSER_DEFAULT = true const DISMISS_UPDATE_NOTIFICATION_DEFAULT = false const SHOW_FILE_SYSTEM_DEFAULT = true const ENABLE_PACKAGE_AUTHOR_FEATURES_DEFAULT = true const DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT = false const AUTO_RELOAD_FROM_FILE_DEFAULT = false const AUTO_RELOAD_FROM_FILE_COOLDOWN_DEFAULT = 0.4 const AUTO_RELOAD_FROM_FILE_IGNORE_PKG_DEFAULT = false const NOTEBOOK_DEFAULT = nothing const INIT_WITH_FILE_VIEWER_DEFAULT = false const SIMULATED_LAG_DEFAULT = 0.0 const SIMULATED_PKG_LAG_DEFAULT = 0.0 const INJECTED_JAVASCRIPT_DATA_URL_DEFAULT = "data:text/javascript;base64," const ON_EVENT_DEFAULT = function(a) #= @info "$(typeof(a))" =# end """ ServerOptions([; kwargs...]) The HTTP server options. See [`SecurityOptions`](@ref) for additional settings. # Keyword arguments - `host::String = "$HOST_DEFAULT"` Set to `"127.0.0.1"` (default) to run on *localhost*, which makes the server available to your computer and the local network (LAN). Set to `"0.0.0.0"` to make the server available to the entire network (internet). - `port::Union{Nothing,Integer} = $PORT_DEFAULT` When specified, this port will be used for the server. - `port_hint::Integer = $PORT_HINT_DEFAULT` If the other setting `port` is not specified, then this setting (`port_hint`) will be used as the starting point in finding an available port to run the server on. - `launch_browser::Bool = $LAUNCH_BROWSER_DEFAULT` - `dismiss_update_notification::Bool = $DISMISS_UPDATE_NOTIFICATION_DEFAULT` If `false`, the Pluto frontend will check the Pluto.jl github releases for any new recommended updates, and show a notification if there are any. If `true`, this is disabled. - `show_file_system::Bool = $SHOW_FILE_SYSTEM_DEFAULT` - `notebook_path_suggestion::String = notebook_path_suggestion()` - `disable_writing_notebook_files::Bool = $DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT` - `auto_reload_from_file::Bool = $AUTO_RELOAD_FROM_FILE_DEFAULT` Watch notebook files for outside changes and update running notebook state automatically - `auto_reload_from_file_cooldown::Real = $AUTO_RELOAD_FROM_FILE_COOLDOWN_DEFAULT` Experimental, will be removed - `auto_reload_from_file_ignore_pkg::Bool = $AUTO_RELOAD_FROM_FILE_IGNORE_PKG_DEFAULT` Experimental flag, will be removed - `notebook::Union{Nothing,String} = $NOTEBOOK_DEFAULT` Optional path of notebook to launch at start - `init_with_file_viewer::Bool = $INIT_WITH_FILE_VIEWER_DEFAULT` - `simulated_lag::Real=$SIMULATED_LAG_DEFAULT` (internal) Extra lag to add to our server responses. Will be multiplied by `0.5 + rand()`. - `simulated_pkg_lag::Real=$SIMULATED_PKG_LAG_DEFAULT` (internal) Extra lag to add to operations done by Pluto's package manager. Will be multiplied by `0.5 + rand()`. - `injected_javascript_data_url::String = "$INJECTED_JAVASCRIPT_DATA_URL_DEFAULT"` (internal) Optional javascript injectables to the front-end. Can be used to customize the editor, but this API is not meant for general use yet. - `on_event::Function = $ON_EVENT_DEFAULT` - `root_url::Union{Nothing,String} = $ROOT_URL_DEFAULT` This setting is used to specify the root URL of the Pluto server, but this setting is *only* used to customize the launch message (*"Go to http://localhost:1234/ in your browser"*). You can probably ignore this and use `base_url` instead. - `base_url::String = "$BASE_URL_DEFAULT"` This (advanced) setting is used to specify a subpath at which the Pluto server will run, it should be a path starting and ending with a '/'. E.g. with `base_url = "/hello/world/"`, the server will run at `http://localhost:1234/hello/world/`, and you edit a notebook at `http://localhost:1234/hello/world/edit?id=...`. """ @option mutable struct ServerOptions root_url::Union{Nothing,String} = ROOT_URL_DEFAULT base_url::String = BASE_URL_DEFAULT host::String = HOST_DEFAULT port::Union{Nothing,Integer} = PORT_DEFAULT port_hint::Integer = PORT_HINT_DEFAULT launch_browser::Bool = LAUNCH_BROWSER_DEFAULT dismiss_update_notification::Bool = DISMISS_UPDATE_NOTIFICATION_DEFAULT show_file_system::Bool = SHOW_FILE_SYSTEM_DEFAULT notebook_path_suggestion::String = notebook_path_suggestion() disable_writing_notebook_files::Bool = DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT auto_reload_from_file::Bool = AUTO_RELOAD_FROM_FILE_DEFAULT auto_reload_from_file_cooldown::Real = AUTO_RELOAD_FROM_FILE_COOLDOWN_DEFAULT auto_reload_from_file_ignore_pkg::Bool = AUTO_RELOAD_FROM_FILE_IGNORE_PKG_DEFAULT notebook::Union{Nothing,String,Vector{<:String}} = NOTEBOOK_DEFAULT init_with_file_viewer::Bool = INIT_WITH_FILE_VIEWER_DEFAULT simulated_lag::Real = SIMULATED_LAG_DEFAULT simulated_pkg_lag::Real = SIMULATED_PKG_LAG_DEFAULT injected_javascript_data_url::String = INJECTED_JAVASCRIPT_DATA_URL_DEFAULT on_event::Function = ON_EVENT_DEFAULT end const REQUIRE_SECRET_FOR_OPEN_LINKS_DEFAULT = true const REQUIRE_SECRET_FOR_ACCESS_DEFAULT = true const WARN_ABOUT_UNTRUSTED_CODE_DEFAULT = true """ SecurityOptions([; kwargs...]) Security settings for the HTTP server. # Arguments - `require_secret_for_open_links::Bool = $REQUIRE_SECRET_FOR_OPEN_LINKS_DEFAULT` Whether the links `http://localhost:1234/open?path=/a/b/c.jl` and `http://localhost:1234/open?url=http://www.a.b/c.jl` should be protected. Use `true` for almost every setup. Only use `false` if Pluto is running in a safe container (like mybinder.org), where arbitrary code execution is not a problem. - `require_secret_for_access::Bool = $REQUIRE_SECRET_FOR_ACCESS_DEFAULT` If `false`, you do not need to use a `secret` in the URL to access Pluto: you will be authenticated by visiting `http://localhost:1234/` in your browser. An authentication cookie is still used for access (to prevent XSS and deceptive links or an img src to `http://localhost:1234/open?url=badpeople.org/script.jl`), and is set automatically, but this request to `/` is protected by cross-origin policy. Use `true` on a computer used by multiple people simultaneously. Only use `false` if necessary. - `warn_about_untrusted_code::Bool = $WARN_ABOUT_UNTRUSTED_CODE_DEFAULT` Should the Pluto GUI show warning messages about executing code from an unknown source, e.g. when opening a notebook from a URL? When `false`, notebooks will still open in Safe mode, but there is no scary message when you run it. **Leave these options on `true` for the most secure setup.** Note that Pluto is quickly evolving software, maintained by designers, educators and enthusiasts — not security experts. If security is a serious concern for your application, then we recommend running Pluto inside a container and verifying the relevant security aspects of Pluto yourself. """ @option mutable struct SecurityOptions require_secret_for_open_links::Bool = REQUIRE_SECRET_FOR_OPEN_LINKS_DEFAULT require_secret_for_access::Bool = REQUIRE_SECRET_FOR_ACCESS_DEFAULT warn_about_untrusted_code::Bool = WARN_ABOUT_UNTRUSTED_CODE_DEFAULT end const RUN_NOTEBOOK_ON_LOAD_DEFAULT = true const WORKSPACE_USE_DISTRIBUTED_DEFAULT = true const WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT = nothing const LAZY_WORKSPACE_CREATION_DEFAULT = false const CAPTURE_STDOUT_DEFAULT = true const WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT = nothing """ EvaluationOptions([; kwargs...]) Options to change Pluto's evaluation behaviour during internal testing and by downstream packages. These options are not intended to be changed during normal use. - `run_notebook_on_load::Bool = $RUN_NOTEBOOK_ON_LOAD_DEFAULT` When running a notebook (not in Safe mode), should all cells evaluate immediately? Warning: this is only for internal testing, and using it will lead to unexpected behaviour and hard-to-reproduce notebooks. It's not the Pluto way! - `workspace_use_distributed::Bool = $WORKSPACE_USE_DISTRIBUTED_DEFAULT` Whether to start notebooks in a separate process. - `workspace_use_distributed_stdlib::Bool? = $WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT` Should we use the Distributed stdlib to run processes? Distributed will be replaced by Malt.jl, you can use this option to already get the old behaviour. `nothing` means: determine automatically (which is currently `false`). - `lazy_workspace_creation::Bool = $LAZY_WORKSPACE_CREATION_DEFAULT` - `capture_stdout::Bool = $CAPTURE_STDOUT_DEFAULT` - `workspace_custom_startup_expr::Union{Nothing,String} = $WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT` An expression to be evaluated in the workspace process before running notebook code. """ @option mutable struct EvaluationOptions run_notebook_on_load::Bool = RUN_NOTEBOOK_ON_LOAD_DEFAULT workspace_use_distributed::Bool = WORKSPACE_USE_DISTRIBUTED_DEFAULT workspace_use_distributed_stdlib::Union{Bool,Nothing} = WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT lazy_workspace_creation::Bool = LAZY_WORKSPACE_CREATION_DEFAULT capture_stdout::Bool = CAPTURE_STDOUT_DEFAULT workspace_custom_startup_expr::Union{Nothing,String} = WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT end const COMPILE_DEFAULT = nothing const PKGIMAGES_DEFAULT = nothing const COMPILED_MODULES_DEFAULT = nothing const SYSIMAGE_DEFAULT = nothing const SYSIMAGE_NATIVE_CODE_DEFAULT = nothing const BANNER_DEFAULT = nothing const DEPWARN_DEFAULT = nothing const OPTIMIZE_DEFAULT = nothing const MIN_OPTLEVEL_DEFAULT = nothing const INLINE_DEFAULT = nothing const CHECK_BOUNDS_DEFAULT = nothing const MATH_MODE_DEFAULT = nothing const STARTUP_FILE_DEFAULT = "no" const HISTORY_FILE_DEFAULT = "no" const HEAP_SIZE_HINT_DEFAULT = nothing function roughly_the_number_of_physical_cpu_cores() # https://gist.github.com/fonsp/738fe244719cae820245aa479e7b4a8d threads = Sys.CPU_THREADS num_threads_is_maybe_doubled_for_marketing = Sys.ARCH === :x86_64 if threads == 1 1 elseif threads == 2 || threads == 3 2 elseif num_threads_is_maybe_doubled_for_marketing # This includes: # - intel hyperthreading # - Apple ARM efficiency cores included in the count (when running the x86 executable) threads ÷ 2 else threads end end function default_number_of_threads() env_value = get(ENV, "JULIA_NUM_THREADS", "") all(isspace, env_value) ? roughly_the_number_of_physical_cpu_cores() : env_value end """ CompilerOptions([; kwargs...]) These options will be passed as command line argument to newly launched processes. See [the Julia documentation on command-line options](https://docs.julialang.org/en/v1/manual/command-line-options/). # Arguments - `compile::Union{Nothing,String} = $COMPILE_DEFAULT` - `pkgimages::Union{Nothing,String} = $PKGIMAGES_DEFAULT` - `compiled_modules::Union{Nothing,String} = $COMPILED_MODULES_DEFAULT` - `sysimage::Union{Nothing,String} = $SYSIMAGE_DEFAULT` - `sysimage_native_code::Union{Nothing,String} = $SYSIMAGE_NATIVE_CODE_DEFAULT` - `banner::Union{Nothing,String} = $BANNER_DEFAULT` - `depwarn::Union{Nothing,String} = $DEPWARN_DEFAULT` - `optimize::Union{Nothing,Int} = $OPTIMIZE_DEFAULT` - `min_optlevel::Union{Nothing,Int} = $MIN_OPTLEVEL_DEFAULT` - `inline::Union{Nothing,String} = $INLINE_DEFAULT` - `check_bounds::Union{Nothing,String} = $CHECK_BOUNDS_DEFAULT` - `math_mode::Union{Nothing,String} = $MATH_MODE_DEFAULT` - `heap_size_hint::Union{Nothing,String} = $HEAP_SIZE_HINT_DEFAULT` - `startup_file::Union{Nothing,String} = "$STARTUP_FILE_DEFAULT"` By default, the startup file isn't loaded in notebooks. - `history_file::Union{Nothing,String} = "$HISTORY_FILE_DEFAULT"` By default, the history isn't loaded in notebooks. - `threads::Union{Nothing,String,Int} = default_number_of_threads()` """ @option mutable struct CompilerOptions compile::Union{Nothing,String} = COMPILE_DEFAULT pkgimages::Union{Nothing,String} = PKGIMAGES_DEFAULT compiled_modules::Union{Nothing,String} = COMPILED_MODULES_DEFAULT sysimage::Union{Nothing,String} = SYSIMAGE_DEFAULT sysimage_native_code::Union{Nothing,String} = SYSIMAGE_NATIVE_CODE_DEFAULT banner::Union{Nothing,String} = BANNER_DEFAULT depwarn::Union{Nothing,String} = DEPWARN_DEFAULT optimize::Union{Nothing,Int} = OPTIMIZE_DEFAULT min_optlevel::Union{Nothing,Int} = MIN_OPTLEVEL_DEFAULT inline::Union{Nothing,String} = INLINE_DEFAULT check_bounds::Union{Nothing,String} = CHECK_BOUNDS_DEFAULT math_mode::Union{Nothing,String} = MATH_MODE_DEFAULT heap_size_hint::Union{Nothing,String} = HEAP_SIZE_HINT_DEFAULT # notebook specified options # the followings are different from # the default julia compiler options startup_file::Union{Nothing,String} = STARTUP_FILE_DEFAULT history_file::Union{Nothing,String} = HISTORY_FILE_DEFAULT threads::Union{Nothing,String,Int} = default_number_of_threads() end """ Collection of all settings that configure a Pluto session. `ServerSession` contains a `Configuration`. """ @option struct Options server::ServerOptions = ServerOptions() security::SecurityOptions = SecurityOptions() evaluation::EvaluationOptions = EvaluationOptions() compiler::CompilerOptions = CompilerOptions() end function from_flat_kwargs(; root_url::Union{Nothing,String} = ROOT_URL_DEFAULT, base_url::String = BASE_URL_DEFAULT, host::String = HOST_DEFAULT, port::Union{Nothing,Integer} = PORT_DEFAULT, port_hint::Integer = PORT_HINT_DEFAULT, launch_browser::Bool = LAUNCH_BROWSER_DEFAULT, dismiss_update_notification::Bool = DISMISS_UPDATE_NOTIFICATION_DEFAULT, show_file_system::Bool = SHOW_FILE_SYSTEM_DEFAULT, notebook_path_suggestion::String = notebook_path_suggestion(), disable_writing_notebook_files::Bool = DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT, auto_reload_from_file::Bool = AUTO_RELOAD_FROM_FILE_DEFAULT, auto_reload_from_file_cooldown::Real = AUTO_RELOAD_FROM_FILE_COOLDOWN_DEFAULT, auto_reload_from_file_ignore_pkg::Bool = AUTO_RELOAD_FROM_FILE_IGNORE_PKG_DEFAULT, notebook::Union{Nothing,String,Vector{<:String}} = NOTEBOOK_DEFAULT, init_with_file_viewer::Bool = INIT_WITH_FILE_VIEWER_DEFAULT, simulated_lag::Real = SIMULATED_LAG_DEFAULT, simulated_pkg_lag::Real = SIMULATED_PKG_LAG_DEFAULT, injected_javascript_data_url::String = INJECTED_JAVASCRIPT_DATA_URL_DEFAULT, on_event::Function = ON_EVENT_DEFAULT, require_secret_for_open_links::Bool = REQUIRE_SECRET_FOR_OPEN_LINKS_DEFAULT, require_secret_for_access::Bool = REQUIRE_SECRET_FOR_ACCESS_DEFAULT, warn_about_untrusted_code::Bool = WARN_ABOUT_UNTRUSTED_CODE_DEFAULT, run_notebook_on_load::Bool = RUN_NOTEBOOK_ON_LOAD_DEFAULT, workspace_use_distributed::Bool = WORKSPACE_USE_DISTRIBUTED_DEFAULT, workspace_use_distributed_stdlib::Union{Bool,Nothing} = WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT, lazy_workspace_creation::Bool = LAZY_WORKSPACE_CREATION_DEFAULT, capture_stdout::Bool = CAPTURE_STDOUT_DEFAULT, workspace_custom_startup_expr::Union{Nothing,String} = WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT, compile::Union{Nothing,String} = COMPILE_DEFAULT, pkgimages::Union{Nothing,String} = PKGIMAGES_DEFAULT, compiled_modules::Union{Nothing,String} = COMPILED_MODULES_DEFAULT, sysimage::Union{Nothing,String} = SYSIMAGE_DEFAULT, sysimage_native_code::Union{Nothing,String} = SYSIMAGE_NATIVE_CODE_DEFAULT, banner::Union{Nothing,String} = BANNER_DEFAULT, depwarn::Union{Nothing,String} = DEPWARN_DEFAULT, optimize::Union{Nothing,Int} = OPTIMIZE_DEFAULT, min_optlevel::Union{Nothing,Int} = MIN_OPTLEVEL_DEFAULT, inline::Union{Nothing,String} = INLINE_DEFAULT, check_bounds::Union{Nothing,String} = CHECK_BOUNDS_DEFAULT, math_mode::Union{Nothing,String} = MATH_MODE_DEFAULT, heap_size_hint::Union{Nothing,String} = HEAP_SIZE_HINT_DEFAULT, startup_file::Union{Nothing,String} = STARTUP_FILE_DEFAULT, history_file::Union{Nothing,String} = HISTORY_FILE_DEFAULT, threads::Union{Nothing,String,Int} = default_number_of_threads(), ) server = ServerOptions(; root_url, base_url, host, port, port_hint, launch_browser, dismiss_update_notification, show_file_system, notebook_path_suggestion, disable_writing_notebook_files, auto_reload_from_file, auto_reload_from_file_cooldown, auto_reload_from_file_ignore_pkg, notebook, init_with_file_viewer, simulated_lag, simulated_pkg_lag, injected_javascript_data_url, on_event, ) security = SecurityOptions(; require_secret_for_open_links, require_secret_for_access, warn_about_untrusted_code, ) evaluation = EvaluationOptions(; run_notebook_on_load, workspace_use_distributed, workspace_use_distributed_stdlib, lazy_workspace_creation, capture_stdout, workspace_custom_startup_expr, ) compiler = CompilerOptions(; compile, pkgimages, compiled_modules, sysimage, sysimage_native_code, banner, depwarn, optimize, min_optlevel, inline, check_bounds, math_mode, heap_size_hint, startup_file, history_file, threads, ) return Options(; server, security, evaluation, compiler) end function _merge_notebook_compiler_options(notebook, options::CompilerOptions)::CompilerOptions if notebook.compiler_options === nothing return options end kwargs = Dict{Symbol,Any}() for each in fieldnames(CompilerOptions) # 1. not specified by notebook options # 2. general notebook specified options if getfield(notebook.compiler_options, each) === nothing kwargs[each] = getfield(options, each) else kwargs[each] = getfield(notebook.compiler_options, each) end end return CompilerOptions(; kwargs...) end function _convert_to_flags(options::CompilerOptions)::Vector{String} option_list = String[] exclude_list = String[] if VERSION < v"1.9" push!(exclude_list, "--heap-size-hint") end for name in fieldnames(CompilerOptions) flagname = string("--", replace(String(name), "_" => "-")) value = getfield(options, name) if value !== nothing && flagname ∉ exclude_list push!(option_list, string(flagname, "=", value)) end end return option_list end end 8/opt/julia/packages/Pluto/GVuR6/src/evaluation/Tokens.jl"A `Token` can only be held by one async process at one time. Use `Base.take!(token)` to claim the token, `Base.put!(token)` to give the token back." struct Token c::Channel{Nothing} Token() = let c = Channel{Nothing}(1) push!(c, nothing) new(c) end end Base.take!(token::Token) = Base.take!(token.c) Base.put!(token::Token) = Base.put!(token.c, nothing) Base.isready(token::Token) = Base.isready(token.c) Base.wait(token::Token) = Base.put!(token.c, Base.take!(token.c)) function withtoken(f::Function, token::Token) take!(token) result = try f() finally put!(token) end result end ### "Track whether some task needs to be done. `request!(requestqueue)` will make sure that the task is done at least once after calling it. Multiple calls might get bundled into one." mutable struct RequestQueue is_processing::Bool c::Channel{Nothing} RequestQueue() = new(false, Channel{Nothing}(1)) end "Give a function (with no arguments) that should be called after a request." function process(f::Function, requestqueue::RequestQueue) @assert !requestqueue.is_processing requestqueue.is_processing = true while true take!(requestqueue.c) f() end end function request!(queue::RequestQueue) if isready(queue.c) push!(queue.c, nothing) end end "Like @async except it prints errors to the terminal. 👶" macro asynclog(expr) quote @async begin # because this is being run asynchronously, we need to catch exceptions manually try $(esc(expr)) catch ex bt = stacktrace(catch_backtrace()) showerror(stderr, ex, bt) rethrow(ex) end end end end ;/opt/julia/packages/Pluto/GVuR6/src/evaluation/Throttled.jl """ throttled(f::Function, timeout::Real) Return a function that when invoked, will only be triggered at most once during `timeout` seconds. The throttled function will run as much as it can, without ever going more than once per `wait` duration. This throttle is 'leading' and has some other properties that are specifically designed for our use in Pluto, see the tests. Inspired by FluxML See: https://github.com/FluxML/Flux.jl/blob/8afedcd6723112ff611555e350a8c84f4e1ad686/src/utils.jl#L662 """ function throttled(f::Function, timeout::Real) tlock = ReentrantLock() iscoolnow = Ref(false) run_later = Ref(false) function flush() lock(tlock) do run_later[] = false f() end end function schedule() @async begin sleep(timeout) if run_later[] flush() end iscoolnow[] = true end end schedule() function throttled_f() if iscoolnow[] iscoolnow[] = false flush() schedule() else run_later[] = true end end return throttled_f, flush end """ simple_leading_throttle(f, delay::Real) Return a function that when invoked, will only be triggered at most once during `timeout` seconds. The throttled function will run as much as it can, without ever going more than once per `wait` duration. Compared to [`throttled`](@ref), this simple function only implements [leading](https://css-tricks.com/debouncing-throttling-explained-examples/) throttling and accepts function with arbitrary number of positional and keyword arguments. """ function simple_leading_throttle(f, delay::Real) last_time = 0.0 return function(args...;kwargs...) now = time() if now - last_time > delay last_time = now f(args...;kwargs...) end end endI/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/PlutoRunner.jl# Will be evaluated _inside_ the workspace process. # Pluto does most things on the server, but it uses worker processes to evaluate notebook code in. # These processes don't import Pluto, they only import this module. # Functions from this module are called by WorkspaceManager.jl via Malt. # When reading this file, pretend that you are living in a worker process, # and you are communicating with Pluto's server, who lives in the main process. # The package environment that this file is loaded with is the NotebookProcessProject.toml file in this directory. # SOME EXTRA NOTES # 1. The entire PlutoRunner should be a single file. # 2. Restrict the communication between this PlutoRunner and the Pluto server to only use *Base Julia types*, like `String`, `Dict`, `NamedTuple`, etc. # These restriction are there to allow flexibility in the way that this file is # loaded on a runner process, which is something that we might want to change # in the future. module PlutoRunner # import these two so that they can be imported from Main on the worker process if it launches without the stdlibs in its LOAD_PATH import Markdown import InteractiveUtils using Markdown import Markdown: html, htmlinline, LaTeX, withtag, htmlesc import Base64 import FuzzyCompletions: FuzzyCompletions, Completion, BslashCompletion, ModuleCompletion, PropertyCompletion, FieldCompletion, PathCompletion, DictCompletion, completion_text, score import Base: show, istextmime import UUIDs: UUID, uuid4 import Dates: DateTime import Logging import REPL export @bind # This is not a struct to make it easier to pass these objects between processes. const MimedOutput = Tuple{Union{String,Vector{UInt8},Dict{Symbol,Any}},MIME} const ObjectID = typeof(objectid("hello computer")) const ObjectDimPair = Tuple{ObjectID,Int64} struct CachedMacroExpansion original_expr_hash::UInt64 expanded_expr::Expr expansion_duration::UInt64 has_pluto_hook_features::Bool did_mention_expansion_time::Bool expansion_logs::Vector{Any} end const cell_expanded_exprs = Dict{UUID,CachedMacroExpansion}() const supported_integration_features = Any[] abstract type SpecialPlutoExprValue end struct GiveMeCellID <: SpecialPlutoExprValue end struct GiveMeRerunCellFunction <: SpecialPlutoExprValue end struct GiveMeRegisterCleanupFunction <: SpecialPlutoExprValue end ### # WORKSPACE MANAGER ### """ `PlutoRunner.notebook_id[]` gives you the notebook ID used to identify a session. """ const notebook_id = Ref{UUID}(uuid4()) function revise_if_possible(m::Module) # Revise.jl support if isdefined(m, :Revise) && isdefined(m.Revise, :revise) && m.Revise.revise isa Function && isdefined(m.Revise, :revision_queue) && m.Revise.revision_queue isa AbstractSet if !isempty(m.Revise.revision_queue) # to avoid the sleep(0.01) in revise() m.Revise.revise() end end end "These expressions get evaluated inside every newly create module inside a `Workspace`." const workspace_preamble = [ :(using Main.PlutoRunner, Main.PlutoRunner.Markdown, Main.PlutoRunner.InteractiveUtils), :(show, showable, showerror, repr, string, print, println), # https://github.com/JuliaLang/julia/issues/18181 ] const PLUTO_INNER_MODULE_NAME = Symbol("#___this_pluto_module_name") const moduleworkspace_count = Ref(0) function increment_current_module()::Symbol id = (moduleworkspace_count[] += 1) new_workspace_name = Symbol("workspace#", id) Core.eval(Main, :( module $(new_workspace_name) $(workspace_preamble...) const $(PLUTO_INNER_MODULE_NAME) = $(new_workspace_name) end )) new_workspace_name end function wrap_dot(ref::GlobalRef) complete_mod_name = fullname(ref.mod) |> wrap_dot Expr(:(.), complete_mod_name, QuoteNode(ref.name)) end function wrap_dot(name) if length(name) == 1 name[1] else Expr(:(.), wrap_dot(name[1:end-1]), QuoteNode(name[end])) end end """ collect_and_eliminate_globalrefs!(ref::Union{GlobalRef,Expr}, mutable_ref_list::Vector{Pair{Symbol,Symbol}}=[]) Goes through an expression and removes all "global" references to workspace modules (e.g. Main.workspace#XXX). It collects the names that we replaced these references with, so that we can add assignments to these special names later. This is useful for us because when we macroexpand, the global refs will normally point to the module it was built in. We don't re-build the macro in every workspace, so we need to remove these refs manually in order to point to the new module instead. TODO? Don't remove the refs, but instead replace them with a new ref pointing to the new module? """ function collect_and_eliminate_globalrefs!(ref::GlobalRef, mutable_ref_list=[]) if is_pluto_workspace(ref.mod) new_name = gensym(ref.name) push!(mutable_ref_list, ref.name => new_name) new_name else ref end end function collect_and_eliminate_globalrefs!(expr::Expr, mutable_ref_list=[]) # Fix for .+ and .|> inside macros # https://github.com/fonsp/Pluto.jl/pull/1032#issuecomment-868819317 # I'm unsure if this was all necessary but 🤷‍♀️ # I take the :call with a GlobalRef to `.|>` or `.+` as args[1], # and then I convert it into a `:.` expr, which is basically (|>).(args...) # which is consistent for us to handle. if expr.head == :call && expr.args[1] isa GlobalRef && startswith(string(expr.args[1].name), ".") old_globalref = expr.args[1] non_broadcast_name = string(old_globalref.name)[begin+1:end] new_globalref = GlobalRef(old_globalref.mod, Symbol(non_broadcast_name)) new_expr = Expr(:., new_globalref, Expr(:tuple, expr.args[begin+1:end]...)) result = collect_and_eliminate_globalrefs!(new_expr, mutable_ref_list) return result else Expr(expr.head, map(arg -> collect_and_eliminate_globalrefs!(arg, mutable_ref_list), expr.args)...) end end collect_and_eliminate_globalrefs!(other, mutable_ref_list=[]) = other function globalref_to_workspaceref(expr) mutable_ref_list = Pair{Symbol, Symbol}[] new_expr = collect_and_eliminate_globalrefs!(expr, mutable_ref_list) Expr(:block, # Create new lines to assign to the replaced names of the global refs. # This way the expression explorer doesn't care (it just sees references to variables outside of the workspace), # and the variables don't get overwriten by local assigments to the same name (because we have special names). (mutable_ref_list .|> ref -> :(local $(ref[2])))..., map(mutable_ref_list) do ref # I can just do Expr(:isdefined, ref[1]) here, but it feels better to macroexpand, # because it's more obvious what's going on, and when they ever change the ast, we're safe :D macroexpand(Main, quote if @isdefined($(ref[1])) $(ref[2]) = $(ref[1]) end end) end..., new_expr, ) end replace_pluto_properties_in_expr(::GiveMeCellID; cell_id, kwargs...) = cell_id replace_pluto_properties_in_expr(::GiveMeRerunCellFunction; rerun_cell_function, kwargs...) = rerun_cell_function replace_pluto_properties_in_expr(::GiveMeRegisterCleanupFunction; register_cleanup_function, kwargs...) = register_cleanup_function replace_pluto_properties_in_expr(expr::Expr; kwargs...) = Expr(expr.head, map(arg -> replace_pluto_properties_in_expr(arg; kwargs...), expr.args)...) replace_pluto_properties_in_expr(m::Module; kwargs...) = if is_pluto_workspace(m) PLUTO_INNER_MODULE_NAME else m end replace_pluto_properties_in_expr(other; kwargs...) = other function replace_pluto_properties_in_expr(ln::LineNumberNode; cell_id, kwargs...) # See https://github.com/fonsp/Pluto.jl/pull/2241 file = string(ln.file) out = if endswith(file, string(cell_id)) # We already have the correct cell_id in this LineNumberNode ln else # We append to the LineNumberNode file #@#==# + cell_id LineNumberNode(ln.line, Symbol(file * "#@#==#$(cell_id)")) end return out end "Similar to [`replace_pluto_properties_in_expr`](@ref), but just checks for existance and doesn't check for [`GiveMeCellID`](@ref)" has_hook_style_pluto_properties_in_expr(::GiveMeRerunCellFunction) = true has_hook_style_pluto_properties_in_expr(::GiveMeRegisterCleanupFunction) = true has_hook_style_pluto_properties_in_expr(expr::Expr)::Bool = any(has_hook_style_pluto_properties_in_expr, expr.args) has_hook_style_pluto_properties_in_expr(other) = false function sanitize_expr(ref::GlobalRef) wrap_dot(ref) end function sanitize_expr(expr::Expr) Expr(expr.head, sanitize_expr.(expr.args)...) end sanitize_expr(linenumbernode::LineNumberNode) = linenumbernode sanitize_expr(quoted::QuoteNode) = QuoteNode(sanitize_expr(quoted.value)) sanitize_expr(bool::Bool) = bool sanitize_expr(symbol::Symbol) = symbol sanitize_expr(number::Union{Int,Int8,Float32,Float64}) = number # In all cases of more complex objects, we just don't send it. # It's not like the expression explorer will look into them at all. sanitize_expr(other) = nothing """ All code necessary for throwing errors when cells return. Right now it just throws an error from the position of the return, this is nice because you get to the line number of the return. However, now it is suddenly possibly to catch the return error... so we might want to actually return the error instead of throwing it, and then handle it in `run_expression` or something. """ module CantReturnInPluto struct CantReturnInPlutoException end function Base.showerror(io::IO, ::CantReturnInPlutoException) print(io, "Pluto: You can only use return inside a function.") end """ We do macro expansion now, so we can also check for `return` statements "statically". This method goes through an expression and replaces all `return` statements with `throw(CantReturnInPlutoException())` """ function replace_returns_with_error(expr::Expr)::Expr if expr.head == :return :(throw($(CantReturnInPlutoException()))) elseif expr.head == :quote Expr(:quote, replace_returns_with_error_in_interpolation(expr.args[1])) elseif Meta.isexpr(expr, :(=)) && expr.args[1] isa Expr && (expr.args[1].head == :call || expr.args[1].head == :where || (expr.args[1].head == :(::) && expr.args[1].args[1] isa Expr && expr.args[1].args[1].head == :call)) # f(x) = ... expr elseif expr.head == :function || expr.head == :macro || expr.head == :(->) expr else Expr(expr.head, map(arg -> replace_returns_with_error(arg), expr.args)...) end end replace_returns_with_error(other) = other "Go through a quoted expression and remove returns" function replace_returns_with_error_in_interpolation(expr::Expr) if expr.head == :$ Expr(:$, replace_returns_with_error_in_interpolation(expr.args[1])) else # We are still in a quote, so we do go deeper, but we keep ignoring everything except :$'s Expr(expr.head, map(arg -> replace_returns_with_error_in_interpolation(arg), expr.args)...) end end replace_returns_with_error_in_interpolation(ex) = ex end function try_macroexpand(mod::Module, notebook_id::UUID, cell_id::UUID, expr; capture_stdout::Bool=true) # Remove the precvious cached expansion, so when we error somewhere before we update, # the old one won't linger around and get run accidentally. pop!(cell_expanded_exprs, cell_id, nothing) # Remove toplevel block, as that screws with the computer and everything expr_not_toplevel = if Meta.isexpr(expr, (:toplevel, :block)) Expr(:block, expr.args...) else @warn "try_macroexpand expression not :toplevel or :block" expr Expr(:block, expr) end capture_logger = CaptureLogger(nothing, get_cell_logger(notebook_id, cell_id), Dict[]) expanded_expr, elapsed_ns = with_logger_and_io_to_logs(capture_logger; capture_stdout) do elapsed_ns = time_ns() expanded_expr = macroexpand(mod, expr_not_toplevel)::Expr elapsed_ns = time_ns() - elapsed_ns expanded_expr, elapsed_ns end logs = capture_logger.logs # Removes baked in references to the module this was macroexpanded in. # Fix for https://github.com/fonsp/Pluto.jl/issues/1112 expr_without_return = CantReturnInPluto.replace_returns_with_error(expanded_expr)::Expr expr_without_globalrefs = globalref_to_workspaceref(expr_without_return) has_pluto_hook_features = has_hook_style_pluto_properties_in_expr(expr_without_globalrefs) expr_to_save = replace_pluto_properties_in_expr(expr_without_globalrefs; cell_id, rerun_cell_function=() -> rerun_cell_from_notebook(cell_id), register_cleanup_function=(fn) -> UseEffectCleanups.register_cleanup(fn, cell_id), ) did_mention_expansion_time = false cell_expanded_exprs[cell_id] = CachedMacroExpansion( expr_hash(expr), expr_to_save, elapsed_ns, has_pluto_hook_features, did_mention_expansion_time, logs, ) return (sanitize_expr(expr_to_save), expr_hash(expr_to_save)) end function exported_names(mod::Module) @static if VERSION ≥ v"1.11.0-DEV.469" filter!(Base.Fix1(Base.isexported, mod), names(mod; all=true)) else names(mod) end end function get_module_names(workspace_module, module_ex::Expr) try Core.eval(workspace_module, Expr(:call, exported_names, module_ex)) |> Set{Symbol} catch Set{Symbol}() end end function collect_soft_definitions(workspace_module, modules::Set{Expr}) mapreduce(module_ex -> get_module_names(workspace_module, module_ex), union!, modules; init=Set{Symbol}()) end ### # EVALUATING NOTEBOOK CODE ### struct Computer f::Function expr_id::ObjectID input_globals::Vector{Symbol} output_globals::Vector{Symbol} end expr_hash(e::Expr) = objectid(e.head) + mapreduce(p -> objectid((p[1], expr_hash(p[2]))), +, enumerate(e.args); init=zero(ObjectID)) expr_hash(x) = objectid(x) const computers = Dict{UUID,Computer}() const computer_workspace = Main const cells_with_hook_functionality_active = Set{UUID}() "Registers a new computer for the cell, cleaning up the old one if there is one." function register_computer(expr::Expr, key::ObjectID, cell_id::UUID, input_globals::Vector{Symbol}, output_globals::Vector{Symbol}) @gensym result e = Expr(:function, Expr(:call, gensym(:function_wrapped_cell), input_globals...), Expr(:block, Expr(:(=), result, timed_expr(expr)), Expr(:tuple, result, Expr(:tuple, map(x -> :(@isdefined($(x)) ? $(x) : $(OutputNotDefined())), output_globals)...) ) )) f = Core.eval(computer_workspace, e) if haskey(computers, cell_id) delete_computer!(computers, cell_id) end computers[cell_id] = Computer(f, key, input_globals, output_globals) end function delete_computer!(computers::Dict{UUID,Computer}, cell_id::UUID) computer = pop!(computers, cell_id) UseEffectCleanups.trigger_cleanup(cell_id) Base.visit(Base.delete_method, methods(computer.f).mt) # Make the computer function uncallable end parse_cell_id(filename::Symbol) = filename |> string |> parse_cell_id parse_cell_id(filename::AbstractString) = match(r"#==#(.*)", filename).captures |> only |> UUID module UseEffectCleanups import UUIDs: UUID const cell_cleanup_functions = Dict{UUID,Set{Function}}() function register_cleanup(f::Function, cell_id::UUID) cleanup_functions = get!(cell_cleanup_functions, cell_id, Set{Function}()) push!(cleanup_functions, f) nothing end function trigger_cleanup(cell_id::UUID) for cleanup_func in get!(cell_cleanup_functions, cell_id, Set{Function}()) try cleanup_func() catch error @warn "Cleanup function gave an error" cell_id error stacktrace=stacktrace(catch_backtrace()) end end delete!(cell_cleanup_functions, cell_id) end end quote_if_needed(x) = x quote_if_needed(x::Union{Expr, Symbol, QuoteNode, LineNumberNode}) = QuoteNode(x) struct OutputNotDefined end function compute(m::Module, computer::Computer) # 1. get the referenced global variables # this might error if the global does not exist, which is exactly what we want input_global_values = Vector{Any}(undef, length(computer.input_globals)) for (i, s) in enumerate(computer.input_globals) input_global_values[i] = getfield(m, s) end # 2. run the function out = Base.invokelatest(computer.f, input_global_values...) result, output_global_values = out for (name, val) in zip(computer.output_globals, output_global_values) # Core.eval(m, Expr(:(=), name, quote_if_needed(val))) Core.eval(m, quote if $(quote_if_needed(val)) !== $(OutputNotDefined()) $(name) = $(quote_if_needed(val)) end end) end result end "Wrap `expr` inside a timing block." function timed_expr(expr::Expr)::Expr # @assert ExpressionExplorer.is_toplevel_expr(expr) @gensym result @gensym elapsed_ns # we don't use `quote ... end` here to avoid the LineNumberNodes that it adds (these would taint the stack trace). Expr(:block, :(local $elapsed_ns = time_ns()), :(local $result = $expr), :($elapsed_ns = time_ns() - $elapsed_ns), :(($result, $elapsed_ns)), ) end """ Run the expression or function inside a try ... catch block, and verify its "return proof". """ function run_inside_trycatch(m::Module, f::Union{Expr,Function})::Tuple{Any,Union{UInt64,Nothing}} return try if f isa Expr # We eval `f` in the global scope of the workspace module: Core.eval(m, f) else # f is a function f() end catch ex bt = stacktrace(catch_backtrace()) (CapturedException(ex, bt), nothing) end end add_runtimes(::Nothing, ::UInt64) = nothing add_runtimes(a::UInt64, b::UInt64) = a+b contains_macrocall(expr::Expr) = if expr.head == :macrocall true elseif expr.head == :module # Modules don't get expanded, sadly, so we don't touch it false else any(arg -> contains_macrocall(arg), expr.args) end contains_macrocall(other) = false """ Run the given expression in the current workspace module. If the third argument is `nothing`, then the expression will be `Core.eval`ed. The result and runtime are stored inside [`cell_results`](@ref) and [`cell_runtimes`](@ref). If the third argument is a `Tuple{Set{Symbol}, Set{Symbol}}` containing the referenced and assigned variables of the expression (computed by the ExpressionExplorer), then the expression will be **wrapped inside a function**, with the references as inputs, and the assignments as outputs. Instead of running the expression directly, Pluto will call this function, with the right globals as inputs. This function is memoized: running the same expression a second time will simply call the same generated function again. This is much faster than evaluating the expression, because the function only needs to be Julia-compiled once. See https://github.com/fonsp/Pluto.jl/pull/720 """ function run_expression( m::Module, expr::Any, notebook_id::UUID, cell_id::UUID, @nospecialize(function_wrapped_info::Union{Nothing,Tuple{Set{Symbol},Set{Symbol}}}=nothing), @nospecialize(forced_expr_id::Union{ObjectID,Nothing}=nothing); user_requested_run::Bool=true, capture_stdout::Bool=true, ) if user_requested_run # TODO Time elapsed? Possibly relays errors in cleanup function? UseEffectCleanups.trigger_cleanup(cell_id) # TODO Could also put explicit `try_macroexpand` here, to make clear that user_requested_run => fresh macro identity end old_currently_running_cell_id = currently_running_cell_id[] currently_running_cell_id[] = cell_id logger = get_cell_logger(notebook_id, cell_id) # reset published objects cell_published_objects[cell_id] = Dict{String,Any}() # reset registered bonds cell_registered_bond_names[cell_id] = Set{Symbol}() # reset JS links unregister_js_link(cell_id) # If the cell contains macro calls, we want those macro calls to preserve their identity, # so we macroexpand this earlier (during expression explorer stuff), and then we find it here. # NOTE Turns out sometimes there is no macroexpanded version even though the expression contains macro calls... # .... So I macroexpand when there is no cached version just to be sure 🤷‍♀️ # NOTE Errors during try_macroexpand will cause no expanded version to be stored. # .... This is fine, because it allows us to try again here and throw the error... # .... But ideally we wouldn't re-macroexpand and store the error the first time (TODO-ish) if !haskey(cell_expanded_exprs, cell_id) || cell_expanded_exprs[cell_id].original_expr_hash != expr_hash(expr) try try_macroexpand(m, notebook_id, cell_id, expr; capture_stdout) catch e result = CapturedException(e, stacktrace(catch_backtrace())) cell_results[cell_id], cell_runtimes[cell_id] = (result, nothing) return (result, nothing) end end # We can be sure there is a cached expression now, yay expanded_cache = cell_expanded_exprs[cell_id] original_expr = expr expr = expanded_cache.expanded_expr # Re-play logs from expansion cache for log in expanded_cache.expansion_logs (level, msg, _module, group, id, file, line, kwargs) = log Logging.handle_message(logger, level, msg, _module, group, id, file, line; kwargs...) end empty!(expanded_cache.expansion_logs) # We add the time it took to macroexpand to the time for the first call, # but we make sure we don't mention it on subsequent calls expansion_runtime = if expanded_cache.did_mention_expansion_time === false did_mention_expansion_time = true cell_expanded_exprs[cell_id] = CachedMacroExpansion( expanded_cache.original_expr_hash, expanded_cache.expanded_expr, expanded_cache.expansion_duration, expanded_cache.has_pluto_hook_features, did_mention_expansion_time, expanded_cache.expansion_logs, ) expanded_cache.expansion_duration else zero(UInt64) end if contains_macrocall(expr) @error "Expression contains a macrocall" expr throw("Expression still contains macro calls!!") end result, runtime = with_logger_and_io_to_logs(logger; capture_stdout) do # about 200ns + 3ms overhead if function_wrapped_info === nothing toplevel_expr = Expr(:toplevel, expr) wrapped = timed_expr(toplevel_expr) ans, runtime = run_inside_trycatch(m, wrapped) (ans, add_runtimes(runtime, expansion_runtime)) else expr_id = forced_expr_id !== nothing ? forced_expr_id : expr_hash(expr) local computer = get(computers, cell_id, nothing) if computer === nothing || computer.expr_id !== expr_id try computer = register_computer(expr, expr_id, cell_id, collect.(function_wrapped_info)...) catch e # @error "Failed to generate computer function" expr exception=(e,stacktrace(catch_backtrace())) return run_expression(m, original_expr, notebook_id, cell_id, nothing; user_requested_run=user_requested_run) end end # This check solves the problem of a cell like `false && variable_that_does_not_exist`. This should run without error, but will fail in our function-wrapping-magic because we get the value of `variable_that_does_not_exist` before calling the generated function. # The fix is to detect this situation and run the expression in the classical way. ans, runtime = if any(name -> !isdefined(m, name), computer.input_globals) # Do run_expression but with function_wrapped_info=nothing so it doesn't go in a Computer() # @warn "Got variables that don't exist, running outside of computer" not_existing=filter(name -> !isdefined(m, name), computer.input_globals) run_expression(m, original_expr, notebook_id, cell_id; user_requested_run) else run_inside_trycatch(m, () -> compute(m, computer)) end ans, add_runtimes(runtime, expansion_runtime) end end currently_running_cell_id[] = old_currently_running_cell_id if (result isa CapturedException) && (result.ex isa InterruptException) throw(result.ex) end cell_results[cell_id], cell_runtimes[cell_id] = result, runtime end precompile(run_expression, (Module, Expr, UUID, UUID, Nothing, Nothing)) # Channel to trigger implicits run const run_channel = Channel{UUID}(10) function rerun_cell_from_notebook(cell_id::UUID) # make sure only one of this cell_id is in the run channel # by emptying it and filling it again new_uuids = UUID[] while isready(run_channel) uuid = take!(run_channel) if uuid != cell_id push!(new_uuids, uuid) end end for uuid in new_uuids put!(run_channel, uuid) end put!(run_channel, cell_id) end ### # DELETING GLOBALS ### # This function checks whether the symbol provided to it represents a name of a memoized_cache variable from Memoize.jl, see https://github.com/fonsp/Pluto.jl/issues/2305 for more details is_memoized_cache(s::Symbol) = startswith(string(s), "##") && endswith(string(s), "_memoized_cache") function do_reimports(workspace_name, module_imports_to_move::Set{Expr}) for expr in module_imports_to_move try Core.eval(workspace_name, expr) catch e end # TODO catch specificallly end end """ Move some of the globals over from one workspace to another. This is how Pluto "deletes" globals - it doesn't, it just executes your new code in a new module where those globals are not defined. Notebook code does run in `Main` - it runs in workspace modules. Every time that you run cells, a new module is created, called `Main.workspace123` with `123` an increasing number. The trick boils down to two things: 1. When we create a new workspace module, we move over some of the global from the old workspace. (But not the ones that we want to 'delete'!) 2. If a function used to be defined, but now we want to delete it, then we go through the method table of that function and snoop out all methods that were defined by us, and not by another package. This is how we reverse extending external functions. For example, if you run a cell with `Base.sqrt(s::String) = "the square root of" * s`, and then delete that cell, then you can still call `sqrt(1)` but `sqrt("one")` will err. Cool right! """ function move_vars( old_workspace_name::Symbol, new_workspace_name::Symbol, vars_to_delete::Set{Symbol}, methods_to_delete::Set{Tuple{UUID,Tuple{Vararg{Symbol}}}}, module_imports_to_move::Set{Expr}, cells_to_macro_invalidate::Set{UUID}, cells_to_js_link_invalidate::Set{UUID}, keep_registered::Set{Symbol}, ) old_workspace = getfield(Main, old_workspace_name) new_workspace = getfield(Main, new_workspace_name) do_reimports(new_workspace, module_imports_to_move) for cell_id in cells_to_macro_invalidate delete!(cell_expanded_exprs, cell_id) end foreach(unregister_js_link, cells_to_js_link_invalidate) # TODO: delete Core.eval(new_workspace, :(import ..($(old_workspace_name)))) old_names = names(old_workspace, all=true, imported=true) funcs_with_no_methods_left = filter(methods_to_delete) do f !try_delete_toplevel_methods(old_workspace, f) end name_symbols_of_funcs_with_no_methods_left = last.(last.(funcs_with_no_methods_left)) for symbol in old_names if (symbol ∈ vars_to_delete) || (symbol ∈ name_symbols_of_funcs_with_no_methods_left) # var will be redefined - unreference the value so that GC can snoop it if haskey(registered_bond_elements, symbol) && symbol ∉ keep_registered delete!(registered_bond_elements, symbol) end # free memory for other variables # & delete methods created in the old module: # for example, the old module might extend an imported function: # `import Base: show; show(io::IO, x::Flower) = print(io, "🌷")` # when you delete/change this cell, you want this extension to disappear. if isdefined(old_workspace, symbol) # try_delete_toplevel_methods(old_workspace, symbol) try # We are clearing this variable from the notebook, so we need to find it's root # If its root is "controlled" by Pluto's workspace system (and is not a package module for example), # we are just clearing out the definition in the old_module, besides giving an error # (so that's what that `catch; end` is for) # will not actually free it from Julia, the older module will still have a reference. module_to_remove_from = which(old_workspace, symbol) if is_pluto_controlled(module_to_remove_from) && !isconst(module_to_remove_from, symbol) Core.eval(module_to_remove_from, :($(symbol) = nothing)) end catch; end # sometimes impossible, eg. when $symbol was constant end else # var will not be redefined in the new workspace, move it over if !(symbol == :eval || symbol == :include || (string(symbol)[1] == '#' && !is_memoized_cache(symbol)) || startswith(string(symbol), "workspace#")) try getfield(old_workspace, symbol) # Expose the variable in the scope of `new_workspace` Core.eval(new_workspace, :(import ..($(old_workspace_name)).$(symbol))) catch ex if !(ex isa UndefVarError) @warn "Failed to move variable $(symbol) to new workspace:" showerror(original_stderr, ex, stacktrace(catch_backtrace())) end end end end end revise_if_possible(new_workspace) end "Return whether the `method` was defined inside this notebook, and not in external code." isfromcell(method::Method, cell_id::UUID) = endswith(String(method.file), string(cell_id)) """ delete_method_doc(m::Method) Tries to delete the documentation for this method, this is used when methods are removed. """ function delete_method_doc(m::Method) binding = Docs.Binding(m.module, m.name) meta = Docs.meta(m.module) if haskey(meta, binding) method_sig = Tuple{m.sig.parameters[2:end]...} multidoc = meta[binding] filter!(multidoc.order) do msig if method_sig == msig pop!(multidoc.docs, msig) false else true end end end end if VERSION < v"1.7.0-0" @eval macro atomic(ex) esc(ex) end end """ Delete all methods of `f` that were defined in this notebook, and leave the ones defined in other packages, base, etc. ✂ Return whether the function has any methods left after deletion. """ function delete_toplevel_methods(f::Function, cell_id::UUID)::Bool # we can delete methods of functions! # instead of deleting all methods, we only delete methods that were defined in this notebook. This is necessary when the notebook code extends a function from remote code methods_table = typeof(f).name.mt deleted_sigs = Set{Type}() Base.visit(methods_table) do method # iterates through all methods of `f`, including overridden ones if isfromcell(method, cell_id) && method.deleted_world == alive_world_val Base.delete_method(method) delete_method_doc(method) push!(deleted_sigs, method.sig) end end if VERSION < v"1.12.0-0" # not necessary in Julia after https://github.com/JuliaLang/julia/pull/53415 💛 # if `f` is an extension to an external function, and we defined a method that overrides a method, for example, # we define `Base.isodd(n::Integer) = rand(Bool)`, which overrides the existing method `Base.isodd(n::Integer)` # calling `Base.delete_method` on this method won't bring back the old method, because our new method still exists in the method table, and it has a world age which is newer than the original. (our method has a deleted_world value set, which disables it) # # To solve this, we iterate again, and _re-enable any methods that were hidden in this way_, by adding them again to the method table with an even newer `primary_world`. if !isempty(deleted_sigs) to_insert = Method[] Base.visit(methods_table) do method if !isfromcell(method, cell_id) && method.sig ∈ deleted_sigs push!(to_insert, method) end end # separate loop to avoid visiting the recently added method for method in Iterators.reverse(to_insert) if VERSION >= v"1.11.0-0" @atomic method.primary_world = one(typeof(alive_world_val)) # `1` will tell Julia to increment the world counter and set it as this function's world @atomic method.deleted_world = alive_world_val # set the `deleted_world` property back to the 'alive' value (for Julia v1.6 and up) else method.primary_world = one(typeof(alive_world_val)) method.deleted_world = alive_world_val end ccall(:jl_method_table_insert, Cvoid, (Any, Any, Ptr{Cvoid}), methods_table, method, C_NULL) # i dont like doing this either! end end end return !isempty(methods(f).ms) end # function try_delete_toplevel_methods(workspace::Module, name::Symbol) # try_delete_toplevel_methods(workspace, [name]) # end function try_delete_toplevel_methods(workspace::Module, (cell_id, name_parts)::Tuple{UUID,Tuple{Vararg{Symbol}}})::Bool try val = workspace for name in name_parts val = getfield(val, name) end try (val isa Function) && delete_toplevel_methods(val, cell_id) catch ex @warn "Failed to delete methods for $(name_parts)" showerror(original_stderr, ex, stacktrace(catch_backtrace())) false end catch false end end const alive_world_val = methods(Base.sqrt).ms[1].deleted_world # typemax(UInt) in Julia v1.3, Int(-1) in Julia 1.0 ### # FORMATTING ### # TODO: clear key when a cell is deleted furever const cell_results = Dict{UUID,Any}() const cell_runtimes = Dict{UUID,Union{Nothing,UInt64}}() const cell_published_objects = Dict{UUID,Dict{String,Any}}() const cell_registered_bond_names = Dict{UUID,Set{Symbol}}() const tree_display_limit = 30 const tree_display_limit_increase = 40 const table_row_display_limit = 10 const table_row_display_limit_increase = 60 const table_column_display_limit = 8 const table_column_display_limit_increase = 30 const tree_display_extra_items = Dict{UUID,Dict{ObjectDimPair,Int64}}() # This is not a struct to make it easier to pass these objects between processes. const FormattedCellResult = NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects, :has_pluto_hook_features),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any},Bool}} function formatted_result_of( notebook_id::UUID, cell_id::UUID, ends_with_semicolon::Bool, known_published_objects::Vector{String}=String[], showmore::Union{ObjectDimPair,Nothing}=nothing, workspace::Module=Main; capture_stdout::Bool=true, )::FormattedCellResult load_integrations_if_needed() currently_running_cell_id[] = cell_id extra_items = if showmore === nothing tree_display_extra_items[cell_id] = Dict{ObjectDimPair,Int64}() else old = get!(() -> Dict{ObjectDimPair,Int64}(), tree_display_extra_items, cell_id) old[showmore] = get(old, showmore, 0) + 1 old end has_pluto_hook_features = haskey(cell_expanded_exprs, cell_id) && cell_expanded_exprs[cell_id].has_pluto_hook_features ans = cell_results[cell_id] errored = ans isa CapturedException output_formatted = if (!ends_with_semicolon || errored) with_logger_and_io_to_logs(get_cell_logger(notebook_id, cell_id); capture_stdout) do format_output(ans; context=IOContext( default_iocontext, :extra_items=>extra_items, :module => workspace, :pluto_notebook_id => notebook_id, :pluto_cell_id => cell_id, )) end else ("", MIME"text/plain"()) end published_objects = get(cell_published_objects, cell_id, Dict{String,Any}()) for k in known_published_objects if haskey(published_objects, k) published_objects[k] = nothing end end return (; output_formatted, errored, interrupted = false, process_exited = false, runtime = get(cell_runtimes, cell_id, nothing), published_objects, has_pluto_hook_features, ) end "Because even showerror can error... 👀" function try_showerror(io::IO, e, args...) try showerror(io, e, args...) catch show_ex print(io, "\nFailed to show error:\n\n") try_showerror(io, show_ex, stacktrace(catch_backtrace())) end end # We add a method for the Markdown -> HTML conversion that takes a LaTeX chunk from the Markdown tree and adds our custom span function htmlinline(io::IO, x::LaTeX) withtag(io, :span, :class => "tex") do print(io, '$') htmlesc(io, x.formula) print(io, '$') end end # this one for block equations: (double $$) function html(io::IO, x::LaTeX) withtag(io, :p, :class => "tex") do print(io, '$', '$') htmlesc(io, x.formula) print(io, '$', '$') end end # because i like that Base.IOContext(io::IOContext, ::Nothing) = io "The `IOContext` used for converting arbitrary objects to pretty strings." const default_iocontext = IOContext(devnull, :color => false, :limit => true, :displaysize => (18, 88), :is_pluto => true, :pluto_supported_integration_features => supported_integration_features, :pluto_published_to_js => (io, x) -> core_published_to_js(io, x), :pluto_with_js_link => (io, callback, on_cancellation) -> core_with_js_link(io, callback, on_cancellation), ) const default_stdout_iocontext = IOContext(devnull, :color => true, :limit => true, :displaysize => (18, 75), :is_pluto => false, ) const imagemimes = MIME[MIME"image/svg+xml"(), MIME"image/png"(), MIME"image/jpg"(), MIME"image/jpeg"(), MIME"image/bmp"(), MIME"image/gif"()] # in descending order of coolness # text/plain always matches - almost always """ The MIMEs that Pluto supports, in order of how much I like them. `text/plain` should always match - the difference between `show(::IO, ::MIME"text/plain", x)` and `show(::IO, x)` is an unsolved mystery. """ const allmimes = MIME[MIME"application/vnd.pluto.table+object"(); MIME"application/vnd.pluto.divelement+object"(); MIME"text/html"(); imagemimes; MIME"application/vnd.pluto.tree+object"(); MIME"text/latex"(); MIME"text/plain"()] """ Format `val` using the richest possible output, return formatted string and used MIME type. See [`allmimes`](@ref) for the ordered list of supported MIME types. """ function format_output_default(@nospecialize(val), @nospecialize(context=default_iocontext))::MimedOutput try io_sprinted, (value, mime) = show_richest_withreturned(context, val) if value === nothing if mime ∈ imagemimes (io_sprinted, mime) else (String(io_sprinted)::String, mime) end else (value, mime) end catch ex title = ErrorException("Failed to show value: \n" * sprint(try_showerror, ex)) bt = stacktrace(catch_backtrace()) format_output(CapturedException(title, bt)) end end format_output(@nospecialize(x); context=default_iocontext) = format_output_default(x, context) format_output(::Nothing; context=default_iocontext) = ("", MIME"text/plain"()) "Downstream packages can set this to false to obtain unprettified stack traces." const PRETTY_STACKTRACES = Ref(true) # @codemirror/lint has only three levels function convert_julia_syntax_level(level) level == :error ? "error" : level == :warning ? "warning" : "info" end """ map_byte_range_to_utf16_codepoints(s::String, start_byte::Int, end_byte::Int)::Tuple{Int,Int} Taken from `Base.transcode(::Type{UInt16}, src::Vector{UInt8})` but without line constraints. It also does not support invalid UTF-8 encoding which `String` should never be anyway. This maps the given raw byte range `(start_byte, end_byte)` range to UTF-16 codepoints indices. The resulting range can then be used by code-mirror on the frontend, quoting from the code-mirror docs: > Character positions are counted from zero, and count each line break and UTF-16 code unit as one unit. Examples: ```julia 123 vv julia> map_byte_range_to_utf16_codepoints("abc", 2, 3) (2, 3) 1122 v v julia> map_byte_range_to_utf16_codepoints("🍕🍕", 1, 8) (1, 4) 11233 v v julia> map_byte_range_to_utf16_codepoints("🍕c🍕", 1, 5) (1, 3) ``` """ function map_byte_range_to_utf16_codepoints(s, start_byte, end_byte) invalid_utf8() = error("invalid UTF-8 string") codeunit(s) == UInt8 || invalid_utf8() i, n = 1, ncodeunits(s) u16 = 0 from, to = -1, -1 a = codeunit(s, 1) while true if i == start_byte from = u16 end if i == end_byte to = u16 break end if i < n && -64 <= a % Int8 <= -12 # multi-byte character i += 1 b = codeunit(s, i) if -64 <= (b % Int8) || a == 0xf4 && 0x8f < b # invalid UTF-8 (non-continuation of too-high code point) invalid_utf8() elseif a < 0xe0 # 2-byte UTF-8 if i == start_byte from = u16 end if i == end_byte to = u16 break end elseif i < n # 3/4-byte character i += 1 c = codeunit(s, i) if -64 <= (c % Int8) # invalid UTF-8 (non-continuation) invalid_utf8() elseif a < 0xf0 # 3-byte UTF-8 if i == start_byte from = u16 end if i == end_byte to = u16 break end elseif i < n i += 1 d = codeunit(s, i) if -64 <= (d % Int8) # invalid UTF-8 (non-continuation) invalid_utf8() elseif a == 0xf0 && b < 0x90 # overlong encoding invalid_utf8() else # 4-byte UTF-8 && 2 codeunits UTF-16 u16 += 1 if i == start_byte from = u16 end if i == end_byte to = u16 break end end else # too short invalid_utf8() end else # too short invalid_utf8() end else # ASCII or invalid UTF-8 (continuation byte or too-high code point) end u16 += 1 if i >= n break end i += 1 a = codeunit(s, i) end if from == -1 from = u16 end if to == -1 to = u16 end return (from, to) end function convert_diagnostic_to_dict(source, diag) code = source.code # JuliaSyntax uses `last_byte < first_byte` to signal an empty range. # https://github.com/JuliaLang/JuliaSyntax.jl/blob/97e2825c68e770a3f56f0ec247deda1a8588070c/src/diagnostics.jl#L67-L75 # it references the byte range as such: `source[first_byte:last_byte]` whereas codemirror # is non inclusive, therefore we move the `last_byte` to the next valid character in the string, # an empty range then becomes `from == to`, also JuliaSyntax is one based whereas code-mirror is zero-based # but this is handled in `map_byte_range_to_utf16_codepoints` with `u16 = 0` initially. first_byte = min(diag.first_byte, lastindex(code) + 1) last_byte = min(nextind(code, diag.last_byte), lastindex(code) + 1) from, to = map_byte_range_to_utf16_codepoints(code, first_byte, last_byte) Dict(:from => from, :to => to, :message => diag.message, :source => "JuliaSyntax.jl", :line => first(Base.JuliaSyntax.source_location(source, diag.first_byte)), :severity => convert_julia_syntax_level(diag.level)) end function convert_parse_error_to_dict(ex) Dict( :source => ex.source.code, :diagnostics => [ convert_diagnostic_to_dict(ex.source, diag) for diag in ex.diagnostics ] ) end """ *Internal* wrapper for syntax errors which have diagnostics. Thrown through PlutoRunner.throw_syntax_error """ struct PrettySyntaxError <: Exception ex::Any end function throw_syntax_error(@nospecialize(syntax_err)) syntax_err isa String && (syntax_err = "syntax: $syntax_err") syntax_err isa Exception || (syntax_err = ErrorException(syntax_err)) if has_julia_syntax && syntax_err isa Base.Meta.ParseError && syntax_err.detail isa Base.JuliaSyntax.ParseError syntax_err = PrettySyntaxError(syntax_err) end throw(syntax_err) end const has_julia_syntax = isdefined(Base, :JuliaSyntax) && fieldcount(Base.Meta.ParseError) == 2 function frame_is_from_plutorunner(frame::Base.StackTraces.StackFrame) if frame.linfo isa Core.MethodInstance frame.linfo.def.module === PlutoRunner else endswith(String(frame.file), "PlutoRunner.jl") end end frame_is_from_usercode(frame::Base.StackTraces.StackFrame) = occursin("#==#", String(frame.file)) function frame_url(frame::Base.StackTraces.StackFrame) if frame.linfo isa Core.MethodInstance Base.url(frame.linfo.def) elseif frame.linfo isa Method Base.url(frame.linfo) else nothing end end function format_output(val::CapturedException; context=default_iocontext) if has_julia_syntax && val.ex isa PrettySyntaxError dict = convert_parse_error_to_dict(val.ex.ex.detail) return dict, MIME"application/vnd.pluto.parseerror+object"() end stacktrace = if PRETTY_STACKTRACES[] ## We hide the part of the stacktrace that belongs to Pluto's evalling of user code. stack = [s for (s, _) in val.processed_bt] # function_wrap_index = findfirst(f -> occursin("function_wrapped_cell", String(f.func)), stack) function_wrap_index = findlast(frame_is_from_usercode, stack) internal_index = findfirst(frame_is_from_plutorunner, stack) limit = if function_wrap_index !== nothing function_wrap_index elseif internal_index !== nothing internal_index - 1 else nothing end stack_relevant = stack[1:something(limit, end)] pretty = map(stack_relevant) do s Dict( :call => pretty_stackcall(s, s.linfo), :inlined => s.inlined, :from_c => s.from_c, :file => basename(String(s.file)), :path => String(s.file), :line => s.line, :url => frame_url(s), :linfo_type => string(typeof(s.linfo)), ) end else val end Dict{Symbol,Any}(:msg => sprint(try_showerror, val.ex), :stacktrace => stacktrace), MIME"application/vnd.pluto.stacktrace+object"() end function format_output(binding::Base.Docs.Binding; context=default_iocontext) try ("""
$(binding.var) $(repr(MIME"text/html"(), Base.Docs.doc(binding)))
""", MIME"text/html"()) catch e @warn "Failed to pretty-print binding" exception=(e, catch_backtrace()) repr(binding, MIME"text/plain"()) end end # from the Julia source code: function pretty_stackcall(frame::Base.StackFrame, linfo::Nothing)::String if frame.func isa Symbol if occursin("function_wrapped_cell", String(frame.func)) "top-level scope" else String(frame.func) end else repr(frame.func) end end function pretty_stackcall(frame::Base.StackFrame, linfo::Core.CodeInfo) "top-level scope" end function pretty_stackcall(frame::Base.StackFrame, linfo::Core.MethodInstance) if linfo.def isa Method @static if isdefined(Base.StackTraces, :show_spec_linfo) && hasmethod(Base.StackTraces.show_spec_linfo, Tuple{IO, Base.StackFrame}) sprint(Base.StackTraces.show_spec_linfo, frame; context=:backtrace => true) else split(string(frame), " at ") |> first end else sprint(Base.show, linfo) end end function pretty_stackcall(frame::Base.StackFrame, linfo::Method) sprint(Base.show_tuple_as_call, linfo.name, linfo.sig) end function pretty_stackcall(frame::Base.StackFrame, linfo::Module) sprint(Base.show, linfo) end "Return a `(String, Any)` tuple containing function output as the second entry." function show_richest_withreturned(context::IOContext, @nospecialize(args)) buffer = IOBuffer(; sizehint=0) val = show_richest(IOContext(buffer, context), args) return (take!(buffer), val) end "Super important thing don't change." struct 🥔 end const struct_showmethod = which(show, (IO, 🥔)) const struct_showmethod_mime = which(show, (IO, MIME"text/plain", 🥔)) function use_tree_viewer_for_struct(@nospecialize(x::T))::Bool where T # types that have no specialized show methods (their fallback is text/plain) are displayed using Pluto's interactive tree viewer. # this is how we check whether this display method is appropriate: isstruct = try T isa DataType && # there are two ways to override the plaintext show method: which(show, (IO, MIME"text/plain", T)) === struct_showmethod_mime && which(show, (IO, T)) === struct_showmethod catch false end isstruct && let # from julia source code, dont know why nf = nfields(x) nb = sizeof(x) nf != 0 || nb == 0 end end """ is_mime_enabled(::MIME) -> Bool Return whether the argument's mimetype is enabled. This defaults to `true`, but additional dispatches can be set to `false` by downstream packages. """ is_mime_enabled(::MIME) = true "Return the first mimetype in `allmimes` which can show `x`." function mimetype(x) # ugly code to fix an ugly performance problem for m in allmimes if pluto_showable(m, x) && is_mime_enabled(m) return m end end end """ Like two-argument `Base.show`, except: 1. the richest MIME type available to Pluto will be used 2. the used MIME type is returned as second element 3. if the first returned element is `nothing`, then we wrote our data to `io`. If it is something else (a Dict), then that object will be the cell's output, instead of the buffered io stream. This allows us to output rich objects to the frontend that are not necessarily strings or byte streams """ function show_richest(io::IO, @nospecialize(x))::Tuple{<:Any,MIME} mime = mimetype(x) if mime isa MIME"text/plain" && is_mime_enabled(MIME"application/vnd.pluto.tree+object"()) && use_tree_viewer_for_struct(x) tree_data(x, io), MIME"application/vnd.pluto.tree+object"() elseif mime isa MIME"application/vnd.pluto.tree+object" try tree_data(x, IOContext(io, :compact => true)), mime catch show(io, MIME"text/plain"(), x) nothing, MIME"text/plain"() end elseif mime isa MIME"application/vnd.pluto.table+object" try table_data(x, IOContext(io, :compact => true)), mime catch show(io, MIME"text/plain"(), x) nothing, MIME"text/plain"() end elseif mime isa MIME"application/vnd.pluto.divelement+object" tree_data(x, io), mime elseif mime ∈ imagemimes show(io, mime, x) nothing, mime elseif mime isa MIME"text/latex" # Some reprs include $ at the start and end. # We strip those, since Markdown.LaTeX should contain the math content. # (It will be rendered by MathJax, which is math-first, not text-first.) texed = repr(mime, x) Markdown.html(io, Markdown.LaTeX(strip(texed, ('$', '\n', ' ')))) nothing, MIME"text/html"() else # the classic: show(io, mime, x) nothing, mime end end # we write our own function instead of extending Base.showable with our new MIME because: # we need the method Base.showable(::MIME"asdfasdf", ::Any) = Tables.rowaccess(x) # but overload ::MIME{"asdf"}, ::Any will cause ambiguity errors in other packages that write a method like: # Base.showable(m::MIME, x::Plots.Plot) # because MIME is less specific than MIME"asdff", but Plots.PLot is more specific than Any. pluto_showable(m::MIME, @nospecialize(x))::Bool = Base.invokelatest(showable, m, x) ### # TREE VIEWER ### # We invent our own MIME _because we can_ but don't use it somewhere else because it might change :) pluto_showable(::MIME"application/vnd.pluto.tree+object", x::AbstractVector{<:Any}) = try eltype(eachindex(x)) === Int; catch; false; end pluto_showable(::MIME"application/vnd.pluto.tree+object", ::AbstractSet{<:Any}) = true pluto_showable(::MIME"application/vnd.pluto.tree+object", ::AbstractDict{<:Any,<:Any}) = true pluto_showable(::MIME"application/vnd.pluto.tree+object", ::Tuple) = true pluto_showable(::MIME"application/vnd.pluto.tree+object", ::NamedTuple) = true pluto_showable(::MIME"application/vnd.pluto.tree+object", ::Pair) = true pluto_showable(::MIME"application/vnd.pluto.tree+object", ::AbstractRange) = false pluto_showable(::MIME"application/vnd.pluto.tree+object", ::Any) = false # in the next functions you see a `context` argument # this is really only used for the circular reference tracking const Context = IOContext{IOBuffer} function tree_data_array_elements(@nospecialize(x::AbstractVector{<:Any}), indices::AbstractVector{I}, context::Context) where {I<:Integer} Tuple{I,Any}[ if isassigned(x, i) i, format_output_default(x[i], context) else i, format_output_default(Text(Base.undef_ref_str), context) end for i in indices ] |> collect end precompile(tree_data_array_elements, (Vector{Any}, Vector{Int}, Context)) function array_prefix(@nospecialize(x::Vector{<:Any})) string(eltype(x))::String end function array_prefix(@nospecialize(x)) original = sprint(Base.showarg, x, false; context=:limit => true) string(lstrip(original, ':'), ": ")::String end function get_my_display_limit(@nospecialize(x), dim::Integer, depth::Integer, context::Context, a::Integer, b::Integer)::Int # needs to be system-dependent Int because it is used as array index let if depth < 3 a ÷ (1 + 2 * depth) else 0 end end + let d = get(context, :extra_items, nothing) if d === nothing 0 else b * get(d, (objectid(x), dim), 0) end end end objectid2str(@nospecialize(x)) = string(objectid(x); base=16)::String function circular(@nospecialize(x)) return Dict{Symbol,Any}( :objectid => objectid2str(x), :type => :circular ) end function tree_data(@nospecialize(x::AbstractSet{<:Any}), context::Context) if Base.show_circular(context, x) return circular(x) else depth = get(context, :tree_viewer_depth, 0) recur_io = IOContext(context, Pair{Symbol,Any}(:SHOWN_SET, x), Pair{Symbol,Any}(:tree_viewer_depth, depth + 1)) my_limit = get_my_display_limit(x, 1, depth, context, tree_display_limit, tree_display_limit_increase) L = min(my_limit+1, length(x)) elements = Vector{Any}(undef, L) index = 1 for value in x if index <= my_limit elements[index] = (index, format_output_default(value, recur_io)) else elements[index] = "more" break end index += 1 end Dict{Symbol,Any}( :prefix => string(typeof(x)), :prefix_short => string(typeof(x) |> trynameof), :objectid => objectid2str(x), :type => :Set, :elements => elements ) end end function tree_data(@nospecialize(x::AbstractVector{<:Any}), context::Context) if Base.show_circular(context, x) return circular(x) else depth = get(context, :tree_viewer_depth, 0)::Int recur_io = IOContext(context, Pair{Symbol,Any}(:SHOWN_SET, x), Pair{Symbol,Any}(:tree_viewer_depth, depth + 1)) indices = eachindex(x) my_limit = get_my_display_limit(x, 1, depth, context, tree_display_limit, tree_display_limit_increase) # additional couple of elements so that we don't cut off 1 or 2 itmes - that's silly elements = if length(x) <= ((my_limit * 6) ÷ 5) tree_data_array_elements(x, indices, recur_io) else firsti = firstindex(x) from_end = my_limit > 20 ? 10 : my_limit > 1 ? 1 : 0 Any[ tree_data_array_elements(x, indices[firsti:firsti-1+my_limit-from_end], recur_io); "more"; tree_data_array_elements(x, indices[end+1-from_end:end], recur_io) ] end prefix = array_prefix(x) Dict{Symbol,Any}( :prefix => prefix, :prefix_short => x isa Vector ? "" : prefix, # if not abstract :objectid => objectid2str(x), :type => :Array, :elements => elements ) end end function tree_data(@nospecialize(x::Tuple), context::Context) depth = get(context, :tree_viewer_depth, 0) recur_io = IOContext(context, Pair{Symbol,Any}(:tree_viewer_depth, depth + 1)) elements = Tuple[] for val in x out = format_output_default(val, recur_io) push!(elements, out) end Dict{Symbol,Any}( :objectid => objectid2str(x), :type => :Tuple, :elements => collect(enumerate(elements)), ) end function tree_data(@nospecialize(x::AbstractDict{<:Any,<:Any}), context::Context) if Base.show_circular(context, x) return circular(x) else depth = get(context, :tree_viewer_depth, 0) recur_io = IOContext(context, Pair{Symbol,Any}(:SHOWN_SET, x), Pair{Symbol,Any}(:tree_viewer_depth, depth + 1)) elements = [] my_limit = get_my_display_limit(x, 1, depth, context, tree_display_limit, tree_display_limit_increase) row_index = 1 for pair in x k, v = pair if row_index <= my_limit push!(elements, (format_output_default(k, recur_io), format_output_default(v, recur_io))) else push!(elements, "more") break end row_index += 1 end Dict{Symbol,Any}( :prefix => string(typeof(x)), :prefix_short => string(typeof(x) |> trynameof), :objectid => objectid2str(x), :type => :Dict, :elements => elements ) end end function tree_data_nt_row(@nospecialize(pair::Tuple), context::Context) # this is an entry of a NamedTuple, the first element of the Tuple is a Symbol, which we want to print as `x` instead of `:x` k, element = pair string(k), format_output_default(element, context) end function tree_data(@nospecialize(x::NamedTuple), context::Context) depth = get(context, :tree_viewer_depth, 0) recur_io = IOContext(context, Pair{Symbol,Any}(:tree_viewer_depth, depth + 1)) elements = Tuple[] for key in eachindex(x) val = x[key] data = tree_data_nt_row((key, val), recur_io) push!(elements, data) end Dict{Symbol,Any}( :objectid => objectid2str(x), :type => :NamedTuple, :elements => elements ) end function tree_data(@nospecialize(x::Pair), context::Context) k, v = x Dict{Symbol,Any}( :objectid => objectid2str(x), :type => :Pair, :key_value => (format_output_default(k, context), format_output_default(v, context)), ) end # Based on Julia source code but without writing to IO function tree_data(@nospecialize(x::Any), context::Context) if Base.show_circular(context, x) return circular(x) else depth = get(context, :tree_viewer_depth, 0) recur_io = IOContext(context, Pair{Symbol,Any}(:SHOWN_SET, x), Pair{Symbol,Any}(:typeinfo, Any), Pair{Symbol,Any}(:tree_viewer_depth, depth + 1), ) t = typeof(x) nf = nfields(x) nb = sizeof(x) elements = Any[ let f = fieldname(t, i) if !isdefined(x, f) Base.undef_ref_str f, format_output_default(Text(Base.undef_ref_str), recur_io) else f, format_output_default(getfield(x, i), recur_io) end end for i in 1:nf ] Dict{Symbol,Any}( :prefix => repr(t; context), :prefix_short => string(trynameof(t)), :objectid => objectid2str(x), :type => :struct, :elements => elements, ) end end function trynameof(::Type{Union{T,Missing}}) where T name = trynameof(T) return name === Symbol() ? name : Symbol(name, "?") end trynameof(x::DataType) = nameof(x) trynameof(x::Any) = Symbol() ### # TABLE VIEWER ## Base.@kwdef struct Integration id::Base.PkgId code::Expr loaded::Ref{Bool}=Ref(false) end # We have a super cool viewer for objects that are a Tables.jl table. To avoid version conflicts, we only load this code after the user (indirectly) loaded the package Tables.jl. # This is similar to how Requires.jl works, except we don't use a callback, we just check every time. const integrations = Integration[ Integration( id = Base.PkgId(Base.UUID(reinterpret(UInt128, codeunits("Paul Berg Berlin")) |> first), "AbstractPlutoDingetjes"), code = quote @assert v"1.0.0" <= AbstractPlutoDingetjes.MY_VERSION < v"2.0.0" supported!(xs...) = append!(supported_integration_features, xs) # don't need feature checks for these because they existed in every version of AbstractPlutoDingetjes: supported!( AbstractPlutoDingetjes, AbstractPlutoDingetjes.Bonds, AbstractPlutoDingetjes.Bonds.initial_value, AbstractPlutoDingetjes.Bonds.transform_value, AbstractPlutoDingetjes.Bonds.possible_values, ) initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value possible_bond_values_ref[] = AbstractPlutoDingetjes.Bonds.possible_values # feature checks because these were added in a later release of AbstractPlutoDingetjes if isdefined(AbstractPlutoDingetjes, :Display) supported!(AbstractPlutoDingetjes.Display) if isdefined(AbstractPlutoDingetjes.Display, :published_to_js) supported!(AbstractPlutoDingetjes.Display.published_to_js) end if isdefined(AbstractPlutoDingetjes.Display, :with_js_link) supported!(AbstractPlutoDingetjes.Display.with_js_link) end end end, ), Integration( id = Base.PkgId(UUID("0c5d862f-8b57-4792-8d23-62f2024744c7"), "Symbolics"), code = quote pluto_showable(::MIME"application/vnd.pluto.tree+object", ::Symbolics.Arr) = false end, ), Integration( id = Base.PkgId(UUID("bd369af6-aec1-5ad0-b16a-f7cc5008161c"), "Tables"), code = quote function maptruncated(f::Function, xs, filler, limit; truncate=true) if truncate result = Any[ # not xs[1:limit] because of https://github.com/JuliaLang/julia/issues/38364 f(xs[i]) for i in Iterators.take(eachindex(xs), limit) ] push!(result, filler) result else Any[f(x) for x in xs] end end function table_data(x::Any, io::Context) rows = Tables.rows(x) my_row_limit = get_my_display_limit(x, 1, 0, io, table_row_display_limit, table_row_display_limit_increase) # TODO: the commented line adds support for lazy loading columns, but it uses the same extra_items counter as the rows. So clicking More Rows will also give more columns, and vice versa, which isn't ideal. To fix, maybe use (objectid,dimension) as index instead of (objectid)? my_column_limit = get_my_display_limit(x, 2, 0, io, table_column_display_limit, table_column_display_limit_increase) # my_column_limit = table_column_display_limit # additional 5 so that we don't cut off 1 or 2 itmes - that's silly truncate_rows = my_row_limit + 5 < length(rows) truncate_columns = if isempty(rows) false else my_column_limit + 5 < length(first(rows)) end row_data_for(row) = maptruncated(row, "more", my_column_limit; truncate=truncate_columns) do el format_output_default(el, io) end # ugliest code in Pluto: # not a map(row) because it needs to be a Vector # not enumerate(rows) because of some silliness # not rows[i] because `getindex` is not guaranteed to exist L = truncate_rows ? my_row_limit : length(rows) row_data = Vector{Any}(undef, L) for (i, row) in zip(1:L,rows) row_data[i] = (i, row_data_for(row)) end if truncate_rows push!(row_data, "more") # In some environments this fails. Not sure why. last_row = applicable(lastindex, rows) ? try last(rows) catch e nothing end : nothing if !isnothing(last_row) push!(row_data, (length(rows), row_data_for(last_row))) end end # TODO: render entire schema by default? schema = Tables.schema(rows) schema_data = schema === nothing ? nothing : Dict{Symbol,Any}( :names => maptruncated(string, schema.names, "more", my_column_limit; truncate=truncate_columns), :types => String.(maptruncated(trynameof, schema.types, "more", my_column_limit; truncate=truncate_columns)), ) Dict{Symbol,Any}( :objectid => objectid2str(x), :schema => schema_data, :rows => row_data, ) end #= If the object we're trying to fileview provides rowaccess, let's try to show it. This is guaranteed to be fast (while Table.rows() may be slow). If the object is a lazy iterator, the show method will probably crash and return text repr. That's good because we don't want the show method of lazy iterators (e.g. database cursors) to be changing the (external) iterator implicitly =# pluto_showable(::MIME"application/vnd.pluto.table+object", x::Any) = try Tables.rowaccess(x)::Bool catch; false end pluto_showable(::MIME"application/vnd.pluto.table+object", t::Type) = false pluto_showable(::MIME"application/vnd.pluto.table+object", t::AbstractVector{<:NamedTuple}) = false pluto_showable(::MIME"application/vnd.pluto.table+object", t::AbstractVector{<:Dict{Symbol,<:Any}}) = false pluto_showable(::MIME"application/vnd.pluto.table+object", t::AbstractVector{Union{}}) = false end, ), Integration( id = Base.PkgId(UUID("91a5bcdd-55d7-5caf-9e0b-520d859cae80"), "Plots"), code = quote approx_size(p::Plots.Plot) = try sum(p.series_list; init=0) do series length(something(get(series, :y, ()), ())) end catch e @warn "Failed to guesstimate plot size" exception=(e,catch_backtrace()) 0 end const max_plot_size = 8000 function pluto_showable(::MIME"image/svg+xml", p::Plots.Plot{Plots.GRBackend}) format = try p.attr[:html_output_format] catch :auto end format === :svg || ( format === :auto && approx_size(p) <= max_plot_size ) end pluto_showable(::MIME"text/html", p::Plots.Plot{Plots.GRBackend}) = false end, ), Integration( id = Base.PkgId(UUID("4e3cecfd-b093-5904-9786-8bbb286a6a31"), "ImageShow"), code = quote pluto_showable(::MIME"text/html", ::AbstractMatrix{<:ImageShow.Colorant}) = false end, ), ] function load_integration_if_needed(integration::Integration) if !integration.loaded[] && haskey(Base.loaded_modules, integration.id) load_integration(integration) end end load_integrations_if_needed() = load_integration_if_needed.(integrations) function load_integration(integration::Integration) integration.loaded[] = true try eval(quote const $(Symbol(integration.id.name)) = Base.loaded_modules[$(integration.id)] $(integration.code) end) true catch e @error "Failed to load integration with $(integration.id.name).jl" exception=(e, catch_backtrace()) false end end ### # REPL THINGS ### function basic_completion_priority((s, description, exported, from_notebook)) c = first(s) if islowercase(c) 1 - 10exported elseif isuppercase(c) 2 - 10exported else 3 - 10exported end end completion_value_type_inner(x::Function) = :Function completion_value_type_inner(x::Number) = :Number completion_value_type_inner(x::AbstractString) = :String completion_value_type_inner(x::Module) = :Module completion_value_type_inner(x::AbstractArray) = :Array completion_value_type_inner(x::Any) = :Any completion_value_type(c::ModuleCompletion) = try completion_value_type_inner(getfield(c.parent, Symbol(c.mod)))::Symbol catch :unknown end completion_value_type(::Completion) = :unknown completion_special_symbol_value(::Completion) = nothing completion_special_symbol_value(completion::BslashCompletion) = haskey(REPL.REPLCompletions.latex_symbols, completion.bslash) ? REPL.REPLCompletions.latex_symbols[completion.bslash] : haskey(REPL.REPLCompletions.emoji_symbols, completion.bslash) ? REPL.REPLCompletions.emoji_symbols[completion.bslash] : nothing function is_pluto_workspace(m::Module) isdefined(m, PLUTO_INNER_MODULE_NAME) && which(m, PLUTO_INNER_MODULE_NAME) == m end """ Returns wether the module is a pluto workspace or any of its ancestors is. For example, writing the following julia code in Pluto: ```julia import Plots module A end ``` will give the following module tree: ``` Main (not pluto controlled) └── var"workspace#1" (pluto controlled) └── A (pluto controlled) └── var"workspace#2" (pluto controlled) └── A (pluto controlled) Plots (not pluto controlled) ``` """ function is_pluto_controlled(m::Module) is_pluto_workspace(m) && return true parent = parentmodule(m) parent != m && is_pluto_controlled(parent) end function completions_exported(cs::Vector{<:Completion}) completed_modules = Set{Module}(c.parent for c in cs if c isa ModuleCompletion) completed_modules_exports = Dict( m => Set(names(m, all=is_pluto_workspace(m), imported=true)) for m in completed_modules ) map(cs) do c if c isa ModuleCompletion Symbol(c.mod) ∈ completed_modules_exports[c.parent] else true end end end completion_from_notebook(c::ModuleCompletion) = is_pluto_workspace(c.parent) && c.mod != "include" && c.mod != "eval" && !startswith(c.mod, "#") completion_from_notebook(c::Completion) = false completion_type(::FuzzyCompletions.PathCompletion) = :path completion_type(::FuzzyCompletions.DictCompletion) = :dict completion_type(::FuzzyCompletions.MethodCompletion) = :method completion_type(::FuzzyCompletions.ModuleCompletion) = :module completion_type(::FuzzyCompletions.BslashCompletion) = :bslash completion_type(::FuzzyCompletions.FieldCompletion) = :field completion_type(::FuzzyCompletions.KeywordArgumentCompletion) = :keyword_argument completion_type(::FuzzyCompletions.KeywordCompletion) = :keyword completion_type(::FuzzyCompletions.PropertyCompletion) = :property completion_type(::FuzzyCompletions.Text) = :text completion_type(::Completion) = :unknown "You say Linear, I say Algebra!" function completion_fetcher(query, pos, workspace::Module) results, loc, found = FuzzyCompletions.completions( query, pos, workspace; enable_questionmark_methods=false, enable_expanduser=false, enable_path=false, enable_methods=false, enable_packages=false, ) partial = query[1:pos] if endswith(partial, '.') filter!(is_dot_completion, results) # we are autocompleting a module, and we want to see its fields alphabetically sort!(results; by=(r -> completion_text(r))) elseif endswith(partial, '/') filter!(is_path_completion, results) sort!(results; by=(r -> completion_text(r))) elseif endswith(partial, '[') filter!(is_dict_completion, results) sort!(results; by=(r -> completion_text(r))) else isenough(x) = x ≥ 0 filter!(r -> is_kwarg_completion(r) || isenough(score(r)) && !is_path_completion(r), results) # too many candiates otherwise end exported = completions_exported(results) smooshed_together = map(zip(results, exported)) do (result, rexported) ( completion_text(result)::String, completion_value_type(result)::Symbol, rexported::Bool, completion_from_notebook(result)::Bool, completion_type(result)::Symbol, completion_special_symbol_value(result), ) end p = if endswith(query, '.') sortperm(smooshed_together; alg=MergeSort, by=basic_completion_priority) else # we give 3 extra score points to exported fields scores = score.(results) sortperm(scores .+ 3.0 * exported; alg=MergeSort, rev=true) end permute!(smooshed_together, p) (smooshed_together, loc, found) end is_dot_completion(::Union{ModuleCompletion,PropertyCompletion,FieldCompletion}) = true is_dot_completion(::Completion) = false is_path_completion(::PathCompletion) = true is_path_completion(::Completion) = false is_dict_completion(::DictCompletion) = true is_dict_completion(::Completion) = false is_kwarg_completion(::FuzzyCompletions.KeywordArgumentCompletion) = true is_kwarg_completion(::Completion) = false """ is_pure_expression(expression::ReturnValue{Meta.parse}) Checks if an expression is approximately pure. Not sure if the type signature conveys it, but this take anything that is returned from `Meta.parse`. It obviously does not actually check if something is strictly pure, as `getproperty()` could be extended, and suddenly there can be side effects everywhere. This is just an approximation. """ function is_pure_expression(expr::Expr) if expr.head == :. || expr.head === :curly || expr.head === :ref all((is_pure_expression(x) for x in expr.args)) else false end end is_pure_expression(s::Symbol) = true is_pure_expression(q::QuoteNode) = true is_pure_expression(q::Number) = true is_pure_expression(q::String) = true is_pure_expression(x) = false # Better safe than sorry I guess # Based on /base/docs/bindings.jl from Julia source code function binding_from(x::Expr, workspace::Module) if x.head == :macrocall macro_name = x.args[1] if is_pure_expression(macro_name) Core.eval(workspace, macro_name) else error("Couldn't infer `$x` for Live Docs.") end elseif is_pure_expression(x) if x.head == :. # Simply calling Core.eval on `a.b` will retrieve the value instead of the binding m = Core.eval(workspace, x.args[1]) isa(m, Module) && return Docs.Binding(m, x.args[2].value) end Core.eval(workspace, x) else error("Couldn't infer `$x` for Live Docs.") end end binding_from(s::Symbol, workspace::Module) = Docs.Binding(workspace, s) binding_from(r::GlobalRef, workspace::Module) = Docs.Binding(r.mod, r.name) binding_from(other, workspace::Module) = error("Invalid @var syntax `$other`.") const DOC_SUGGESTION_LIMIT = 10 struct Suggestion match::String query::String end # inspired from REPL.printmatch() function Base.show(io::IO, ::MIME"text/html", suggestion::Suggestion) print(io, "") is, _ = REPL.bestmatch(suggestion.query, suggestion.match) for (i, char) in enumerate(suggestion.match) esc_c = get(Markdown._htmlescape_chars, char, char) if i in is print(io, "", esc_c, "") else print(io, esc_c) end end print(io, "") end "You say doc_fetcher, I say You say doc_fetcher, I say You say doc_fetcher, I say You say doc_fetcher, I say ...!!!!" function doc_fetcher(query, workspace::Module) try parsed_query = Meta.parse(query; raise=false, depwarn=false) doc_md = if Meta.isexpr(parsed_query, (:incomplete, :error, :return)) && haskey(Docs.keywords, Symbol(query)) Docs.parsedoc(Docs.keywords[Symbol(query)]) else binding = binding_from(parsed_query, workspace) doc_md = Docs.doc(binding) if !showable(MIME("text/html"), doc_md) # PyPlot returns `Text{String}` objects from their docs... # which is a bit silly, but turns out it actuall is markdown if you look hard enough. doc_md = Markdown.parse(repr(doc_md)) end improve_docs!(doc_md, parsed_query, binding) end (repr(MIME("text/html"), doc_md), :👍) catch ex (nothing, :👎) end end function improve_docs!(doc_md::Markdown.MD, query::Symbol, binding::Docs.Binding) # Reverse latex search ("\scrH" -> "\srcH") symbol = string(query) latex = REPL.symbol_latex(symbol) if !isempty(latex) push!(doc_md.content, Markdown.HorizontalRule(), Markdown.Paragraph([ Markdown.Code(symbol), " can be typed by ", Markdown.Code(latex), Base.Docs.HTML("<tab>"), ".", ])) end # Add suggestions results if no docstring was found if !Docs.defined(binding) && haskey(doc_md.meta, :results) && isempty(doc_md.meta[:results]) suggestions = REPL.accessible(binding.mod) suggestions_scores = map(s -> REPL.fuzzyscore(symbol, s), suggestions) removed_indices = [i for (i, s) in enumerate(suggestions_scores) if s < 0] deleteat!(suggestions_scores, removed_indices) deleteat!(suggestions, removed_indices) perm = sortperm(suggestions_scores; rev=true) permute!(suggestions, perm) links = map(s -> Suggestion(string(s), symbol), Iterators.take(suggestions, DOC_SUGGESTION_LIMIT)) if length(links) > 0 push!(doc_md.content, Markdown.HorizontalRule(), Markdown.Paragraph(["Similar result$(length(links) > 1 ? "s" : ""):"]), Markdown.List(links)) end end doc_md end improve_docs!(other, _, _) = other ### # BONDS ### const registered_bond_elements = Dict{Symbol, Any}() function transform_bond_value(s::Symbol, value_from_js) element = get(registered_bond_elements, s, nothing) return try transform_value_ref[](element, value_from_js) catch e @error "🚨 AbstractPlutoDingetjes: Bond value transformation errored." exception=(e, catch_backtrace()) (; message=Text("🚨 AbstractPlutoDingetjes: Bond value transformation errored."), exception=Text( sprint(showerror, e, stacktrace(catch_backtrace())) ), value_from_js, ) end end function get_bond_names(cell_id) get(cell_registered_bond_names, cell_id, Set{Symbol}()) end function possible_bond_values(s::Symbol; get_length::Bool=false) element = registered_bond_elements[s] possible_values = possible_bond_values_ref[](element) if possible_values === :NotGiven # Short-circuit to avoid the checks below, which only work if AbstractPlutoDingetjes is loaded. :NotGiven elseif possible_values isa AbstractPlutoDingetjes.Bonds.InfinitePossibilities # error("Bond \"$s\" has an unlimited number of possible values, try changing the `@bind` to something with a finite number of possible values like `PlutoUI.CheckBox(...)` or `PlutoUI.Slider(...)` instead.") :InfinitePossibilities elseif (possible_values isa AbstractPlutoDingetjes.Bonds.NotGiven) # error("Bond \"$s\" did not specify its possible values with `AbstractPlutoDingetjes.Bond.possible_values()`. Try using PlutoUI for the `@bind` values.") # If you change this, change it everywhere in this file. :NotGiven else get_length ? try length(possible_values) catch length(make_serializable(possible_values)) end : make_serializable(possible_values) end end make_serializable(x::Any) = x make_serializable(x::Union{AbstractVector,AbstractSet,Base.Generator}) = collect(x) make_serializable(x::Union{Vector,Set,OrdinalRange}) = x """ _“The name is Bond, James Bond.”_ Wraps around an `element` and not much else. When you `show` a `Bond` with the `text/html` MIME type, you will get: ```html \$(repr(MIME"text/html"(), bond.element)) ``` For example, `Bond(html"", :x)` becomes: ```html ``` The actual reactive-interactive functionality is not done in Julia - it is handled by the Pluto front-end (JavaScript), which searches cell output for `` elements, and attaches event listeners to them. Put on your slippers and have a look at the JS code to learn more. """ struct Bond element::Any defines::Symbol unique_id::String Bond(element, defines::Symbol) = showable(MIME"text/html"(), element) ? new(element, defines, Base64.base64encode(rand(UInt8,9))) : error("""Can only bind to html-showable objects, ie types T for which show(io, ::MIME"text/html", x::T) is defined.""") end function create_bond(element, defines::Symbol, cell_id::UUID) push!(cell_registered_bond_names[cell_id], defines) registered_bond_elements[defines] = element Bond(element, defines) end function Base.show(io::IO, m::MIME"text/html", bond::Bond) withtag(io, :bond, :def => bond.defines, :unique_id => bond.unique_id) do show(io, m, bond.element) end end const initial_value_getter_ref = Ref{Function}(element -> missing) const transform_value_ref = Ref{Function}((element, x) -> x) const possible_bond_values_ref = Ref{Function}((_args...; _kwargs...) -> :NotGiven) """ ```julia @bind symbol element ``` Return the HTML `element`, and use its latest JavaScript value as the definition of `symbol`. # Example ```julia @bind x html"" ``` and in another cell: ```julia x^2 ``` The first cell will show a slider as the cell's output, ranging from 0 until 100. The second cell will show the square of `x`, and is updated in real-time as the slider is moved. """ macro bind(def, element) if def isa Symbol quote $(load_integrations_if_needed)() local el = $(esc(element)) global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : $(initial_value_getter_ref)[](el) PlutoRunner.create_bond(el, $(Meta.quot(def)), $(GiveMeCellID())) end else :(throw(ArgumentError("""\nMacro example usage: \n\n\t@bind my_number html""\n\n"""))) end end """ Will be inserted in saved notebooks that use the @bind macro, make sure that they still contain legal syntax when executed as a vanilla Julia script. Overloading `Base.get` for custom UI objects gives bound variables a sensible value. """ const fake_bind = """macro bind(def, element) quote local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end local el = \$(esc(element)) global \$(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el) el end end""" ### # PUBLISHED OBJECTS ### """ **(Internal API.)** A `Ref` containing the id of the cell that is currently **running** or **displaying**. """ const currently_running_cell_id = Ref{UUID}(uuid4()) function core_published_to_js(io, x) assertpackable(x) id_start = objectid2str(x) _notebook_id = get(io, :pluto_notebook_id, notebook_id[])::UUID _cell_id = get(io, :pluto_cell_id, currently_running_cell_id[])::UUID # The unique identifier of this object id = "$_notebook_id/$id_start" d = get!(Dict{String,Any}, cell_published_objects, _cell_id) d[id] = x write(io, "/* See the documentation for AbstractPlutoDingetjes.Display.published_to_js */ getPublishedObject(\"$(id)\")") return nothing end # TODO: This is the deprecated old function. Remove me at some point. struct PublishedToJavascript published_object end function Base.show(io::IO, ::MIME"text/javascript", published::PublishedToJavascript) core_published_to_js(io, published.published_object) end Base.show(io::IO, ::MIME"text/plain", published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) Base.show(io::IO, published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) # TODO: This is the deprecated old function. Remove me at some point. function publish_to_js(x) @warn "Deprecated, use `AbstractPlutoDingetjes.Display.published_to_js(x)` instead of `PlutoRunner.publish_to_js(x)`." assertpackable(x) PublishedToJavascript(x) end const Packable = Union{Nothing,Missing,String,Symbol,Int64,Int32,Int16,Int8,UInt64,UInt32,UInt16,UInt8,Float32,Float64,Bool,MIME,UUID,DateTime} assertpackable(::Packable) = nothing assertpackable(t::Any) = throw(ArgumentError("Only simple objects can be shared with JS, like vectors and dictionaries. $(string(typeof(t))) is not compatible.")) assertpackable(::Vector{<:Packable}) = nothing assertpackable(::Dict{<:Packable,<:Packable}) = nothing assertpackable(x::Vector) = foreach(assertpackable, x) assertpackable(d::Dict) = let foreach(assertpackable, keys(d)) foreach(assertpackable, values(d)) end assertpackable(t::Tuple) = foreach(assertpackable, t) assertpackable(t::NamedTuple) = foreach(assertpackable, t) const _EmbeddableDisplay_enable_html_shortcut = Ref{Bool}(true) struct EmbeddableDisplay x script_id::String end function Base.show(io::IO, m::MIME"text/html", e::EmbeddableDisplay) body, mime = format_output_default(e.x, io) to_write = if mime === m && _EmbeddableDisplay_enable_html_shortcut[] # In this case, we can just embed the HTML content directly. body else s = """""" replace(replace(s, r"//.+" => ""), "\n" => "") end write(io, to_write) end export embed_display """ embed_display(x) A wrapper around any object that will display it using Pluto's interactive multimedia viewer (images, arrays, tables, etc.), the same system used to display cell output. The returned object can be **embedded in HTML output** (we recommend [HypertextLiteral.jl](https://github.com/MechanicalRabbit/HypertextLiteral.jl) or [HyperScript.jl](https://github.com/yurivish/Hyperscript.jl)), which means that you can use it to create things like _"table viewer left, plot right"_. # Example Markdown can interpolate HTML-showable objects, including the embedded display: ```julia md"\"" # Cool data \$(embed_display(rand(10))) Wow! "\"" ``` You can use HTML templating packages to create cool layouts, like two arrays side-by-side: ```julia using HypertextLiteral ``` ```julia @htl("\""
\$(embed_display(rand(4))) \$(embed_display(rand(4)))
"\"") ``` """ embed_display(x) = EmbeddableDisplay(x, rand('a':'z',16) |> join) # if an embedded display is being rendered _directly by Pluto's viewer_, then rendered the embedded object directly. When interpolating an embedded display into HTML, the user code will render the embedded display to HTML using the HTML show method above, and this shortcut is not called. # We add this short-circuit to increase performance for UI that uses an embedded display when it is not necessary. format_output_default(@nospecialize(val::EmbeddableDisplay), @nospecialize(context=default_iocontext)) = format_output_default(val.x, context) ### # EMBEDDED CELL OUTPUT ### Base.@kwdef struct DivElement children::Vector style::String="" class::Union{String,Nothing}=nothing end tree_data(@nospecialize(e::DivElement), context::Context) = Dict{Symbol, Any}( :style => e.style, :classname => e.class, :children => Any[ format_output_default(value, context) for value in e.children ], ) pluto_showable(::MIME"application/vnd.pluto.divelement+object", ::DivElement) = true function Base.show(io::IO, m::MIME"text/html", e::DivElement) Base.show(io, m, embed_display(e)) end ### # JS LINK ### struct JSLink callback::Function on_cancellation::Union{Nothing,Function} cancelled_ref::Ref{Bool} end const cell_js_links = Dict{UUID,Dict{String,JSLink}}() function core_with_js_link(io, callback, on_cancellation) _cell_id = get(io, :pluto_cell_id, currently_running_cell_id[])::UUID link_id = String(rand('a':'z', 16)) links = get!(() -> Dict{String,JSLink}(), cell_js_links, _cell_id) links[link_id] = JSLink(callback, on_cancellation, Ref(false)) write(io, "/* See the documentation for AbstractPlutoDingetjes.Display.with_js_link */ _internal_getJSLinkResponse(\"$(_cell_id)\", \"$(link_id)\")") end function unregister_js_link(cell_id::UUID) # cancel old links old_links = get!(() -> Dict{String,JSLink}(), cell_js_links, cell_id) for (name, link) in old_links link.cancelled_ref[] = true end for (name, link) in old_links c = link.on_cancellation c === nothing || c() end # clear cell_js_links[cell_id] = Dict{String,JSLink}() end function evaluate_js_link(notebook_id::UUID, cell_id::UUID, link_id::String, input::Any) links = get(() -> Dict{String,JSLink}(), cell_js_links, cell_id) link = get(links, link_id, nothing) with_logger_and_io_to_logs(get_cell_logger(notebook_id, cell_id); capture_stdout=false) do if link === nothing @warn "🚨 AbstractPlutoDingetjes: JS link not found." link_id (false, "link not found") elseif link.cancelled_ref[] @warn "🚨 AbstractPlutoDingetjes: JS link has already been invalidated." link_id (false, "link has been invalidated") else try result = link.callback(input) assertpackable(result) (true, result) catch ex @error "🚨 AbstractPlutoDingetjes.Display.with_js_link: Exception while evaluating Julia callback." input exception=(ex, catch_backtrace()) (false, "exception in Julia callback:\n\n$(ex)") end end end end ### # LOGGING ### const original_stdout = stdout const original_stderr = stderr const old_logger = Ref{Union{Logging.AbstractLogger,Nothing}}(nothing) struct PlutoCellLogger <: Logging.AbstractLogger stream # some packages expect this field to exist... log_channel::Channel{Any} cell_id::UUID workspace_count::Int # Used to invalidate previous logs message_limits::Dict{Any,Int} end function PlutoCellLogger(notebook_id, cell_id) notebook_log_channel = pluto_log_channels[notebook_id] PlutoCellLogger(nothing, notebook_log_channel, cell_id, moduleworkspace_count[], Dict{Any,Int}()) end struct CaptureLogger <: Logging.AbstractLogger stream logger::PlutoCellLogger logs::Vector{Any} end Logging.shouldlog(cl::CaptureLogger, args...) = Logging.shouldlog(cl.logger, args...) Logging.min_enabled_level(cl::CaptureLogger) = Logging.min_enabled_level(cl.logger) Logging.catch_exceptions(cl::CaptureLogger) = Logging.catch_exceptions(cl.logger) function Logging.handle_message(cl::CaptureLogger, level, msg, _module, group, id, file, line; kwargs...) push!(cl.logs, (level, msg, _module, group, id, file, line, kwargs)) end const pluto_cell_loggers = Dict{UUID,PlutoCellLogger}() # One logger per cell const pluto_log_channels = Dict{UUID,Channel{Any}}() # One channel per notebook function get_cell_logger(notebook_id, cell_id) logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id) if logger.workspace_count < moduleworkspace_count[] logger = pluto_cell_loggers[cell_id] = PlutoCellLogger(notebook_id, cell_id) end logger end function Logging.shouldlog(logger::PlutoCellLogger, level, _module, _...) # Accept logs # - Only if the logger is the latest for this cell using the increasing workspace_count tied to each logger # - From the user's workspace module # - Info level and above for other modules # - LogLevel(-1) because that's what ProgressLogging.jl uses for its messages current_logger = pluto_cell_loggers[logger.cell_id] if current_logger.workspace_count > logger.workspace_count return false end level = convert(Logging.LogLevel, level) (_module isa Module && is_pluto_workspace(_module)) || level >= Logging.Info || level == progress_log_level || level == stdout_log_level end const BuiltinInts = @static isdefined(Core, :BuiltinInts) ? Core.BuiltinInts : Union{Bool, Int32, Int64, UInt32, UInt64, UInt8, Int128, Int16, Int8, UInt128, UInt16} Logging.min_enabled_level(::PlutoCellLogger) = min(Logging.Debug, stdout_log_level) Logging.catch_exceptions(::PlutoCellLogger) = false function Logging.handle_message(pl::PlutoCellLogger, level, msg, _module, group, id, file, line; kwargs...) # println("receiving msg from ", _module, " ", group, " ", id, " ", msg, " ", level, " ", line, " ", file) # println("with types: ", "_module: ", typeof(_module), ", ", "msg: ", typeof(msg), ", ", "group: ", typeof(group), ", ", "id: ", typeof(id), ", ", "file: ", typeof(file), ", ", "line: ", typeof(line), ", ", "kwargs: ", typeof(kwargs)) # thanks Copilot # https://github.com/JuliaLang/julia/blob/eb2e9687d0ac694d0aa25434b30396ee2cfa5cd3/stdlib/Logging/src/ConsoleLogger.jl#L110-L115 if get(kwargs, :maxlog, nothing) isa BuiltinInts maxlog = kwargs[:maxlog] remaining = get!(pl.message_limits, id, Int(maxlog)::Int) pl.message_limits[id] = remaining - 1 if remaining <= 0 return end end try yield() po() = get(cell_published_objects, pl.cell_id, Dict{String,Any}()) before_published_object_keys = collect(keys(po())) # Render the log arguments: msg_formatted = format_output_default(msg isa AbstractString ? Text(msg) : msg) kwargs_formatted = Tuple{String,Any}[(string(k), format_log_value(v)) for (k, v) in kwargs if k != :maxlog] after_published_object_keys = collect(keys(po())) new_published_object_keys = setdiff(after_published_object_keys, before_published_object_keys) # (Running `put!(pl.log_channel, x)` will send `x` to the pluto server. See `start_relaying_logs` for the receiving end.) put!(pl.log_channel, Dict{String,Any}( "level" => string(level), "msg" => msg_formatted, # This is a dictionary containing all published objects that were published during the rendering of the log arguments (we cannot track which objects were published during the execution of the log statement itself i think...) "new_published_objects" => Dict{String,Any}( key => po()[key] for key in new_published_object_keys ), "group" => string(group), "id" => string(id), "file" => string(file), "cell_id" => pl.cell_id, "line" => line isa Union{Int32,Int64} ? line : nothing, "kwargs" => kwargs_formatted, )) yield() catch e println(original_stderr, "Failed to relay log from PlutoRunner") showerror(original_stderr, e, stacktrace(catch_backtrace())) nothing end end format_log_value(v) = format_output_default(v) format_log_value(v::Tuple{<:Exception,Vector{<:Any}}) = format_output(CapturedException(v...)) function _send_stdio_output!(output, loglevel) output_str = String(take!(output)) if !isempty(output_str) Logging.@logmsg loglevel output_str end end const stdout_log_level = Logging.LogLevel(-555) # https://en.wikipedia.org/wiki/555_timer_IC const progress_log_level = Logging.LogLevel(-1) # https://github.com/JuliaLogging/ProgressLogging.jl/blob/0e7933005233722d6214b0debe3316c82b4d14a7/src/ProgressLogging.jl#L36 function with_io_to_logs(f::Function; enabled::Bool=true, loglevel::Logging.LogLevel=Logging.LogLevel(1)) if !enabled return f() end # Taken from https://github.com/JuliaDocs/IOCapture.jl/blob/master/src/IOCapture.jl with some modifications to make it log. # Original implementation from Documenter.jl (MIT license) # Save the default output streams. default_stdout = stdout default_stderr = stderr # Redirect both the `stdout` and `stderr` streams to a single `Pipe` object. pipe = Pipe() Base.link_pipe!(pipe; reader_supports_async = true, writer_supports_async = true) pe_stdout = pipe.in pe_stderr = pipe.in redirect_stdout(pe_stdout) redirect_stderr(pe_stderr) # Bytes written to the `pipe` are captured in `output` and eventually converted to a # `String`. We need to use an asynchronous task to continously tranfer bytes from the # pipe to `output` in order to avoid the buffer filling up and stalling write() calls in # user code. execution_done = Ref(false) output = IOBuffer() @async begin pipe_reader = Base.pipe_reader(pipe) try while !eof(pipe_reader) write(output, readavailable(pipe_reader)) # NOTE: we don't really have to wait for the end of execution to stream output logs # so maybe we should just enable it? if execution_done[] _send_stdio_output!(output, loglevel) end end _send_stdio_output!(output, loglevel) catch err @error "Failed to redirect stdout/stderr to logs" exception=(err,catch_backtrace()) if err isa InterruptException rethrow(err) end end end # To make the `display` function work. redirect_display = TextDisplay(IOContext(pe_stdout, default_stdout_iocontext)) pushdisplay(redirect_display) # Run the function `f`, capturing all output that it might have generated. # Success signals whether the function `f` did or did not throw an exception. result = try f() finally # Restore display try popdisplay(redirect_display) catch e # This happens when the user calls `popdisplay()`, fine. # @warn "Pluto's display was already removed?" e end execution_done[] = true # Restore the original output streams. redirect_stdout(default_stdout) redirect_stderr(default_stderr) close(pe_stdout) close(pe_stderr) end result end function with_logger_and_io_to_logs(f, logger; capture_stdout=true, stdio_loglevel=stdout_log_level) Logging.with_logger(logger) do with_io_to_logs(f; enabled=capture_stdout, loglevel=stdio_loglevel) end end function setup_plutologger(notebook_id::UUID, log_channel::Channel{Any}) pluto_log_channels[notebook_id] = log_channel end include("./precompile.jl") end H/opt/julia/packages/Pluto/GVuR6/src/runner/PlutoRunner/src/precompile.jlusing PrecompileTools: PrecompileTools using UUIDs: uuid1 const __TEST_NOTEBOOK_ID = uuid1() PrecompileTools.@compile_workload begin let channel = Channel{Any}(10) PlutoRunner.setup_plutologger( __TEST_NOTEBOOK_ID, channel, ) end expr = Expr(:toplevel, :(1 + 1)) cell_id = uuid1() workspace = Module() PlutoRunner.run_expression(workspace, expr, __TEST_NOTEBOOK_ID, cell_id, nothing); PlutoRunner.formatted_result_of(__TEST_NOTEBOOK_ID, cell_id, false, String[], nothing, workspace; capture_stdout=true) foreach(("sq", "\\sq", "Base.a", "sqrt(", "sum(x; dim")) do s PlutoRunner.completion_fetcher(s, ncodeunits(s), Main) end end 9/opt/julia/packages/Pluto/GVuR6/src/packages/PkgCompat.jl@module PkgCompat export package_versions, package_completions import REPL import Pkg import Pkg.Types: VersionRange import RegistryInstances import ..Pluto const PRESERVE_ALL_INSTALLED = isdefined(Pkg, :PRESERVE_ALL_INSTALLED) ? Pkg.PRESERVE_ALL_INSTALLED : Pkg.PRESERVE_ALL @static if isdefined(Pkg,:REPLMode) && isdefined(Pkg.REPLMode,:complete_remote_package) const REPLMode = Pkg.REPLMode else const REPLMode = Base.get_extension(Pkg, :REPLExt) end # Should be in Base flatmap(args...) = vcat(map(args...)...) # Should be in Base function select(f::Function, xs) for x ∈ xs if f(x) return x end end nothing end #= NOTE ABOUT PUBLIC/INTERNAL PKG API Pkg.jl exposes lots of API, but only some of it is "public": guaranteed to remain available. API is public if it is listed here: https://pkgdocs.julialang.org/v1/api/ In this file, I labeled functions by their status using 🐸, ⚠️, etc. A status in brackets (like this) means that it is only called within this file, and the fallback might be in a caller function. --- I tried to only use public API, except: - I use the `Pkg.Types.Context` value as first argument for many functions, since the server process manages multiple notebook processes, each with their own package environment. We could get rid of this, by settings `Base.ACTIVE_PROJECT[]` before and after each Pkg call. (This is temporarily activating the notebook environment.) This does have a performance impact, since the project and manifest caches are regenerated every time. - https://github.com/JuliaLang/Pkg.jl/issues/2607 seems to be impossible with the current public API. - Some functions try to use internal API for optimization/better features. =# ### # CONTEXT ### const PkgContext = if isdefined(Pkg, :Context) Pkg.Context elseif isdefined(Pkg, :Types) && isdefined(Pkg.Types, :Context) Pkg.Types.Context elseif isdefined(Pkg, :API) && isdefined(Pkg.API, :Context) Pkg.API.Context else Pkg.Types.Context end function PkgContext!(ctx::PkgContext; kwargs...) for (k, v) in kwargs setfield!(ctx, k, v) end ctx end # 🐸 "Public API", but using PkgContext load_ctx(env_dir)::PkgContext = PkgContext(;env=Pkg.Types.EnvCache(joinpath(env_dir, "Project.toml"))) # 🐸 "Public API", but using PkgContext load_ctx!(ctx::PkgContext, env_dir)::PkgContext = PkgContext!(ctx; env=Pkg.Types.EnvCache(joinpath(env_dir, "Project.toml"))) # 🐸 "Public API", but using PkgContext load_empty_ctx!(ctx) = @static if :io ∈ fieldnames(PkgContext) PkgContext!(create_empty_ctx(); io=ctx.io) else create_empty_ctx() end # 🐸 "Public API", but using PkgContext create_empty_ctx()::PkgContext = load_ctx!(PkgContext(), mktempdir()) # ⚠️ Internal API with fallback function load_ctx!(original::PkgContext) original_project = deepcopy(original.env.original_project) original_manifest = deepcopy(original.env.original_manifest) new = load_ctx!(original, env_dir(original)) try new.env.original_project = original_project new.env.original_manifest = original_manifest catch e @warn "Pkg compat: failed to set original_project" exception=(e,catch_backtrace()) end new end # ⚠️ Internal API with fallback function mark_original!(ctx::PkgContext) try ctx.env.original_project = deepcopy(ctx.env.project) ctx.env.original_manifest = deepcopy(ctx.env.manifest) catch e @warn "Pkg compat: failed to set original_project" exception=(e,catch_backtrace()) end end # ⚠️ Internal API with fallback function is_original(ctx::PkgContext)::Bool try ctx.env.original_project == ctx.env.project && ctx.env.original_manifest == ctx.env.manifest catch e @warn "Pkg compat: failed to get original_project" exception=(e,catch_backtrace()) false end end # 🐸 "Public API", but using PkgContext env_dir(ctx::PkgContext) = dirname(ctx.env.project_file) project_file(x::AbstractString) = joinpath(x, "Project.toml") manifest_file(x::AbstractString) = joinpath(x, "Manifest.toml") project_file(ctx::PkgContext) = joinpath(env_dir(ctx), "Project.toml") manifest_file(ctx::PkgContext) = joinpath(env_dir(ctx), "Manifest.toml") function read_project_file(x)::String path = project_file(x) isfile(path) ? read(path, String) : "" end function read_manifest_file(x)::String path = manifest_file(x) isfile(path) ? read(path, String) : "" end # ⚠️ Internal API with fallback function withio(f::Function, ctx::PkgContext, io::IO) @static if :io ∈ fieldnames(PkgContext) old_io = ctx.io ctx.io = io result = try f() finally ctx.io = old_io nothing end result else f() end end # I'm a pirate harrr 🏴‍☠️ @static if isdefined(Pkg, :can_fancyprint) Pkg.can_fancyprint(io::IOContext{IOBuffer}) = get(io, :sneaky_enable_tty, false) === true end ### # REGISTRIES ### # (✅ "Public" API using RegistryInstances) "Return all installed registries as `RegistryInstances.RegistryInstance` structs." _get_registries() = RegistryInstances.reachable_registries() # (✅ "Public" API using RegistryInstances) "The cached output value of `_get_registries`." const _parsed_registries = Ref(RegistryInstances.RegistryInstance[]) # (✅ "Public" API using RegistryInstances) "Re-parse the installed registries from disk." function refresh_registry_cache() _parsed_registries[] = _get_registries() end # ⚠️✅ Internal API with fallback const _updated_registries_compat = @static if isdefined(Pkg, :UPDATED_REGISTRY_THIS_SESSION) && Pkg.UPDATED_REGISTRY_THIS_SESSION isa Ref{Bool} Pkg.UPDATED_REGISTRY_THIS_SESSION else Ref(false) end # ✅ Public API function update_registries(; force::Bool=false) if force || !_updated_registries_compat[] Pkg.Registry.update() try refresh_registry_cache() catch end _updated_registries_compat[] = true end end # ✅ Public API """ Check when the registries were last updated. If it is recent (max 7 days), then `Pkg.UPDATED_REGISTRY_THIS_SESSION[]` is set to `true`, which will prevent Pkg from doing an automatic registry update. Returns the new value of `Pkg.UPDATED_REGISTRY_THIS_SESSION[]`. """ function check_registry_age(max_age_ms = 1000.0 * 60 * 60 * 24 * 7)::Bool if get(ENV, "GITHUB_ACTIONS", "false") == "true" # don't do this optimization in CI return false end paths = [s.path for s in _get_registries()] isempty(paths) && return _updated_registries_compat[] mtimes = map(paths) do p try mtime(p) catch zero(time()) end end if all(mtimes .> time() - max_age_ms) _updated_registries_compat[] = true end _updated_registries_compat[] end ### # Instantiate ### # ⚠️ Internal API function instantiate(ctx; update_registry::Bool, allow_autoprecomp::Bool) Pkg.instantiate(ctx; update_registry, allow_autoprecomp) # Not sure how to make a fallback: # - hasmethod cannot test for kwargs because instantiate takes kwargs... that are passed on somewhere else # - can't catch for a CallError because the error is weird end ### # Standard Libraries ### # (⚠️ Internal API with fallback) _stdlibs() = try stdlibs = values(Pkg.Types.stdlibs()) T = eltype(stdlibs) if T == String stdlibs elseif T <: Tuple{String,Any} first.(stdlibs) else error() end catch e @warn "Pkg compat: failed to load standard libraries." exception=(e,catch_backtrace()) String["ArgTools", "Artifacts", "Base64", "CRC32c", "CompilerSupportLibraries_jll", "Dates", "DelimitedFiles", "Distributed", "Downloads", "FileWatching", "Future", "GMP_jll", "InteractiveUtils", "LLD_jll", "LLVMLibUnwind_jll", "LazyArtifacts", "LibCURL", "LibCURL_jll", "LibGit2", "LibGit2_jll", "LibOSXUnwind_jll", "LibSSH2_jll", "LibUV_jll", "LibUnwind_jll", "Libdl", "LinearAlgebra", "Logging", "MPFR_jll", "Markdown", "MbedTLS_jll", "Mmap", "MozillaCACerts_jll", "NetworkOptions", "OpenBLAS_jll", "OpenLibm_jll", "PCRE2_jll", "Pkg", "Printf", "Profile", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "SuiteSparse", "SuiteSparse_jll", "TOML", "Tar", "Test", "UUIDs", "Unicode", "Zlib_jll", "dSFMT_jll", "libLLVM_jll", "libblastrampoline_jll", "nghttp2_jll", "p7zip_jll"] end # ⚠️ Internal API with fallback is_stdlib(package_name::AbstractString) = package_name ∈ _stdlibs() # Initial fill of registry cache function __init__() refresh_registry_cache() global global_ctx=PkgContext() end ### # Package names ### # ⚠️ Internal API with fallback function package_completions(partial_name::AbstractString)::Vector{String} String[ filter(s -> startswith(s, partial_name), collect(_stdlibs())); _registered_package_completions(partial_name) ] end # (⚠️ Internal API with fallback) function _registered_package_completions(partial_name::AbstractString)::Vector{String} # compat try @static if hasmethod(REPLMode.complete_remote_package, (String,)) REPLMode.complete_remote_package(partial_name) else REPLMode.complete_remote_package(partial_name, 1, length(partial_name))[1] end catch e @warn "Pkg compat: failed to autocomplete packages" exception=(e,catch_backtrace()) String[] end end ### # Package versions ### # (✅ "Public" API) """ Return paths to all found registry entries of a given package name. # Example ```julia julia> Pluto.PkgCompat._registry_entries("Pluto") 1-element Vector{String}: "/Users/fons/.julia/registries/General/P/Pluto" ``` """ function _registry_entries(package_name::AbstractString, registries::Vector=_parsed_registries[])::Vector{String} flatmap(registries) do reg packages = values(reg.pkgs) String[ joinpath(reg.path, d.path) for d in packages if d.name == package_name ] end end # (🐸 "Public API", but using PkgContext) function _package_versions_from_path(registry_entry_fullpath::AbstractString)::Vector{VersionNumber} # compat vd = @static if isdefined(Pkg, :Operations) && isdefined(Pkg.Operations, :load_versions) && hasmethod(Pkg.Operations.load_versions, (String,)) Pkg.Operations.load_versions(registry_entry_fullpath) else Pkg.Operations.load_versions(PkgContext(), registry_entry_fullpath) end vd |> keys |> collect end # ✅ "Public" API using RegistryInstances """ Return all registered versions of the given package. Returns `["stdlib"]` for standard libraries, a `Vector{VersionNumber}` for registered packages, or `["latest"]` if it crashed. """ function package_versions(package_name::AbstractString)::Vector if is_stdlib(package_name) ["stdlib"] else try flatmap(_parsed_registries[]) do reg uuids_with_name = RegistryInstances.uuids_from_name(reg, package_name) flatmap(uuids_with_name) do u pkg = get(reg, u, nothing) if pkg !== nothing info = RegistryInstances.registry_info(pkg) collect(keys(info.version_info)) else [] end end end |> sort! catch e @warn "Pkg compat: failed to get installable versions." exception=(e,catch_backtrace()) ["latest"] end end end # ⚠️ Internal API with fallback "Does a package with this name exist in one of the installed registries?" package_exists(package_name::AbstractString)::Bool = package_versions(package_name) |> !isempty # 🐸 "Public API", but using PkgContext function dependencies(ctx) try # ctx.env.manifest @static if hasmethod(Pkg.dependencies, (PkgContext,)) Pkg.dependencies(ctx) else Pkg.dependencies(ctx.env) end catch e if !any(occursin(sprint(showerror, e)), ( r"expected.*exist.*manifest", r"no method.*project_rel_path.*Nothing\)", # https://github.com/JuliaLang/Pkg.jl/issues/3404 )) @error """ Pkg error: you might need to use Pluto.reset_notebook_environment(notebook_path) to reset this notebook's environment. Before doing so, consider sending your notebook file to https://github.com/fonsp/Pluto.jl/issues together with the following info: """ Pluto.PLUTO_VERSION VERSION exception=(e,catch_backtrace()) end Dict() end end function project(ctx::PkgContext) @static if hasmethod(Pkg.project, (PkgContext,)) Pkg.project(ctx) else Pkg.project(ctx.env) end end # 🐸 "Public API", but using PkgContext "Find a package in the manifest. Return `nothing` if not found." _get_manifest_entry(ctx::PkgContext, package_name::AbstractString) = select(e -> e.name == package_name, values(dependencies(ctx))) # ⚠️ Internal API with fallback """ Find a package in the manifest given its name, and return its installed version. Return `"stdlib"` for a standard library, and `nothing` if not found. """ function get_manifest_version(ctx::PkgContext, package_name::AbstractString) if is_stdlib(package_name) "stdlib" else entry = _get_manifest_entry(ctx, package_name) entry === nothing ? nothing : entry.version end end ### # WRITING COMPAT ENTRIES ### _project_key_order = ["name", "uuid", "keywords", "license", "desc", "deps", "compat"] project_key_order(key::String) = something(findfirst(x -> x == key, _project_key_order), length(_project_key_order) + 1) # ✅ Public API function _modify_compat!(f!::Function, ctx::PkgContext)::PkgContext project_path = project_file(ctx) toml = if isfile(project_path) Pkg.TOML.parsefile(project_path) else Dict{String,Any}() end compat = get!(Dict{String,Any}, toml, "compat") f!(compat) isempty(compat) && delete!(toml, "compat") write(project_path, sprint() do io Pkg.TOML.print(io, toml; sorted=true, by=(key -> (project_key_order(key), key))) end) return _update_project_hash!(load_ctx!(ctx)) end # ✅ Internal API with fallback "Update the project hash in the manifest file (https://github.com/JuliaLang/Pkg.jl/pull/2815)" function _update_project_hash!(ctx::PkgContext) VERSION >= v"1.8.0" && isfile(manifest_file(ctx)) && try Pkg.Operations.record_project_hash(ctx.env) Pkg.Types.write_manifest(ctx.env) catch e @info "Failed to update project hash." exception=(e,catch_backtrace()) end ctx end # ✅ Public API """ Add any missing [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries to the `Project.toml` for all direct dependencies. This serves as a 'fallback' in case someone (with a different Julia version) opens your notebook without being able to load the `Manifest.toml`. Return the new `PkgContext`. The automatic compat entry is: `"~" * string(installed_version)`. """ function write_auto_compat_entries!(ctx::PkgContext)::PkgContext _modify_compat!(ctx) do compat for p in keys(project(ctx).dependencies) if !haskey(compat, p) m_version = get_manifest_version(ctx, p) if m_version !== nothing && !is_stdlib(p) compat[p] = "~" * string(VersionNumber(m_version.major, m_version.minor, m_version.patch)) # drop build number end end end end end # ✅ Public API """ Remove all [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries from the `Project.toml`. """ function clear_compat_entries!(ctx::PkgContext)::PkgContext if isfile(project_file(ctx)) _modify_compat!(empty!, ctx) else ctx end end # ✅ Public API """ Remove any automatically-generated [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries from the `Project.toml`. This will undo the effects of [`write_auto_compat_entries!`](@ref) but leave other (e.g. manual) compat entries intact. Return the new `PkgContext`. """ function clear_auto_compat_entries!(ctx::PkgContext)::PkgContext if isfile(project_file(ctx)) _modify_compat!(ctx) do compat for p in keys(compat) m_version = get_manifest_version(ctx, p) if m_version !== nothing && !is_stdlib(p) if compat[p] == "~" * string(m_version) delete!(compat, p) end end end end else ctx end end # ✅ Public API """ Remove any [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries from the `Project.toml` for standard libraries. These entries are created when an old version of Julia uses a package that later became a standard library, like https://github.com/JuliaPackaging/Artifacts.jl. Return the new `PkgContext`. """ function clear_stdlib_compat_entries!(ctx::PkgContext)::PkgContext if isfile(project_file(ctx)) _modify_compat!(ctx) do compat for p in keys(compat) if is_stdlib(p) @info "Removing compat entry for stdlib" p delete!(compat, p) end end end else ctx end end end 7/opt/julia/packages/Pluto/GVuR6/src/webserver/Status.jl module Status _default_update_listener() = nothing Base.@kwdef mutable struct Business name::Symbol=:ignored success::Union{Nothing,Bool}=nothing started_at::Union{Nothing,Float64}=nothing finished_at::Union{Nothing,Float64}=nothing subtasks::Dict{Symbol,Business}=Dict{Symbol,Business}() update_listener_ref::Ref{Function}=Ref{Function}(_default_update_listener) lock::Threads.SpinLock=Threads.SpinLock() end tojs(b::Business) = Dict{String,Any}( "name" => b.name, "success" => b.success, "started_at" => b.started_at, "finished_at" => b.finished_at, "subtasks" => Dict{String,Any}( String(s) => tojs(r) for (s, r) in b.subtasks ), ) function report_business_started!(business::Business) lock(business.lock) do business.success = nothing business.started_at = time() business.finished_at = nothing empty!(business.subtasks) end business.update_listener_ref[]() return business end function report_business_finished!(business::Business, success::Bool=true) if business.success === nothing && business.started_at !== nothing && business.finished_at === nothing business.success = success end lock(business.lock) do # if it never started, then lets "start" it now business.started_at = something(business.started_at, time()) # if it already finished, then leave the old finish time. business.finished_at = something(business.finished_at, max(business.started_at, time())) end # also finish all subtasks (this can't be inside the same lock) for v in values(business.subtasks) report_business_finished!(v, success) end business.update_listener_ref[]() return business end create_for_child(parent::Business, name::Symbol) = function() Business(; name, update_listener_ref=parent.update_listener_ref, lock=parent.lock) end get_child(parent::Business, name::Symbol) = lock(parent.lock) do get!(create_for_child(parent, name), parent.subtasks, name) end report_business_finished!(parent::Business, name::Symbol, success::Bool=true) = report_business_finished!(get_child(parent, name), success) report_business_started!(parent::Business, name::Symbol) = get_child(parent, name) |> report_business_started! report_business_planned!(parent::Business, name::Symbol) = get_child(parent, name) report_business!(f::Function, parent::Business, args...) = try report_business_started!(parent, args...) f() finally report_business_finished!(parent, args...) end delete_business!(business::Business, name::Symbol) = lock(business.lock) do delete!(business.subtasks, name) end # GLOBAL # registry update ## once per process # waiting for other notebook packages # PER NOTEBOOK # notebook process starting # installing packages # updating packages # running cells end 4/opt/julia/packages/Pluto/GVuR6/src/notebook/Cell.jl import UUIDs: UUID, uuid1 const METADATA_DISABLED_KEY = "disabled" const METADATA_SHOW_LOGS_KEY = "show_logs" const METADATA_SKIP_AS_SCRIPT_KEY = "skip_as_script" # Make sure to keep this in sync with DEFAULT_CELL_METADATA in ../frontend/components/Editor.js const DEFAULT_CELL_METADATA = Dict{String, Any}( METADATA_DISABLED_KEY => false, METADATA_SHOW_LOGS_KEY => true, METADATA_SKIP_AS_SCRIPT_KEY => false, ) Base.@kwdef struct CellOutput body::Union{Nothing,String,Vector{UInt8},Dict}=nothing mime::MIME=MIME("text/plain") rootassignee::Union{Symbol,Nothing}=nothing "Time that the last output was created, used only on the frontend to rerender the output" last_run_timestamp::Float64=0 "Whether `this` inside `") == "" ╠═╡ =# # ╔═╡ 525b2cb6-b7b9-436e-898e-a951e6a1f2f1 #=╠═╡ @test occursin("reactive", download_cool_string("https://raw.githubusercontent.com/fonsp/Pluto.jl/v0.17.1/README.md")) ╠═╡ =# # ╔═╡ 3630b4bc-ff63-426d-b95d-ae4e4f9ccd88 download_cool_data(args...) = read(download_cool(args...)) # ╔═╡ 40b48818-e191-4509-85ad-b9ff745cd0cb #=╠═╡ @test_throws Exception download_cool("data:xoxo;base10,asdfasdfasdf") ╠═╡ =# # ╔═╡ 1f175fcd-8b94-4f13-a912-02a21c95f8ca #=╠═╡ @test_throws Exception download_cool("data:text/plain;base10,asdfasdfasdf") ╠═╡ =# # ╔═╡ a4f671e6-0e23-4753-9301-048b2ef505e3 #=╠═╡ @test_throws Exception download_cool("data:asdfasdfasdf") ╠═╡ =# # ╔═╡ ae296e09-08dd-4ee8-87ac-eb2bf24b28b9 #=╠═╡ random_data_url = "data:asf;base64,$( Base64.base64encode(random_data) )" ╠═╡ =# # ╔═╡ 2eabfa58-2d8f-4479-9c00-a58b934638d9 #=╠═╡ @test download_cool_data(random_data_url) == random_data ╠═╡ =# # ╔═╡ Cell order: # ╟─cc180e7e-46c3-11ec-3fff-05e1b5c77986 # ╠═a85c0c0b-47d0-4377-bc22-3c87239a67b3 # ╠═6339496d-11be-40d0-b4e5-9247e5199367 # ╠═bf7b4241-9cb0-4d90-9ded-b527bf220803 # ╠═d6e01532-a8e4-4173-a270-eae37c8002c7 # ╠═b0ba1add-f452-4a44-ab23-becbc610e2b9 # ╠═e630e261-1c2d-4117-9c44-dd49199fa3de # ╠═4bb75573-09bd-4ce7-b76f-34c0249d7b88 # ╠═301eee81-7715-4d39-89aa-37bffde3557f # ╠═2385dd3b-15f8-4790-907f-e0576a56c4c0 # ╠═ae296e09-08dd-4ee8-87ac-eb2bf24b28b9 # ╠═2eabfa58-2d8f-4479-9c00-a58b934638d9 # ╠═525b2cb6-b7b9-436e-898e-a951e6a1f2f1 # ╠═6e1dd79c-a7bf-44d6-bfa6-ced75b45170a # ╠═3630b4bc-ff63-426d-b95d-ae4e4f9ccd88 # ╠═40b48818-e191-4509-85ad-b9ff745cd0cb # ╠═1f175fcd-8b94-4f13-a912-02a21c95f8ca # ╠═a4f671e6-0e23-4753-9301-048b2ef505e3 # ╠═d8ed6d44-33cd-4c9d-828b-d237d43769f5 # ╠═b3f685a3-b52d-4190-9196-6977a7e76aa1 # ╠═b987a8a2-6ab0-4e88-af3c-d7f2778af657 8/opt/julia/packages/Pluto/GVuR6/src/webserver/MsgPack.jl# this file is just a bunch of ugly code to make sure that the Julia Array{UInt8,1} becomes a JS Uint8Array() instead of a normal array. This improves performance of the client. # ignore it if you are not interested in that kind of stuff import Dates import UUIDs: UUID import MsgPack import .Configuration import Pkg # MsgPack.jl doesn't define a serialization method for MIME and UUID objects, so we write these ourselves: MsgPack.msgpack_type(::Type{<:MIME}) = MsgPack.StringType() MsgPack.msgpack_type(::Type{UUID}) = MsgPack.StringType() MsgPack.msgpack_type(::Type{VersionNumber}) = MsgPack.StringType() MsgPack.msgpack_type(::Type{Pkg.Types.VersionRange}) = MsgPack.StringType() MsgPack.to_msgpack(::MsgPack.StringType, m::MIME) = string(m) MsgPack.to_msgpack(::MsgPack.StringType, u::UUID) = string(u) MsgPack.to_msgpack(::MsgPack.StringType, v::VersionNumber) = string(v) MsgPack.to_msgpack(::MsgPack.StringType, v::Pkg.Types.VersionRange) = string(v) # Support for sending Dates MsgPack.msgpack_type(::Type{Dates.DateTime}) = MsgPack.ExtensionType() MsgPack.to_msgpack(::MsgPack.ExtensionType, d::Dates.DateTime) = let millisecs_since_1970_because_thats_how_computers_work = Dates.value(d - Dates.DateTime(1970)) MsgPack.Extension(0x0d, reinterpret(UInt8, [millisecs_since_1970_because_thats_how_computers_work])) end # Our Configuration types: MsgPack.msgpack_type(::Type{Configuration.Options}) = MsgPack.StructType() MsgPack.msgpack_type(::Type{Configuration.EvaluationOptions}) = MsgPack.StructType() MsgPack.msgpack_type(::Type{Configuration.CompilerOptions}) = MsgPack.StructType() MsgPack.msgpack_type(::Type{Configuration.ServerOptions}) = MsgPack.StructType() MsgPack.msgpack_type(::Type{Configuration.SecurityOptions}) = MsgPack.StructType() # Don't try to send callback functions which can't be serialized (see ServerOptions.event_listener) MsgPack.msgpack_type(::Type{Function}) = MsgPack.NilType() MsgPack.to_msgpack(::MsgPack.NilType, ::Function) = nothing # We want typed integer arrays to arrive as JS typed integer arrays: const JSTypedIntSupport = [Int8, UInt8, Int16, UInt16, Int32, UInt32, Float32, Float64] const JSTypedInt = Union{Int8,UInt8,Int16,UInt16,Int32,UInt32,Float32,Float64} MsgPack.msgpack_type(::Type{Vector{T}}) where T <: JSTypedInt = MsgPack.ExtensionType() function MsgPack.to_msgpack(::MsgPack.ExtensionType, x::Vector{T}) where T <: JSTypedInt type = findfirst(isequal(T), JSTypedIntSupport) + 0x10 MsgPack.Extension(type, reinterpret(UInt8, x)) end MsgPack.msgpack_type(::Type{Vector{Union{}}}) = MsgPack.ArrayType() # The other side does the same (/frontend/common/MsgPack.js), and we decode it here: function decode_extension_and_addbits(x::MsgPack.Extension) if x.type == 0x0d # the datetime type millisecs_since_1970_because_thats_how_computers_work = reinterpret(Int64, x.data)[1] Dates.DateTime(1970) + Dates.Millisecond(millisecs_since_1970_because_thats_how_computers_work) # TODO? Dates.unix2datetime does exactly this ?? - DRAL else # the array types julia_type = JSTypedIntSupport[x.type - 0x10] if eltype(x.data) == julia_type x.data else reinterpret(julia_type, x.data) end end end function decode_extension_and_addbits(x::Dict) # we mutate the dictionary, that's fine in our use case and saves memory? for (k, v) in x x[k] = decode_extension_and_addbits(v) end x end decode_extension_and_addbits(x::Array) = map(decode_extension_and_addbits, x) # We also convert everything (except the JS typed arrays) to 64 bit numbers, just to make it easier to work with. decode_extension_and_addbits(x::T) where T <: Union{Signed,Unsigned} = Int64(x) decode_extension_and_addbits(x::T) where T <: AbstractFloat = Float64(x) decode_extension_and_addbits(x::Any) = x function pack(args...) MsgPack.pack(args...) end function unpack(args...) MsgPack.unpack(args...) |> decode_extension_and_addbits end precompile(unpack, (Vector{UInt8},))?/opt/julia/packages/Pluto/GVuR6/src/webserver/SessionActions.jl)module SessionActions import ..Pluto: Pluto, Status, ServerSession, Notebook, Cell, emptynotebook, tamepath, new_notebooks_directory, without_pluto_file_extension, numbered_until_new, cutename, readwrite, update_save_run!, update_nbpkg_cache!, update_from_file, wait_until_file_unchanged, putnotebookupdates!, putplutoupdates!, load_notebook, clientupdate_notebook_list, WorkspaceManager, try_event_call, NewNotebookEvent, OpenNotebookEvent, ShutdownNotebookEvent, @asynclog, ProcessStatus, maybe_convert_path_to_wsl, move_notebook!, throttled using FileWatching import ..Pluto.DownloadCool: download_cool import HTTP import UUIDs: UUID, uuid1 struct NotebookIsRunningException <: Exception notebook::Notebook end abstract type AbstractUserError <: Exception end struct UserError <: AbstractUserError msg::String end function Base.showerror(io::IO, e::UserError) print(io, e.msg) end function open_url(session::ServerSession, url::AbstractString; kwargs...) name_from_url = startswith(url, r"https?://") ? strip(HTTP.unescapeuri(splitext(basename(HTTP.URI(url).path))[1])) : "" new_name = isempty(name_from_url) ? cutename() : name_from_url random_notebook = emptynotebook() random_notebook.path = numbered_until_new( joinpath( new_notebooks_directory(), new_name ); suffix=".jl") path = download_cool(url, random_notebook.path) result = try_event_call(session, NewNotebookEvent()) notebook = if result isa UUID open(session, path; notebook_id=result, kwargs...) else open(session, path; kwargs...) end return notebook end "Open the notebook at `path` into `session::ServerSession` and run it. Returns the `Notebook`." function open(session::ServerSession, path::AbstractString; execution_allowed::Bool=true, run_async::Bool=true, compiler_options=nothing, as_sample::Bool=false, risky_file_source::Union{Nothing,String}=nothing, clear_frontmatter::Bool=false, notebook_id::UUID=uuid1() ) path = maybe_convert_path_to_wsl(path) if as_sample new_filename = "sample " * without_pluto_file_extension(basename(path)) new_path = numbered_until_new(joinpath(new_notebooks_directory(), new_filename); suffix=".jl") readwrite(path, new_path) path = new_path end for notebook in values(session.notebooks) if isfile(notebook.path) && realpath(notebook.path) == realpath(tamepath(path)) throw(NotebookIsRunningException(notebook)) end end notebook = load_notebook(tamepath(path); disable_writing_notebook_files=session.options.server.disable_writing_notebook_files) execution_allowed = execution_allowed && !haskey(notebook.metadata, "risky_file_source") notebook.notebook_id = notebook_id if !isnothing(risky_file_source) notebook.metadata["risky_file_source"] = risky_file_source end notebook.process_status = execution_allowed ? ProcessStatus.starting : ProcessStatus.waiting_for_permission # overwrites the notebook environment if specified if compiler_options !== nothing notebook.compiler_options = compiler_options end if clear_frontmatter Pluto.set_frontmatter!(notebook, nothing) end session.notebooks[notebook.notebook_id] = notebook if execution_allowed && session.options.evaluation.run_notebook_on_load Pluto._report_business_cells_planned!(notebook) end if !execution_allowed Status.delete_business!(notebook.status_tree, :run) Status.delete_business!(notebook.status_tree, :workspace) Status.delete_business!(notebook.status_tree, :pkg) end update_nbpkg_cache!(notebook) update_save_run!(session, notebook, notebook.cells; run_async, prerender_text=true) add(session, notebook; run_async) try_event_call(session, OpenNotebookEvent(notebook)) return notebook end function add(session::ServerSession, notebook::Notebook; run_async::Bool=true) session.notebooks[notebook.notebook_id] = notebook if run_async @asynclog putplutoupdates!(session, clientupdate_notebook_list(session.notebooks)) else putplutoupdates!(session, clientupdate_notebook_list(session.notebooks)) end update_from_file_throttled = let running = Ref(false) function() if !running[] running[] = true @info "Updating from file..." sleep(0.1) ## There seems to be a synchronization issue if your OS is VERYFAST wait_until_file_unchanged(notebook.path, .3) # call update_from_file. If it returns false, that means that the notebook file was corrupt, so we try again, a maximum of 10 times. for _ in 1:10 if update_from_file(session, notebook) break end end @info "Updating from file done!" running[] = false end end end in_session() = get(session.notebooks, notebook.notebook_id, nothing) === notebook session.options.server.auto_reload_from_file && @asynclog try while in_session() if !isfile(notebook.path) # notebook file deleted... let's ignore this, changing the notebook will cause it to save again. Fine for now sleep(2) else e = watch_file(notebook.path, 3) if e.timedout continue end # the above call is blocking until the file changes local modified_time = mtime(notebook.path) local _tries = 0 # mtime might return zero if the file is temporarily removed while modified_time == 0.0 && _tries < 10 modified_time = mtime(notebook.path) _tries += 1 sleep(.05) end # current_time = time() # @info "File changed" (current_time - notebook.last_save_time) (modified_time - notebook.last_save_time) (current_time - modified_time) if !in_session() break end # if current_time - notebook.last_save_time < 2.0 # @info "Notebook was saved by me very recently, not reloading from file." if modified_time == 0.0 # @warn "Failed to hot reload: file no longer exists." elseif modified_time - notebook.last_save_time < session.options.server.auto_reload_from_file_cooldown # @info "Modified time is very close to my last save time, not reloading from file." else update_from_file_throttled() end end end catch e if !(e isa InterruptException) rethrow(e) end end notebook.status_tree.update_listener_ref[] = first(throttled(1.0 / 20) do # TODO: this throttle should be trailing Pluto.send_notebook_changes!(Pluto.ClientRequest(; session, notebook)) end) return notebook end """ Generate a non-existing new notebook filename, and write `contents` to that file. Return the generated filename. # Example ```julia save_upload(some_notebook_data; filename_base="hello") == "~/.julia/pluto_notebooks/hello 5.jl" ``` """ function save_upload(contents::Union{String,Vector{UInt8}}; filename_base::Union{Nothing,AbstractString}=nothing) save_path = numbered_until_new( joinpath( new_notebooks_directory(), something(filename_base, cutename()) ); suffix=".jl") write(save_path, contents) save_path end "Create a new empty notebook inside `session::ServerSession`. Returns the `Notebook`." function new(session::ServerSession; run_async=true, notebook_id::UUID=uuid1()) if session.options.server.init_with_file_viewer @error "DEPRECATED: init_with_file_viewer has been removed." end notebook = if session.options.compiler.sysimage === nothing emptynotebook() else Notebook([Cell("import Pkg"), Cell("# This cell disables Pluto's package manager and activates the global environment. Click on ? inside the bubble next to Pkg.activate to learn more.\n# (added automatically because a sysimage is used)\nPkg.activate()"), Cell()]) end # Run NewNotebookEvent handler before assigning ID isid = try_event_call(session, NewNotebookEvent()) notebook.notebook_id = isnothing(isid) ? notebook_id : isid update_save_run!(session, notebook, notebook.cells; run_async, prerender_text=true) add(session, notebook; run_async) try_event_call(session, OpenNotebookEvent(notebook)) return notebook end "Shut down `notebook` inside `session`. If `keep_in_session` is `false` (default), you will not be allowed to run a notebook with the same notebook_id again." function shutdown(session::ServerSession, notebook::Notebook; keep_in_session::Bool=false, async::Bool=false, verbose::Bool=true) notebook.nbpkg_restart_recommended_msg = nothing notebook.nbpkg_restart_required_msg = nothing if notebook.process_status ∈ (ProcessStatus.ready, ProcessStatus.starting) notebook.process_status = ProcessStatus.no_process end if !keep_in_session listeners = putnotebookupdates!(session, notebook) # TODO: shutdown message delete!(session.notebooks, notebook.notebook_id) putplutoupdates!(session, clientupdate_notebook_list(session.notebooks)) for client in listeners @async close(client.stream) end end WorkspaceManager.unmake_workspace((session, notebook); async, verbose, allow_restart=keep_in_session) try_event_call(session, ShutdownNotebookEvent(notebook)) end function move(session::ServerSession, notebook::Notebook, newpath::String) newpath = tamepath(newpath) if isfile(newpath) error("File exists already - you need to delete the old file manually.") else move_notebook!(notebook, newpath; disable_writing_notebook_files=session.options.server.disable_writing_notebook_files) putplutoupdates!(session, clientupdate_notebook_list(session.notebooks)) WorkspaceManager.cd_workspace((session, notebook), newpath) end end end 7/opt/julia/packages/Pluto/GVuR6/src/webserver/Static.jl import HTTP import Markdown: htmlesc import UUIDs: UUID import Pkg import MIMEs function frontend_directory(; allow_bundled::Bool=true) if allow_bundled && isdir(project_relative_path("frontend-dist")) && (get(ENV, "JULIA_PLUTO_FORCE_BUNDLED", "nein") == "ja" || !is_pluto_dev()) "frontend-dist" else "frontend" end end function should_cache(path::String) dir, filename = splitdir(path) endswith(dir, "frontend-dist") && occursin(r"\.[0-9a-f]{8}\.", filename) end const day = let second = 1 hour = 60second day = 24hour end function default_404(req = nothing) HTTP.Response(404, "Not found!") end function asset_response(path; cacheable::Bool=false) if !isfile(path) && !endswith(path, ".html") return asset_response(path * ".html"; cacheable) end if isfile(path) data = read(path) response = HTTP.Response(200, data) HTTP.setheader(response, "Content-Type" => MIMEs.contenttype_from_mime(MIMEs.mime_from_path(path, MIME"application/octet-stream"()))) HTTP.setheader(response, "Content-Length" => string(length(data))) HTTP.setheader(response, "Access-Control-Allow-Origin" => "*") cacheable && HTTP.setheader(response, "Cache-Control" => "public, max-age=$(30day), immutable") response else default_404() end end function error_response( status_code::Integer, title, advice, body="") template = read(project_relative_path(frontend_directory(), "error.jl.html"), String) body_title = body == "" ? "" : "Error message:" filled_in = replace(replace(replace(replace(replace(template, "\$STYLE" => """"""), "\$TITLE" => title), "\$ADVICE" => advice), "\$BODYTITLE" => body_title), "\$BODY" => htmlesc(body)) response = HTTP.Response(status_code, filled_in) HTTP.setheader(response, "Content-Type" => MIMEs.contenttype_from_mime(MIME"text/html"())) response end function notebook_response(notebook; home_url="./", as_redirect=true) if as_redirect response = HTTP.Response(302, "") HTTP.setheader(response, "Location" => home_url * "edit?id=" * string(notebook.notebook_id)) return response else HTTP.Response(200, string(notebook.notebook_id)) end end const found_is_pluto_dev = Ref{Union{Nothing,Bool}}(nothing) function is_pluto_dev() if found_is_pluto_dev[] !== nothing return found_is_pluto_dev[] end found_is_pluto_dev[] = try deps = Pkg.dependencies() p_index = findfirst(p -> p.name == "Pluto", deps) p = deps[p_index] p.is_tracking_path catch false end end ?/opt/julia/packages/Pluto/GVuR6/src/webserver/Authentication.jl """ Return whether the `request` was authenticated in one of two ways: 1. the session's `secret` was included in the URL as a search parameter, or 2. the session's `secret` was included in a cookie. """ function is_authenticated(session::ServerSession, request::HTTP.Request) ( secret_in_url = try uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) get(query, "secret", "") == session.secret catch e @warn "Failed to authenticate request using URL" exception = (e, catch_backtrace()) false end ) || ( secret_in_cookie = try cookies = HTTP.cookies(request) any(cookies) do cookie cookie.name == "secret" && cookie.value == session.secret end catch e @warn "Failed to authenticate request using cookies" exception = (e, catch_backtrace()) false end ) # that ) || ( kind of looks like Krabs from spongebob end # Function to log the url with secret on the Julia CLI when a request comes to the server without the secret. Executes at most once every 5 seconds const log_secret_throttled = simple_leading_throttle(5) do session::ServerSession, request::HTTP.Request host = HTTP.header(request, "Host") target = request.target url = Text(string(HTTP.URI(HTTP.URI("http://$host/"); query=Dict("secret" => session.secret)))) @info("No longer authenticated? Visit this URL to continue:", url) end function add_set_secret_cookie!(session::ServerSession, response::HTTP.Response) HTTP.setheader(response, "Set-Cookie" => "secret=$(session.secret); SameSite=Strict; HttpOnly") response end # too many layers i know """ Generate a middleware (i.e. a function `HTTP.Handler -> HTTP.Handler`) that stores the `session` in every `request`'s context. """ function create_session_context_middleware(session::ServerSession) function session_context_middleware(handler::Function)::Function function(request::HTTP.Request) request.context[:pluto_session] = session handler(request) end end end session_from_context(request::HTTP.Request) = request.context[:pluto_session]::ServerSession function auth_required(session::ServerSession, request::HTTP.Request) path = HTTP.URI(request.target).path ext = splitext(path)[2] security = session.options.security if path ∈ ("/ping", "/possible_binder_token_please") || ext ∈ (".ico", ".js", ".css", ".png", ".gif", ".svg", ".ico", ".woff2", ".woff", ".ttf", ".eot", ".otf", ".json", ".map") false elseif path ∈ ("", "/") # / does not need security.require_secret_for_open_links, because this is how we handle the configuration where: # require_secret_for_open_links == true # require_secret_for_access == false # # This means that access to all 'risky' endpoints is restricted to authenticated requests (to prevent CSRF), but we allow an unauthenticated request to visit the `/` page and acquire the cookie (see `add_set_secret_cookie!`). # # (By default, `require_secret_for_access` (and `require_secret_for_open_links`) is `true`.) security.require_secret_for_access else security.require_secret_for_access || security.require_secret_for_open_links end end """ auth_middleware(f::HTTP.Handler) -> HTTP.Handler Returns an `HTTP.Handler` (i.e. a function `HTTP.Request → HTTP.Response`) which does three things: 1. Check whether the request is authenticated (by calling `is_authenticated`), if not, return a 403 error. 2. Call your `f(request)` to create the response message. 3. Add a `Set-Cookie` header to the response with the session's `secret`. This is for HTTP requests, the authentication mechanism for WebSockets is separate. """ function auth_middleware(handler) return function (request::HTTP.Request) session = session_from_context(request) required = auth_required(session, request) if !required || is_authenticated(session, request) response = handler(request) if !required filter!(p -> p[1] != "Access-Control-Allow-Origin", response.headers) HTTP.setheader(response, "Access-Control-Allow-Origin" => "*") end if required || HTTP.URI(request.target).path ∈ ("", "/") add_set_secret_cookie!(session, response) end response else log_secret_throttled(session, request) error_response(403, "Not yet authenticated", "Open the link that was printed in the terminal where you launched Pluto. It includes a secret, which is needed to access this server.

If you are running the server yourself and want to change this configuration, have a look at the keyword arguments to Pluto.run.

Please report this error if you did not expect it!") end end end 7/opt/julia/packages/Pluto/GVuR6/src/webserver/Router.jl, function http_router_for(session::ServerSession) router = HTTP.Router(default_404) security = session.options.security function create_serve_onefile(path) return request::HTTP.Request -> asset_response(normpath(path)) end HTTP.register!(router, "GET", "/", create_serve_onefile(project_relative_path(frontend_directory(), "index.html"))) HTTP.register!(router, "GET", "/edit", create_serve_onefile(project_relative_path(frontend_directory(), "editor.html"))) HTTP.register!(router, "GET", "/ping", r -> HTTP.Response(200, "OK!")) HTTP.register!(router, "GET", "/possible_binder_token_please", r -> session.binder_token === nothing ? HTTP.Response(200,"") : HTTP.Response(200, session.binder_token)) function try_launch_notebook_response( action::Function, path_or_url::AbstractString; as_redirect=true, title="", advice="", home_url="./", action_kwargs... ) try nb = action(session, path_or_url; action_kwargs...) notebook_response(nb; home_url, as_redirect) catch e if e isa SessionActions.NotebookIsRunningException notebook_response(e.notebook; home_url, as_redirect) else error_response(500, title, advice, sprint(showerror, e, stacktrace(catch_backtrace()))) end end end function serve_newfile(request::HTTP.Request) notebook_response(SessionActions.new(session); as_redirect=(request.method == "GET")) end HTTP.register!(router, "GET", "/new", serve_newfile) HTTP.register!(router, "POST", "/new", serve_newfile) # This is not in Dynamic.jl because of bookmarks, how HTML works, # real loading bars and the rest; Same for CustomLaunchEvent function serve_openfile(request::HTTP.Request) try uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) as_sample = haskey(query, "as_sample") execution_allowed = haskey(query, "execution_allowed") if haskey(query, "path") path = tamepath(maybe_convert_path_to_wsl(query["path"])) if isfile(path) return try_launch_notebook_response( SessionActions.open, path; execution_allowed, as_redirect=(request.method == "GET"), as_sample, risky_file_source=nothing, title="Failed to load notebook", advice="The file $(htmlesc(path)) could not be loaded. Please report this error!", ) else return error_response(404, "Can't find a file here", "Please check whether $(htmlesc(path)) exists.") end elseif haskey(query, "url") url = query["url"] return try_launch_notebook_response( SessionActions.open_url, url; execution_allowed, as_redirect=(request.method == "GET"), as_sample, risky_file_source=url, title="Failed to load notebook", advice="The notebook from $(htmlesc(url)) could not be loaded. Please report this error!" ) else # You can ask Pluto to handle CustomLaunch events # and do some magic with how you open files. # You are responsible to keep this up to date. # See Events.jl for types and explanation # maybe_notebook_response = try_event_call(session, CustomLaunchEvent(query, request, try_launch_notebook_response)) isnothing(maybe_notebook_response) && return error("Empty request") return maybe_notebook_response end catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/open", serve_openfile) HTTP.register!(router, "POST", "/open", serve_openfile) # normally shutdown is done through Dynamic.jl, with the exception of shutdowns made from the desktop app function serve_shutdown(request::HTTP.Request) notebook = notebook_from_uri(request) SessionActions.shutdown(session, notebook) return HTTP.Response(200) end HTTP.register!(router, "GET", "/shutdown", serve_shutdown) HTTP.register!(router, "POST", "/shutdown", serve_shutdown) # used in desktop app # looks like `/move?id=&newpath=`` function serve_move(request::HTTP.Request) uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) notebook = notebook_from_uri(request) newpath = query["newpath"] try SessionActions.move(session, notebook, newpath) HTTP.Response(200, notebook.path) catch e error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/move", serve_move) HTTP.register!(router, "POST", "/move", serve_move) function serve_notebooklist(request::HTTP.Request) return HTTP.Response(200, pack(Dict(k => v.path for (k, v) in session.notebooks))) end HTTP.register!(router, "GET", "/notebooklist", serve_notebooklist) function serve_sample(request::HTTP.Request) uri = HTTP.URI(request.target) sample_filename = split(HTTP.unescapeuri(uri.path), "sample/")[2] sample_path = project_relative_path("sample", sample_filename) try_launch_notebook_response( SessionActions.open, sample_path; as_redirect=(request.method == "GET"), home_url="../", as_sample=true, title="Failed to load sample", advice="Please report this error!" ) end HTTP.register!(router, "GET", "/sample/*", serve_sample) HTTP.register!(router, "POST","/sample/*", serve_sample) notebook_from_uri(request) = let uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) id = UUID(query["id"]) session.notebooks[id] end function serve_notebookfile(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, sprint(save_notebook, notebook)) HTTP.setheader(response, "Content-Type" => "text/julia; charset=utf-8") HTTP.setheader(response, "Content-Disposition" => "inline; filename=\"$(basename(notebook.path))\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/notebookfile", serve_notebookfile) function serve_statefile(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, Pluto.pack(Pluto.notebook_to_js(notebook))) HTTP.setheader(response, "Content-Type" => "application/octet-stream") HTTP.setheader(response, "Content-Disposition" => "attachment; filename=\"$(without_pluto_file_extension(basename(notebook.path))).plutostate\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/statefile", serve_statefile) function serve_notebookexport(request::HTTP.Request) try notebook = notebook_from_uri(request) response = HTTP.Response(200, generate_html(notebook)) HTTP.setheader(response, "Content-Type" => "text/html; charset=utf-8") HTTP.setheader(response, "Content-Disposition" => "attachment; filename=\"$(without_pluto_file_extension(basename(notebook.path))).html\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) end end HTTP.register!(router, "GET", "/notebookexport", serve_notebookexport) function serve_notebookupload(request::HTTP.Request) uri = HTTP.URI(request.target) query = HTTP.queryparams(uri) save_path = SessionActions.save_upload(request.body; filename_base=get(query, "name", nothing)) try_launch_notebook_response( SessionActions.open, save_path; as_redirect=false, as_sample=false, execution_allowed=haskey(query, "execution_allowed"), clear_frontmatter=haskey(query, "clear_frontmatter"), title="Failed to load notebook", advice="The contents could not be read as a Pluto notebook file. When copying contents from somewhere else, make sure that you copy the entire notebook file. You can also report this error!" ) end HTTP.register!(router, "POST", "/notebookupload", serve_notebookupload) function serve_asset(request::HTTP.Request) uri = HTTP.URI(request.target) filepath = project_relative_path(frontend_directory(), relpath(HTTP.unescapeuri(uri.path), "/")) asset_response(filepath; cacheable=should_cache(filepath)) end HTTP.register!(router, "GET", "/**", serve_asset) HTTP.register!(router, "GET", "/favicon.ico", create_serve_onefile(project_relative_path(frontend_directory(allow_bundled=false), "img", "favicon.ico"))) return scoped_router(session.options.server.base_url, router) end """ scoped_router(base_url::String, base_router::HTTP.Router)::HTTP.Router Returns a new `HTTP.Router` which delegates all requests to `base_router` but with requests trimmed so that they seem like they arrived at `/**` instead of `/\$base_url/**`. """ function scoped_router(base_url, base_router) base_url == "/" && return base_router @assert startswith(base_url, '/') "base_url \"$base_url\" should start with a '/'" @assert endswith(base_url, '/') "base_url \"$base_url\" should end with a '/'" @assert !occursin('*', base_url) "'*' not allowed in base_url \"$base_url\" " function handler(request) request.target = request.target[length(base_url):end] return base_router(request) end router = HTTP.Router(base_router._404, base_router._405) HTTP.register!(router, base_url * "**", handler) HTTP.register!(router, base_url, handler) return router end 8/opt/julia/packages/Pluto/GVuR6/src/webserver/Dynamic.jlbimport UUIDs: uuid1 import .PkgCompat import .Status "Will hold all 'response handlers': functions that respond to a WebSocket request from the client." const responses = Dict{Symbol,Function}() Base.@kwdef struct ClientRequest session::ServerSession notebook::Union{Nothing,Notebook} body::Any=nothing initiator::Union{Initiator,Nothing}=nothing end require_notebook(r::ClientRequest) = if r.notebook === nothing throw(ArgumentError("Notebook request called without a notebook 😗")) end ### # RESPONDING TO A NOTEBOOK STATE UPDATE ### """ ## State management in Pluto *Aka: how do the server and clients stay in sync?* A Pluto notebook session has *state*: with this, we mean: 1. The input and ouput of each cell, the cell order, and more metadata about the notebook and cells [^state] This state needs to be **synchronised between the server and all clients** (we support multiple synchronised clients), and note that: - Either side wants to update the state. Generally, a client will update cell inputs, the server will update cell outputs. - Both sides want to *react* to state updates - The server is in Julia, the clients are in JS - This is built on top of our websocket+msgpack connection, but that doesn't matter too much We do this by implementing something similar to how you use Google Firebase: there is **one shared state object, any party can mutate it, and it will synchronise to all others automatically**. The state object is a nested structure of mutable `Dict`s, with immutable ints, strings, bools, arrays, etc at the endpoints. Some cool things are: - Our system uses object diffing, so only *changes to the state* are actually tranferred over the network. But you can use it as if the entire state is sent around constantly. - In the frontend, the *shared state* is part of the *react state*, i.e. shared state updates automatically trigger visual updates. - Within the client, state changes take effect instantly, without waiting for a round trip to the server. This means that when you add a cell, it shows up instantly. Diffing is done using `immer.js` (frontend) and `src/webserver/Firebasey.jl` (server). We wrote Firebasey ourselves to match immer's functionality, and the cool thing is: **it is a Pluto notebook**! Since Pluto notebooks are `.jl` files, we can just `include` it in our module. The shared state object is generated by [`notebook_to_js`](@ref). Take a look! The Julia server orchestrates this firebasey stuff. For this, we keep a **copy** of the latest state of each client on the server (see [`current_state_for_clients`](@ref)). When anything changes to the Julia state (e.g. when a cell finished running), we call [`send_notebook_changes!`](@ref), which will call [`notebook_to_js`](@ref) to compute the new desired state object. For each client, we diff the new state to their last known state, and send them the difference. ### Responding to changes made by a client When a client updates the shared state object, we want the server to *react* to that change by taking an action. Which action to take depends on which field changes. For example, when `state["path"]` changes, we should rename the notebook file. When `state["cell_inputs"][a_cell_id]["code"]` changes, we should reparse and analyze that cel, etc. This location of the change, e.g. `"cell_inputs//code"` is called the *path* of the change. [`effects_of_changed_state`](@ref) define these pattern-matchers. We use a `Wildcard()` to take the place of *any* key, see [`Wildcard`](@ref), and we use the change/update/patch inside the given function. ### Not everything uses the shared state (yet) Besides `:update_notebook`, you will find more functions in [`responses`](@ref) that respond to classic 'client requests', such as `:reshow_cell` and `:shutdown_notebook`. Some of these requests get a direct response, like the list of autocomplete options to a `:complete` request (in `src/webserver/REPLTools.jl`). On the javascript side, these direct responses can be `awaited`, because every message has a unique ID. [^state]: Two other meanings of _state_ could be: 2. The reactivity data: the parsed AST (`Expr`) of each cell, which variables are defined or referenced by which cells, in what order will cells run? 3. The state of the Julia process: i.e. which variables are defined, which packages are imported, etc. The first two (1 & 2) are stored in a [`Notebook`](@ref) struct, remembered by the server process (Julia). (In fact, (2) is entirely described by (1), but we store it for performance reasons.) I included (3) for completeness, but it is not stored by us, we hope to control and minimize (3) by keeping track of (1) and (2). """ module Firebasey include("./Firebasey.jl") end module FirebaseyUtils # I put Firebasey here manually THANKS JULIA import ..Firebasey include("./FirebaseyUtils.jl") end # All of the arrays in the notebook_to_js object are 'immutable' (we write code as if they are), so we can enable this optimization: Firebasey.use_triple_equals_for_arrays[] = true # the only possible Arrays are: # - cell_order # - cell_execution_order # - cell_result > * > output > body # - bonds > * > value > * # - cell_dependencies > * > downstream_cells_map > * > # - cell_dependencies > * > upstream_cells_map > * > function notebook_to_js(notebook::Notebook) Dict{String,Any}( "pluto_version" => PLUTO_VERSION_STR, "notebook_id" => notebook.notebook_id, "path" => notebook.path, "shortpath" => basename(notebook.path), "in_temp_dir" => startswith(notebook.path, new_notebooks_directory()), "process_status" => notebook.process_status, "last_save_time" => notebook.last_save_time, "last_hot_reload_time" => notebook.last_hot_reload_time, "cell_inputs" => Dict{UUID,Dict{String,Any}}( id => Dict{String,Any}( "cell_id" => cell.cell_id, "code" => cell.code, "code_folded" => cell.code_folded, "metadata" => cell.metadata, ) for (id, cell) in notebook.cells_dict), "cell_dependencies" => Dict{UUID,Dict{String,Any}}( id => Dict{String,Any}( "cell_id" => cell.cell_id, "downstream_cells_map" => Dict{String,Vector{UUID}}( String(s) => cell_id.(r) for (s, r) in cell.cell_dependencies.downstream_cells_map ), "upstream_cells_map" => Dict{String,Vector{UUID}}( String(s) => cell_id.(r) for (s, r) in cell.cell_dependencies.upstream_cells_map ), "precedence_heuristic" => cell.cell_dependencies.precedence_heuristic, ) for (id, cell) in notebook.cells_dict), "cell_results" => Dict{UUID,Dict{String,Any}}( id => Dict{String,Any}( "cell_id" => cell.cell_id, "depends_on_disabled_cells" => cell.depends_on_disabled_cells, "output" => FirebaseyUtils.ImmutableMarker(cell.output), "published_object_keys" => collect(keys(cell.published_objects)), "queued" => cell.queued, "running" => cell.running, "errored" => cell.errored, "runtime" => cell.runtime, "logs" => FirebaseyUtils.AppendonlyMarker(cell.logs), "depends_on_skipped_cells" => cell.depends_on_skipped_cells, ) for (id, cell) in notebook.cells_dict), "cell_order" => notebook.cell_order, "published_objects" => merge!(Dict{String,Any}(), (c.published_objects for c in values(notebook.cells_dict))...), "bonds" => Dict{String,Dict{String,Any}}( String(key) => Dict{String,Any}( "value" => bondvalue.value, ) for (key, bondvalue) in notebook.bonds), "metadata" => notebook.metadata, "nbpkg" => let ctx = notebook.nbpkg_ctx Dict{String,Any}( "enabled" => ctx !== nothing, "waiting_for_permission" => notebook.process_status === ProcessStatus.waiting_for_permission, "waiting_for_permission_but_probably_disabled" => notebook.process_status === ProcessStatus.waiting_for_permission && !use_plutopkg(notebook.topology), "restart_recommended_msg" => notebook.nbpkg_restart_recommended_msg, "restart_required_msg" => notebook.nbpkg_restart_required_msg, "installed_versions" => ctx === nothing ? Dict{String,String}() : notebook.nbpkg_installed_versions_cache, "terminal_outputs" => notebook.nbpkg_terminal_outputs, "install_time_ns" => notebook.nbpkg_install_time_ns, "busy_packages" => notebook.nbpkg_busy_packages, "instantiated" => notebook.nbpkg_ctx_instantiated, ) end, "status_tree" => Status.tojs(notebook.status_tree), "cell_execution_order" => cell_id.(collect(topological_order(notebook))), ) end """ For each connected client, we keep a copy of their current state. This way we know exactly which updates to send when the server-side state changes. """ const current_state_for_clients = WeakKeyDict{ClientSession,Any}() const current_state_for_clients_lock = ReentrantLock() """ Update the local state of all clients connected to this notebook. """ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing, skip_send::Bool=false) outbox = Set{Tuple{ClientSession,UpdateMessage}}() lock(current_state_for_clients_lock) do notebook_dict = notebook_to_js(🙋.notebook) for (_, client) in 🙋.session.connected_clients if client.connected_notebook !== nothing && client.connected_notebook.notebook_id == 🙋.notebook.notebook_id current_dict = get(current_state_for_clients, client, :empty) patches = Firebasey.diff(current_dict, notebook_dict) patches_as_dicts = Firebasey._convert(Vector{Dict}, patches) current_state_for_clients[client] = deep_enough_copy(notebook_dict) # Make sure we do send a confirmation to the client who made the request, even without changes is_response = 🙋.initiator !== nothing && client == 🙋.initiator.client if !skip_send && (!isempty(patches) || is_response) response = Dict( :patches => patches_as_dicts, :response => is_response ? commentary : nothing ) push!(outbox, (client, UpdateMessage(:notebook_diff, response, 🙋.notebook, nothing, 🙋.initiator))) end end end end for (client, msg) in outbox putclientupdates!(client, msg) end try_event_call(🙋.session, FileEditEvent(🙋.notebook)) end "Like `deepcopy`, but anything onther than `Dict` gets a shallow (reference) copy." function deep_enough_copy(d::Dict{A,B}) where {A, B} Dict{A,B}( k => deep_enough_copy(v) for (k, v) in d ) end deep_enough_copy(x) = x """ A placeholder path. The path elements that it replaced will be given to the function as arguments. """ struct Wildcard end abstract type Changed end struct CodeChanged <: Changed end struct FileChanged <: Changed end struct BondChanged <: Changed bond_name::Symbol is_first_value::Bool end # to support push!(x, y...) # with y = [] Base.push!(x::Set{Changed}) = x const no_changes = Changed[] const effects_of_changed_state = Dict( "path" => function(; request::ClientRequest, patch::Firebasey.ReplacePatch) SessionActions.move(request.session, request.notebook, patch.value) return no_changes end, "process_status" => function(; request::ClientRequest, patch::Firebasey.ReplacePatch) newstatus = patch.value @info "Process status set by client" newstatus end, # "execution_allowed" => function(; request::ClientRequest, patch::Firebasey.ReplacePatch) # Firebasey.applypatch!(request.notebook, patch) # newstatus = patch.value # @info "execution_allowed set by client" newstatus # if newstatus # @info "lets run some cells!" # update_save_run!(request.session, request.notebook, notebook.cells; # run_async=true, save=true # ) # end # end, "in_temp_dir" => function(; _...) no_changes end, "cell_inputs" => Dict( Wildcard() => function(cell_id, rest...; request::ClientRequest, patch::Firebasey.JSONPatch) Firebasey.applypatch!(request.notebook, patch) if length(rest) == 0 [CodeChanged(), FileChanged()] elseif length(rest) == 1 && Symbol(rest[1]) == :code [CodeChanged(), FileChanged()] else [FileChanged()] end end, ), "cell_order" => function(; request::ClientRequest, patch::Firebasey.ReplacePatch) Firebasey.applypatch!(request.notebook, patch) [FileChanged()] end, "bonds" => Dict( Wildcard() => function(name; request::ClientRequest, patch::Firebasey.JSONPatch) name = Symbol(name) Firebasey.applypatch!(request.notebook, patch) [BondChanged(name, patch isa Firebasey.AddPatch)] end, ), "metadata" => Dict( Wildcard() => function(property; request::ClientRequest, patch::Firebasey.JSONPatch) Firebasey.applypatch!(request.notebook, patch) [FileChanged()] end ) ) responses[:update_notebook] = function response_update_notebook(🙋::ClientRequest) require_notebook(🙋) try notebook = 🙋.notebook patches = (Base.convert(Firebasey.JSONPatch, update) for update in 🙋.body["updates"]) if length(patches) == 0 send_notebook_changes!(🙋) return nothing end if !haskey(current_state_for_clients, 🙋.initiator.client) throw(ErrorException("Updating without having a first version of the notebook??")) end # TODO Immutable ?? for patch in patches Firebasey.applypatch!(current_state_for_clients[🙋.initiator.client], patch) end changes = Set{Changed}() for patch in patches (mutator, matches, rest) = trigger_resolver(effects_of_changed_state, patch.path) current_changes = if isempty(rest) && applicable(mutator, matches...) mutator(matches...; request=🙋, patch) else mutator(matches..., rest...; request=🙋, patch) end union!(changes, current_changes) end # We put a flag to check whether any patch changes the skip_as_script metadata. This is to eventually trigger a notebook updated if no reactive_run is part of this update skip_as_script_changed = any(patches) do patch path = patch.path metadata_idx = findfirst(isequal("metadata"), path) if metadata_idx === nothing false else isequal(path[metadata_idx+1], "skip_as_script") end end # If CodeChanged ∈ changes, then the client will also send a request like run_multiple_cells, which will trigger a file save _before_ running the cells. # In the future, we should get rid of that request, and save the file here. For now, we don't save the file here, to prevent unnecessary file IO. # (You can put a log in save_notebook to track how often the file is saved) if FileChanged() ∈ changes && CodeChanged() ∉ changes if skip_as_script_changed # If skip_as_script has changed but no cell run is happening we want to update the notebook dependency here before saving the file update_skipped_cells_dependency!(notebook) end save_notebook(🙋.session, notebook) end let bond_changes = filter(x -> x isa BondChanged, changes) bound_sym_names = Symbol[x.bond_name for x in bond_changes] is_first_values = Bool[x.is_first_value for x in bond_changes] set_bond_values_reactive(; session=🙋.session, notebook=🙋.notebook, bound_sym_names=bound_sym_names, is_first_values=is_first_values, run_async=true, initiator=🙋.initiator, ) end send_notebook_changes!(🙋; commentary=Dict(:update_went_well => :👍)) catch ex @error "Update notebook failed" 🙋.body["updates"] exception=(ex, stacktrace(catch_backtrace())) response = Dict( :update_went_well => :👎, :why_not => sprint(showerror, ex), :should_i_tell_the_user => ex isa SessionActions.UserError, ) send_notebook_changes!(🙋; commentary=response) end end function trigger_resolver(anything, path, values=[]) (value=anything, matches=values, rest=path) end function trigger_resolver(resolvers::Dict, path, values=[]) if isempty(path) throw(BoundsError("resolver path ends at Dict with keys $(keys(resolvers))")) end segment, rest... = path if haskey(resolvers, segment) trigger_resolver(resolvers[segment], rest, values) elseif haskey(resolvers, Wildcard()) trigger_resolver(resolvers[Wildcard()], rest, (values..., segment)) else throw(BoundsError("failed to match path $(path), possible keys $(keys(resolvers))")) end end ### # MISC RESPONSES ### responses[:current_time] = function response_current_time(🙋::ClientRequest) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:current_time, Dict(:time => time()), nothing, nothing, 🙋.initiator)) end responses[:connect] = function response_connect(🙋::ClientRequest) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:👋, Dict( :notebook_exists => (🙋.notebook !== nothing), :options => 🙋.session.options, :version_info => Dict( :pluto => PLUTO_VERSION_STR, :julia => JULIA_VERSION_STR, :dismiss_update_notification => 🙋.session.options.server.dismiss_update_notification, ), ), nothing, nothing, 🙋.initiator)) end responses[:ping] = function response_ping(🙋::ClientRequest) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:pong, Dict(), nothing, nothing, 🙋.initiator)) end responses[:reset_shared_state] = function response_reset_shared_state(🙋::ClientRequest) delete!(current_state_for_clients, 🙋.initiator.client) send_notebook_changes!(🙋; commentary=Dict(:from_reset => true)) end responses[:run_multiple_cells] = function response_run_multiple_cells(🙋::ClientRequest) require_notebook(🙋) uuids = UUID.(🙋.body["cells"]) cells = map(uuids) do uuid 🙋.notebook.cells_dict[uuid] end if will_run_code(🙋.notebook) foreach(c -> c.queued = true, cells) # run send_notebook_changes! without actually sending it, to update current_state_for_clients for our client with c.queued = true. # later, during update_save_run!, the cell will actually run, eventually setting c.queued = false again, which will be sent to the client through a patch update. # We *need* to send *something* to the client, because of https://github.com/fonsp/Pluto.jl/pull/1892, but we also don't want to send unnecessary updates. We can skip sending this update, because update_save_run! will trigger a send_notebook_changes! very very soon. send_notebook_changes!(🙋; skip_send=true) end function on_auto_solve_multiple_defs(disabled_cells_dict) response = Dict{Symbol,Any}( :disabled_cells => Dict{UUID,Any}(cell_id(k) => v for (k,v) in disabled_cells_dict), ) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:run_feedback, response, 🙋.notebook, nothing, 🙋.initiator)) end wfp = 🙋.notebook.process_status == ProcessStatus.waiting_for_permission update_save_run!(🙋.session, 🙋.notebook, cells; run_async=true, save=true, auto_solve_multiple_defs=true, on_auto_solve_multiple_defs, # special case: just render md cells in "Safe preview" mode prerender_text=wfp, clear_not_prerenderable_cells=wfp, ) end responses[:get_all_notebooks] = function response_get_all_notebooks(🙋::ClientRequest) putplutoupdates!(🙋.session, clientupdate_notebook_list(🙋.session.notebooks, initiator=🙋.initiator)) end responses[:interrupt_all] = function response_interrupt_all(🙋::ClientRequest) require_notebook(🙋) session_notebook = (🙋.session, 🙋.notebook) workspace = WorkspaceManager.get_workspace(session_notebook; allow_creation=false) already_interrupting = 🙋.notebook.wants_to_interrupt anything_running = !isready(workspace.dowork_token) if !already_interrupting && anything_running 🙋.notebook.wants_to_interrupt = true WorkspaceManager.interrupt_workspace(session_notebook) end # TODO: notify user whether interrupt was successful end responses[:shutdown_notebook] = function response_shutdown_notebook(🙋::ClientRequest) require_notebook(🙋) SessionActions.shutdown(🙋.session, 🙋.notebook; keep_in_session=🙋.body["keep_in_session"]) end without_initiator(🙋::ClientRequest) = ClientRequest(session=🙋.session, notebook=🙋.notebook) responses[:restart_process] = function response_restart_process(🙋::ClientRequest; run_async::Bool=true) require_notebook(🙋) if 🙋.notebook.process_status != ProcessStatus.waiting_to_restart 🙋.notebook.process_status = ProcessStatus.waiting_to_restart 🙋.session.options.evaluation.run_notebook_on_load && _report_business_cells_planned!(🙋.notebook) send_notebook_changes!(🙋 |> without_initiator) # TODO skip necessary? SessionActions.shutdown(🙋.session, 🙋.notebook; keep_in_session=true, async=true) 🙋.notebook.process_status = ProcessStatus.starting send_notebook_changes!(🙋 |> without_initiator) update_save_run!(🙋.session, 🙋.notebook, 🙋.notebook.cells; run_async=run_async, save=true) end end responses[:reshow_cell] = function response_reshow_cell(🙋::ClientRequest) require_notebook(🙋) @assert will_run_code(🙋.notebook) cell = let cell_id = UUID(🙋.body["cell_id"]) 🙋.notebook.cells_dict[cell_id] end run = WorkspaceManager.format_fetch_in_workspace( (🙋.session, 🙋.notebook), cell.cell_id, ends_with_semicolon(cell.code), collect(keys(cell.published_objects)), (parse(PlutoRunner.ObjectID, 🙋.body["objectid"], base=16), convert(Int64, 🙋.body["dim"])), ) set_output!(cell, run, ExprAnalysisCache(🙋.notebook.topology.codes[cell]); persist_js_state=true) # send to all clients, why not send_notebook_changes!(🙋 |> without_initiator) end responses[:request_js_link_response] = function response_request_js_link_response(🙋::ClientRequest) require_notebook(🙋) @assert will_run_code(🙋.notebook) Threads.@spawn try result = WorkspaceManager.eval_fetch_in_workspace( (🙋.session, 🙋.notebook), quote PlutoRunner.evaluate_js_link( $(🙋.notebook.notebook_id), $(UUID(🙋.body["cell_id"])), $(🙋.body["link_id"]), $(🙋.body["input"]), ) end ) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🐤, result, nothing, nothing, 🙋.initiator)) catch ex @error "Error in request_js_link_response" exception=(ex, stacktrace(catch_backtrace())) end end responses[:nbpkg_available_versions] = function response_nbpkg_available_versions(🙋::ClientRequest) # require_notebook(🙋) all_versions = PkgCompat.package_versions(🙋.body["package_name"]) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🍕, Dict( :versions => string.(all_versions), ), nothing, nothing, 🙋.initiator)) end responses[:package_completions] = function response_package_completions(🙋::ClientRequest) results = PkgCompat.package_completions(🙋.body["query"]) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🍳, Dict( :results => results, ), nothing, nothing, 🙋.initiator)) end responses[:pkg_update] = function response_pkg_update(🙋::ClientRequest) require_notebook(🙋) update_nbpkg(🙋.session, 🙋.notebook) putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🦆, Dict(), nothing, nothing, 🙋.initiator)) end :/opt/julia/packages/Pluto/GVuR6/src/webserver/Firebasey.jl### A Pluto.jl notebook ### # v0.19.12 using Markdown using InteractiveUtils # ╔═╡ e748600a-2de1-11eb-24be-d5f0ecab8fa4 # ╠═╡ show_logs = false # ╠═╡ skip_as_script = true #=╠═╡ # Only define this in Pluto - assume we are `using Test` otherwise begin import Pkg Pkg.activate(mktempdir()) Pkg.add(Pkg.PackageSpec(name="PlutoTest")) using PlutoTest end ╠═╡ =# # ╔═╡ 3e07f976-6cd0-4841-9762-d40337bb0645 # ╠═╡ skip_as_script = true #=╠═╡ using Markdown: @md_str ╠═╡ =# # ╔═╡ d948dc6e-2de1-11eb-19e7-cb3bb66353b6 # ╠═╡ skip_as_script = true #=╠═╡ md"# Diffing" ╠═╡ =# # ╔═╡ 1a6e1853-6db1-4074-bce0-5f274351cece # ╠═╡ skip_as_script = true #=╠═╡ md""" We define a _diffing system_ for Julia `Dict`s, which is analogous to the diffing system of immer.js. This notebook is part of Pluto's source code (included in `src/webserver/Dynamic.jl`). """ ╠═╡ =# # ╔═╡ 49fc1f97-3b8f-4297-94e5-2e24c001d35c # ╠═╡ skip_as_script = true #=╠═╡ md""" ## Example Computing a diff: """ ╠═╡ =# # ╔═╡ d8e73b90-24c5-4e50-830b-b1dbe6224c8e # ╠═╡ skip_as_script = true #=╠═╡ dict_1 = Dict{String,Any}( "a" => 1, "b" => Dict( "c" => [3,4], "d" => 99, ), "e" => "hello!" ); ╠═╡ =# # ╔═╡ 19646596-b35b-44fa-bfcf-891f9ffb748c # ╠═╡ skip_as_script = true #=╠═╡ dict_2 = Dict{String,Any}( "a" => 1, "b" => Dict( "c" => [3,4,5], "d" => 99, "🏝" => "👍", ), ); ╠═╡ =# # ╔═╡ 9d2c07d9-16a9-4b9f-a375-2adb6e5b907a # ╠═╡ skip_as_script = true #=╠═╡ md""" Applying a set of patches: """ ╠═╡ =# # ╔═╡ 336bfd4f-8a8e-4a2d-be08-ee48d6a9f747 # ╠═╡ skip_as_script = true #=╠═╡ md""" ## JSONPatch objects """ ╠═╡ =# # ╔═╡ db116c0a-2de1-11eb-2a56-872af797c547 abstract type JSONPatch end # ╔═╡ bd0d46bb-3e58-4522-bae0-83eb799196c4 const PatchPath = Vector # ╔═╡ db2d8a3e-2de1-11eb-02b8-9ffbfaeff61c struct AddPatch <: JSONPatch path::PatchPath value::Any end # ╔═╡ ffe9b3d9-8e35-4a31-bab2-8787a4140594 struct RemovePatch <: JSONPatch path::PatchPath end # ╔═╡ 894de8a7-2757-4d7a-a2be-1069fa872911 struct ReplacePatch <: JSONPatch path::PatchPath value::Any end # ╔═╡ 9a364714-edb1-4bca-9387-a8bbacccd10d struct CopyPatch <: JSONPatch path::PatchPath from::PatchPath end # ╔═╡ 9321d3be-cb91-4406-9dc7-e5c38f7d377c struct MovePatch <: JSONPatch path::PatchPath from::PatchPath end # ╔═╡ 73631aea-5e93-4da2-a32d-649029660d4e const Patches = Vector{JSONPatch} # ╔═╡ 0fd3e910-abcc-4421-9d0b-5cfb90034338 const NoChanges = Patches() # ╔═╡ aad7ab32-eecf-4aad-883d-1c802cad6c0c # ╠═╡ skip_as_script = true #=╠═╡ md"### ==" ╠═╡ =# # ╔═╡ 732fd744-acdb-4507-b1de-6866ec5563dd Base.hash(a::AddPatch) = hash([AddPatch, a.value, a.path]) # ╔═╡ 17606cf6-2d0f-4245-89a3-746ad818a664 Base.hash(a::RemovePatch) = hash([RemovePatch, a.path]) # ╔═╡ c7ac7d27-7bf9-4209-8f3c-e4d52c543e29 Base.hash(a::ReplacePatch) = hash([ReplacePatch, a.value, a.path]) # ╔═╡ 042f7788-e996-430e-886d-ffb4f70dea9e Base.hash(a::CopyPatch) = hash([CopyPatch, a.from, a.path]) # ╔═╡ 9d2dde5c-d404-4fbc-b8e0-5024303c8052 Base.hash(a::MovePatch) = hash([MovePatch, a.from, a.path]) # ╔═╡ f649f67c-aab0-4d35-a799-f398e5f3ecc4 function Base.:(==)(a::AddPatch, b::AddPatch) a.value == b.value && a.path == b.path end # ╔═╡ 63087738-d70c-46f5-b072-21cd8953df35 function Base.:(==)(a::RemovePatch, b::RemovePatch) a.path == b.path end # ╔═╡ aa81974a-7254-45e0-9bfe-840c4793147f function Base.:(==)(a::ReplacePatch, b::ReplacePatch) a.path == b.path && a.value == b.value end # ╔═╡ 31188a03-76ba-40cf-a333-4d339ce37711 function Base.:(==)(a::CopyPatch, b::CopyPatch) a.path == b.path && a.from == b.from end # ╔═╡ 7524a9e8-1a6d-4851-b50e-19415f25a84b function Base.:(==)(a::MovePatch, b::MovePatch) a.path == b.path && a.from == b.from end # ╔═╡ 5ddfd616-db20-451b-bc1e-2ad52e0e2777 #=╠═╡ @test Base.hash(ReplacePatch(["asd"], Dict("a" => 2))) == Base.hash(ReplacePatch(["asd"], Dict("a" => 2))) ╠═╡ =# # ╔═╡ 24e93923-eab9-4a7b-9bc7-8d8a1209a78f #=╠═╡ @test ReplacePatch(["asd"], Dict("a" => 2)) == ReplacePatch(["asd"], Dict("a" => 2)) ╠═╡ =# # ╔═╡ 09ddf4d9-5ccb-4530-bfab-d11b864e872a #=╠═╡ @test Base.hash(RemovePatch(["asd"])) == Base.hash(RemovePatch(["asd"])) ╠═╡ =# # ╔═╡ d9e764db-94fc-44f7-8c2e-3d63f4809617 #=╠═╡ @test RemovePatch(["asd"]) == RemovePatch(["asd"]) ╠═╡ =# # ╔═╡ 99df99ad-aad5-4275-97d4-d1ceeb2f8d15 #=╠═╡ @test Base.hash(RemovePatch(["aasd"])) != Base.hash(RemovePatch(["asd"])) ╠═╡ =# # ╔═╡ 2d665639-7274-495a-ae9d-f358a8219bb7 #=╠═╡ @test Base.hash(ReplacePatch(["asd"], Dict("a" => 2))) != Base.hash(AddPatch(["asd"], Dict("a" => 2))) ╠═╡ =# # ╔═╡ f658a72d-871d-49b3-9b73-7efedafbd7a6 # ╠═╡ skip_as_script = true #=╠═╡ md"### convert(::Type{Dict}, ::JSONPatch)" ╠═╡ =# # ╔═╡ 230bafe2-aaa7-48f0-9fd1-b53956281684 function Base.convert(::Type{Dict}, patch::AddPatch) Dict{String,Any}("op" => "add", "path" => patch.path, "value" => patch.value) end # ╔═╡ b48e2c08-a94a-4247-877d-949d92dde626 function Base.convert(::Type{Dict}, patch::RemovePatch) Dict{String,Any}("op" => "remove", "path" => patch.path) end # ╔═╡ 921a130e-b028-4f91-b077-3bd79dcb6c6d function Base.convert(::Type{JSONPatch}, patch_dict::Dict) op = patch_dict["op"] if op == "add" AddPatch(patch_dict["path"], patch_dict["value"]) elseif op == "remove" RemovePatch(patch_dict["path"]) elseif op == "replace" ReplacePatch(patch_dict["path"], patch_dict["value"]) else throw(ArgumentError("Unknown operation :$(patch_dict["op"]) in Dict to JSONPatch conversion")) end end # ╔═╡ 07eeb122-6706-4544-a007-1c8d6581eec8 # ╠═╡ skip_as_script = true #=╠═╡ Base.convert(Dict, AddPatch([:x, :y], 10)) ╠═╡ =# # ╔═╡ c59b30b9-f702-41f1-bb2e-1736c8cd5ede # ╠═╡ skip_as_script = true #=╠═╡ Base.convert(Dict, RemovePatch([:x, :y])) ╠═╡ =# # ╔═╡ 6d67f8a5-0e0c-4b6e-a267-96b34d580946 # ╠═╡ skip_as_script = true #=╠═╡ add_patch = AddPatch(["counter"], 10) ╠═╡ =# # ╔═╡ 56b28842-4a67-44d7-95e7-55d457a44fb1 # ╠═╡ skip_as_script = true #=╠═╡ remove_patch = RemovePatch(["counter"]) ╠═╡ =# # ╔═╡ f10e31c0-1d2c-4727-aba5-dd676a10041b # ╠═╡ skip_as_script = true #=╠═╡ replace_patch = ReplacePatch(["counter"], 10) ╠═╡ =# # ╔═╡ 3a99e22d-42d6-4b2d-9381-022b41b0e852 # ╠═╡ skip_as_script = true #=╠═╡ md"### wrappath" ╠═╡ =# # ╔═╡ 831d84a6-1c71-4e68-8c7c-27d9093a82c4 function wrappath(path::PatchPath, patches::Vector{JSONPatch}) map(patches) do patch wrappath(path, patch) end end # ╔═╡ 2ad11c73-4691-4283-8f98-3d2a87926b99 function wrappath(path, patch::AddPatch) AddPatch([path..., patch.path...], patch.value) end # ╔═╡ 5513ea3b-9498-426c-98cb-7dc23d32f72e function wrappath(path, patch::RemovePatch) RemovePatch([path..., patch.path...]) end # ╔═╡ 0c2d6da1-cad3-4c9f-93e9-922457083945 function wrappath(path, patch::ReplacePatch) ReplacePatch([path..., patch.path...], patch.value) end # ╔═╡ 84c87031-7733-4d1f-aa90-f8ab71506251 function wrappath(path, patch::CopyPatch) CopyPatch([path..., patch.path...], patch.from) end # ╔═╡ 8f265a33-3a2d-4508-9477-ca62e8ce3c12 function wrappath(path, patch::MovePatch) MovePatch([path..., patch.path...], patch.from) end # ╔═╡ daf9ec12-2de1-11eb-3a8d-59d9c2753134 # ╠═╡ skip_as_script = true #=╠═╡ md"## Diff" ╠═╡ =# # ╔═╡ 0b50f6b2-8e85-4565-9f04-f99c913b4592 const use_triple_equals_for_arrays = Ref(false) # ╔═╡ 59e94cb2-c2f9-4f6c-9562-45e8c15931af function diff(old::T, new::T) where T <: AbstractArray if use_triple_equals_for_arrays[] ? ((old === new) || (old == new)) : (old == new) NoChanges else JSONPatch[ReplacePatch([], new)] end end # ╔═╡ c9d5d81c-b0b6-4d1a-b1de-96d3b3701700 function diff(old::T, new::T) where T if old == new NoChanges else JSONPatch[ReplacePatch([], new)] end end # ╔═╡ 24389a0a-c3ac-4438-9dfe-1d14cd033d25 diff(::Missing, ::Missing) = NoChanges # ╔═╡ 9cbaaec2-709c-4769-886c-ec92b12c18bc struct Deep{T} value::T end # ╔═╡ db75df12-2de1-11eb-0726-d1995cebd382 function diff(old::Deep{T}, new::Deep{T}) where T changes = JSONPatch[] for property in propertynames(old.value) for change in diff(getproperty(old.value, property), getproperty(new.value, property)) push!(changes, wrappath([property], change)) end end changes # changes = [] # for property in fieldnames(T) # for change in diff(getfield(old.value, property), getfield(new.value, property)) # push!(changes, wrappath([property], change)) # end # end # changes end # ╔═╡ dbc7f97a-2de1-11eb-362f-055a734d1a9e function diff(o1::AbstractDict, o2::AbstractDict) changes = JSONPatch[] # for key in keys(o1) ∪ keys(o2) # for change in diff(get(o1, key, nothing), get(o2, key, nothing)) # push!(changes, wrappath([key], change)) # end # end # same as above but faster: for (key1, val1) in o1 for change in diff(val1, get(o2, key1, nothing)) push!(changes, wrappath([key1], change)) end end for (key2, val2) in o2 if !haskey(o1, key2) for change in diff(nothing, val2) push!(changes, wrappath([key2], change)) end end end changes end # ╔═╡ 67ade214-2de3-11eb-291d-135a397d629b function diff(o1, o2) JSONPatch[ReplacePatch([], o2)] end # ╔═╡ b8c58aa4-c24d-48a3-b2a8-7c01d50a3349 function diff(o1::Nothing, o2) JSONPatch[AddPatch([], o2)] end # ╔═╡ 5ab390f9-3b0c-4978-9e21-2aaa61db2ce4 function diff(o1, o2::Nothing) JSONPatch[RemovePatch([])] end # ╔═╡ 09f53db0-21ae-490b-86b5-414eba403d57 function diff(o1::Nothing, o2::Nothing) NoChanges end # ╔═╡ 7ca087b8-73ac-49ea-9c5a-2971f0da491f #=╠═╡ example_patches = diff(dict_1, dict_2) ╠═╡ =# # ╔═╡ 59b46bfe-da74-43af-9c11-cb0bdb2c13a2 # ╠═╡ skip_as_script = true #=╠═╡ md""" ### Dict example """ ╠═╡ =# # ╔═╡ 200516da-8cfb-42fe-a6b9-cb4730168923 # ╠═╡ skip_as_script = true #=╠═╡ celldict1 = Dict(:x => 1, :y => 2, :z => 3) ╠═╡ =# # ╔═╡ 76326e6c-b95a-4b2d-a78c-e283e5fadbe2 # ╠═╡ skip_as_script = true #=╠═╡ celldict2 = Dict(:x => 1, :y => 2, :z => 4) ╠═╡ =# # ╔═╡ 664cd334-91c7-40dd-a2bf-0da720307cfc # ╠═╡ skip_as_script = true #=╠═╡ notebook1 = Dict( :x => 1, :y => 2, ) ╠═╡ =# # ╔═╡ b7fa5625-6178-4da8-a889-cd4f014f43ba # ╠═╡ skip_as_script = true #=╠═╡ notebook2 = Dict( :y => 4, :z => 5 ) ╠═╡ =# # ╔═╡ dbdd1df0-2de1-11eb-152f-8d1af1ad02fe #=╠═╡ notebook1_to_notebook2 = diff(notebook1, notebook2) ╠═╡ =# # ╔═╡ 3924953f-787a-4912-b6ee-9c9d3030f0f0 # ╠═╡ skip_as_script = true #=╠═╡ md""" ### Large Dict example 1 """ ╠═╡ =# # ╔═╡ 80689881-1b7e-49b2-af97-9e3ab639d006 # ╠═╡ skip_as_script = true #=╠═╡ big_array = rand(UInt8, 1_000_000) ╠═╡ =# # ╔═╡ fd22b6af-5fd2-428a-8291-53e223ea692c # ╠═╡ skip_as_script = true #=╠═╡ big_string = repeat('a', 1_000_000); ╠═╡ =# # ╔═╡ bcd5059b-b0d2-49d8-a756-92349aa56aca #=╠═╡ large_dict_1 = Dict{String,Any}( "cell_$(i)" => Dict{String,Any}( "x" => 1, "y" => big_array, "z" => big_string, ) for i in 1:10 ); ╠═╡ =# # ╔═╡ e7fd6bab-c114-4f3e-b9ad-1af2d1147770 #=╠═╡ begin large_dict_2 = Dict{String,Any}( "cell_$(i)" => Dict{String,Any}( "x" => 1, "y" => big_array, "z" => big_string, ) for i in 1:10 ) large_dict_2["cell_5"]["y"] = [2,20] delete!(large_dict_2, "cell_2") large_dict_2["hello"] = Dict("a" => 1, "b" => 2) large_dict_2 end; ╠═╡ =# # ╔═╡ 43c36ab7-e9ac-450a-8abe-435412f2be1d #=╠═╡ diff(large_dict_1, large_dict_2) ╠═╡ =# # ╔═╡ 1cf22fe6-4b58-4220-87a1-d7a18410b4e8 # ╠═╡ skip_as_script = true #=╠═╡ md""" With `===` comparison for arrays: """ ╠═╡ =# # ╔═╡ ffb01ab4-e2e3-4fa4-8c0b-093d2899a536 # ╠═╡ skip_as_script = true #=╠═╡ md""" ### Large Dict example 2 """ ╠═╡ =# # ╔═╡ 8188de75-ae6e-48aa-9495-111fd27ffd26 # ╠═╡ skip_as_script = true #=╠═╡ many_items_1 = Dict{String,Any}( "cell_$(i)" => Dict{String,Any}( "x" => 1, "y" => [2,3], "z" => "four", ) for i in 1:100 ) ╠═╡ =# # ╔═╡ fdc427f0-dfe8-4114-beca-48fc15434534 #=╠═╡ @test isempty(diff(many_items_1, many_items_1)) ╠═╡ =# # ╔═╡ d807195e-ba27-4015-92a7-c9294d458d47 #=╠═╡ begin many_items_2 = deepcopy(many_items_1) many_items_2["cell_5"]["y"][2] = 20 delete!(many_items_2, "cell_2") many_items_2["hello"] = Dict("a" => 1, "b" => 2) many_items_2 end ╠═╡ =# # ╔═╡ 2e91a1a2-469c-4123-a0d7-3dcc49715738 #=╠═╡ diff(many_items_1, many_items_2) ╠═╡ =# # ╔═╡ b8061c1b-dd03-4cd1-b275-90359ae2bb39 fairly_equal(a,b) = Set(a) == Set(b) # ╔═╡ 2983f6d4-c1ca-4b66-a2d3-f858b0df2b4c #=╠═╡ @test fairly_equal(diff(large_dict_1, large_dict_2), [ ReplacePatch(["cell_5","y"], [2,20]), RemovePatch(["cell_2"]), AddPatch(["hello"], Dict("b" => 2, "a" => 1)), ]) ╠═╡ =# # ╔═╡ 61b81430-d26e-493c-96da-b6818e58c882 #=╠═╡ @test fairly_equal(diff(many_items_1, many_items_2), [ ReplacePatch(["cell_5","y"], [2,20]), RemovePatch(["cell_2"]), AddPatch(["hello"], Dict("b" => 2, "a" => 1)), ]) ╠═╡ =# # ╔═╡ aeab3363-08ba-47c2-bd33-04a004ed72c4 #=╠═╡ diff(many_items_1, many_items_1) ╠═╡ =# # ╔═╡ 62de3e79-4b4e-41df-8020-769c3c255c3e #=╠═╡ @test isempty(diff(many_items_1, many_items_1)) ╠═╡ =# # ╔═╡ c7de406d-ccfe-41cf-8388-6bd2d7c42d64 # ╠═╡ skip_as_script = true #=╠═╡ md"### Struct example" ╠═╡ =# # ╔═╡ b9cc11ae-394b-44b9-bfbe-541d7720ead0 # ╠═╡ skip_as_script = true #=╠═╡ struct Cell id code folded end ╠═╡ =# # ╔═╡ c3c675be-9178-4176-afe0-30501786b72c #=╠═╡ deep_diff(old::Cell, new::Cell) = diff(Deep(old), Deep(new)) ╠═╡ =# # ╔═╡ 02585c72-1d92-4526-98c2-1ca07aad87a3 #=╠═╡ function direct_diff(old::Cell, new::Cell) changes = [] if old.id ≠ new.id push!(changes, ReplacePatch([:id], new.id)) end if old.code ≠ new.code push!(changes, ReplacePatch([:code], new.code)) end if old.folded ≠ new.folded push!(changes, ReplacePatch([:folded], new.folded)) end changes end ╠═╡ =# # ╔═╡ 2d084dd1-240d-4443-a8a2-82ae6e0b8900 # ╠═╡ skip_as_script = true #=╠═╡ cell1 = Cell(1, 2, 3) ╠═╡ =# # ╔═╡ 3e05200f-071a-4ebe-b685-ff980f07cde7 # ╠═╡ skip_as_script = true #=╠═╡ cell2 = Cell(1, 2, 4) ╠═╡ =# # ╔═╡ dd312598-2de1-11eb-144c-f92ed6484f5d # ╠═╡ skip_as_script = true #=╠═╡ md"## Update" ╠═╡ =# # ╔═╡ d2af2a4b-8982-4e43-9fd7-0ecfdfb70511 const strict_applypatch = Ref(false) # ╔═╡ 640663fc-06ba-491e-bd85-299514237651 begin function force_convert_key(::Dict{T,<:Any}, value::T) where T value end function force_convert_key(::Dict{T,<:Any}, value::Any) where T T(value) end end # ╔═╡ 48a45941-2489-4666-b4e5-88d3f82e5145 function getpath(value, path) if length(path) == 0 return value end current, rest... = path if value isa AbstractDict key = force_convert_key(value, current) getpath(getindex(value, key), rest) else getpath(getproperty(value, Symbol(current)), rest) end end # ╔═╡ 752b2da3-ff24-4758-8843-186368069888 function applypatch!(value, patches::Array{JSONPatch}) for patch in patches applypatch!(value, patch) end return value end # ╔═╡ 3e285076-1d97-4728-87cf-f71b22569e57 # ╠═╡ skip_as_script = true #=╠═╡ md"### applypatch! AddPatch" ╠═╡ =# # ╔═╡ d7ea6052-9d9f-48e3-92fb-250afd69e417 begin _convert(::Type{Base.UUID}, s::String) = Base.UUID(s) _convert(::Type{T}, a::AbstractArray) where {T<:Array} = _convert.(eltype(T), a) _convert(x, y) = convert(x, y) function _convert(::Type{<:Dict}, patch::ReplacePatch) Dict{String,Any}("op" => "replace", "path" => patch.path, "value" => patch.value) end function _setproperty!(x, f::Symbol, v) type = fieldtype(typeof(x), f) return setfield!(x, f, _convert(type, v)) end end # ╔═╡ 7feeee3a-3aec-47ce-b8d7-74a0d9b0b381 # ╠═╡ skip_as_script = true #=╠═╡ _convert(Dict, ReplacePatch([:x, :y], 10)) ╠═╡ =# # ╔═╡ dd87ca7e-2de1-11eb-2ec3-d5721c32f192 function applypatch!(value, patch::AddPatch) if length(patch.path) == 0 throw("Impossible") else last = patch.path[end] rest = patch.path[begin:end - 1] subvalue = getpath(value, rest) if subvalue isa AbstractDict key = force_convert_key(subvalue, last) if strict_applypatch[] @assert get(subvalue, key, nothing) === nothing end subvalue[key] = patch.value else key = Symbol(last) if strict_applypatch[] @assert getproperty(subvalue, key) === nothing end _setproperty!(subvalue, key, patch.value) end end return value end # ╔═╡ a11e4082-4ff4-4c1b-9c74-c8fa7dcceaa6 # ╠═╡ skip_as_script = true #=╠═╡ md"*Should throw in strict mode:*" ╠═╡ =# # ╔═╡ be6b6fc4-e12a-4cef-81d8-d5115fda50b7 # ╠═╡ skip_as_script = true #=╠═╡ md"### applypatch! ReplacePatch" ╠═╡ =# # ╔═╡ 6509d62e-77b6-499c-8dab-4a608e44720a function applypatch!(value, patch::ReplacePatch) if length(patch.path) == 0 throw("Impossible") else last = patch.path[end] rest = patch.path[begin:end - 1] subvalue = getpath(value, rest) if subvalue isa AbstractDict key = force_convert_key(subvalue, last) if strict_applypatch[] @assert get(subvalue, key, nothing) !== nothing end subvalue[key] = patch.value else key = Symbol(last) if strict_applypatch[] @assert getproperty(subvalue, key) !== nothing end _setproperty!(subvalue, key, patch.value) end end return value end # ╔═╡ f1dde1bd-3fa4-48b7-91ed-b2f98680fcc1 # ╠═╡ skip_as_script = true #=╠═╡ md"*Should throw in strict mode:*" ╠═╡ =# # ╔═╡ f3ef354b-b480-4b48-8358-46dbf37e1d95 # ╠═╡ skip_as_script = true #=╠═╡ md"### applypatch! RemovePatch" ╠═╡ =# # ╔═╡ ddaf5b66-2de1-11eb-3348-b905b94a984b function applypatch!(value, patch::RemovePatch) if length(patch.path) == 0 throw("Impossible") else last = patch.path[end] rest = patch.path[begin:end - 1] subvalue = getpath(value, rest) if subvalue isa AbstractDict key = force_convert_key(subvalue, last) if strict_applypatch[] @assert get(subvalue, key, nothing) !== nothing end delete!(subvalue, key) else key = Symbol(last) if strict_applypatch[] @assert getproperty(subvalue, key) !== nothing end _setproperty!(subvalue, key, nothing) end end return value end # ╔═╡ e65d483a-4c13-49ba-bff1-1d54de78f534 #=╠═╡ let dict_1_copy = deepcopy(dict_1) applypatch!(dict_1_copy, example_patches) end ╠═╡ =# # ╔═╡ 595fdfd4-3960-4fbd-956c-509c4cf03473 #=╠═╡ @test applypatch!(deepcopy(notebook1), notebook1_to_notebook2) == notebook2 ╠═╡ =# # ╔═╡ c3e4738f-4568-4910-a211-6a46a9d447ee #=╠═╡ @test applypatch!(Dict(:y => "x"), AddPatch([:x], "-")) == Dict(:y => "x", :x => "-") ╠═╡ =# # ╔═╡ 0f094932-10e5-40f9-a3fc-db27a85b4999 #=╠═╡ @test applypatch!(Dict(:x => "x"), AddPatch([:x], "-")) == Dict(:x => "-") ╠═╡ =# # ╔═╡ a560fdca-ee12-469c-bda5-62d7203235b8 #=╠═╡ @test applypatch!(Dict(:x => "x"), ReplacePatch([:x], "-")) == Dict(:x => "-") ╠═╡ =# # ╔═╡ 01e3417e-334e-4a8d-b086-4bddc42737b3 #=╠═╡ @test applypatch!(Dict(:y => "x"), ReplacePatch([:x], "-")) == Dict(:x => "-", :y => "x") ╠═╡ =# # ╔═╡ 96a80a23-7c56-4c41-b489-15bc1c4e3700 #=╠═╡ @test applypatch!(Dict(:x => "x"), RemovePatch([:x])) == Dict() ╠═╡ =# # ╔═╡ df41caa7-f0fc-4b0d-ab3d-ebdab4804040 # ╠═╡ skip_as_script = true #=╠═╡ md"*Should throw in strict mode:*" ╠═╡ =# # ╔═╡ fac65755-2a2a-4a3c-b5a8-fc4f6d256754 #=╠═╡ @test applypatch!(Dict(:y => "x"), RemovePatch([:x])) == Dict(:y => "x") ╠═╡ =# # ╔═╡ e55d1cea-2de1-11eb-0d0e-c95009eedc34 # ╠═╡ skip_as_script = true #=╠═╡ md"## Testing" ╠═╡ =# # ╔═╡ b05fcb88-3781-45d0-9f24-e88c339a72e5 # ╠═╡ skip_as_script = true #=╠═╡ macro test2(expr) quote nothing end end ╠═╡ =# # ╔═╡ e7e8d076-2de1-11eb-0214-8160bb81370a #=╠═╡ @test notebook1 == deepcopy(notebook1) ╠═╡ =# # ╔═╡ ee70e282-36d5-4772-8585-f50b9a67ca54 # ╠═╡ skip_as_script = true #=╠═╡ md"## Track" ╠═╡ =# # ╔═╡ a3e8fe70-cbf5-4758-a0f2-d329d138728c # ╠═╡ skip_as_script = true #=╠═╡ function prettytime(time_ns::Number) suffices = ["ns", "μs", "ms", "s"] current_amount = time_ns suffix = "" for current_suffix in suffices if current_amount >= 1000.0 current_amount = current_amount / 1000.0 else suffix = current_suffix break end end # const roundedtime = time_ns.toFixed(time_ns >= 100.0 ? 0 : 1) roundedtime = if current_amount >= 100.0 round(current_amount; digits=0) else round(current_amount; digits=1) end return "$(roundedtime) $(suffix)" end ╠═╡ =# # ╔═╡ 0e1c6442-9040-49d9-b754-173583db7ba2 # ╠═╡ skip_as_script = true #=╠═╡ begin Base.@kwdef struct Tracked expr value time bytes times_ran = 1 which = nothing code_info = nothing end function Base.show(io::IO, mime::MIME"text/html", value::Tracked) times_ran = if value.times_ran === 1 "" else """ ($(value.times_ran)×)""" end # method = sprint(show, MIME("text/plain"), value.which) code_info = if value.code_info ≠ nothing codelength = length(value.code_info.first.code) "$(codelength) frames in @code_typed" else "" end color = if value.time > 1 "red" elseif value.time > 0.001 "orange" elseif value.time > 0.0001 "blue" else "green" end show(io, mime, HTML("""
$(value.expr)
$(prettytime(value.time * 1e9 / value.times_ran)) $(times_ran)
$(code_info)
""")) end Tracked end ╠═╡ =# # ╔═╡ 7618aef7-1884-4e32-992d-0fd988e1ab20 # ╠═╡ skip_as_script = true #=╠═╡ macro track(expr) times_ran_expr = :(1) expr_to_show = expr if expr.head == :for @assert expr.args[1].head == :(=) times_ran_expr = expr.args[1].args[2] expr_to_show = expr.args[2].args[2] end Tracked # reference so that baby Pluto understands quote local times_ran = length($(esc(times_ran_expr))) local value, time, bytes = @timed $(esc(expr)) local method = nothing local code_info = nothing try # Uhhh method = @which $(expr_to_show) code_info = @code_typed $(expr_to_show) catch nothing end Tracked( expr=$(QuoteNode(expr_to_show)), value=value, time=time, bytes=bytes, times_ran=times_ran, which=method, code_info=code_info ) end end ╠═╡ =# # ╔═╡ 7b8ab89b-bf56-4ddf-b220-b4881f4a2050 #=╠═╡ @track Base.convert(JSONPatch, convert(Dict, add_patch)) == add_patch ╠═╡ =# # ╔═╡ 48ccd28a-060d-4214-9a39-f4c4e506d1aa #=╠═╡ @track Base.convert(JSONPatch, convert(Dict, remove_patch)) == remove_patch ╠═╡ =# # ╔═╡ 34d86e02-dd34-4691-bb78-3023568a5d16 #=╠═╡ @track Base.convert(JSONPatch, _convert(Dict, replace_patch)) == replace_patch ╠═╡ =# # ╔═╡ 95ff676d-73c8-44cb-ac35-af94418737e9 #=╠═╡ @track for _ in 1:100 diff(celldict1, celldict2) end ╠═╡ =# # ╔═╡ 8c069015-d922-4c60-9340-8d65c80b1a06 #=╠═╡ @track for _ in 1:1000 diff(large_dict_1, large_dict_1) end ╠═╡ =# # ╔═╡ bc9a0822-1088-4ee7-8c79-98e06fd50f11 #=╠═╡ @track for _ in 1:1000 diff(large_dict_1, large_dict_2) end ╠═╡ =# # ╔═╡ ddf1090c-5239-41df-ae4d-70aeb3a75f2b #=╠═╡ let old = use_triple_equals_for_arrays[] use_triple_equals_for_arrays[] = true result = @track for _ in 1:1000 diff(large_dict_1, large_dict_1) end use_triple_equals_for_arrays[] = old result end ╠═╡ =# # ╔═╡ 88009db3-f40e-4fd0-942a-c7f4a7eecb5a #=╠═╡ let old = use_triple_equals_for_arrays[] use_triple_equals_for_arrays[] = true result = @track for _ in 1:1000 diff(large_dict_1, large_dict_2) end use_triple_equals_for_arrays[] = old result end ╠═╡ =# # ╔═╡ c287009f-e864-45d2-a4d0-a525c988a6e0 #=╠═╡ @track for _ in 1:1000 diff(many_items_1, many_items_1) end ╠═╡ =# # ╔═╡ 67a1ae27-f7df-4f84-8809-1cc6a9bcd1ce #=╠═╡ @track for _ in 1:1000 diff(many_items_1, many_items_2) end ╠═╡ =# # ╔═╡ fa959806-3264-4dd5-9f94-ba369697689b #=╠═╡ @track for _ in 1:1000 direct_diff(cell2, cell1) end ╠═╡ =# # ╔═╡ a9088341-647c-4fe1-ab85-d7da049513ae #=╠═╡ @track for _ in 1:1000 diff(Deep(cell1), Deep(cell2)) end ╠═╡ =# # ╔═╡ 1a26eed8-670c-43bf-9726-2db84b1afdab #=╠═╡ @track sleep(0.1) ╠═╡ =# # ╔═╡ Cell order: # ╟─d948dc6e-2de1-11eb-19e7-cb3bb66353b6 # ╟─1a6e1853-6db1-4074-bce0-5f274351cece # ╟─49fc1f97-3b8f-4297-94e5-2e24c001d35c # ╠═d8e73b90-24c5-4e50-830b-b1dbe6224c8e # ╠═19646596-b35b-44fa-bfcf-891f9ffb748c # ╠═7ca087b8-73ac-49ea-9c5a-2971f0da491f # ╟─9d2c07d9-16a9-4b9f-a375-2adb6e5b907a # ╠═e65d483a-4c13-49ba-bff1-1d54de78f534 # ╟─336bfd4f-8a8e-4a2d-be08-ee48d6a9f747 # ╠═db116c0a-2de1-11eb-2a56-872af797c547 # ╠═bd0d46bb-3e58-4522-bae0-83eb799196c4 # ╠═db2d8a3e-2de1-11eb-02b8-9ffbfaeff61c # ╠═ffe9b3d9-8e35-4a31-bab2-8787a4140594 # ╠═894de8a7-2757-4d7a-a2be-1069fa872911 # ╠═9a364714-edb1-4bca-9387-a8bbacccd10d # ╠═9321d3be-cb91-4406-9dc7-e5c38f7d377c # ╠═73631aea-5e93-4da2-a32d-649029660d4e # ╠═0fd3e910-abcc-4421-9d0b-5cfb90034338 # ╟─aad7ab32-eecf-4aad-883d-1c802cad6c0c # ╠═732fd744-acdb-4507-b1de-6866ec5563dd # ╠═17606cf6-2d0f-4245-89a3-746ad818a664 # ╠═c7ac7d27-7bf9-4209-8f3c-e4d52c543e29 # ╠═042f7788-e996-430e-886d-ffb4f70dea9e # ╠═9d2dde5c-d404-4fbc-b8e0-5024303c8052 # ╠═f649f67c-aab0-4d35-a799-f398e5f3ecc4 # ╠═63087738-d70c-46f5-b072-21cd8953df35 # ╠═aa81974a-7254-45e0-9bfe-840c4793147f # ╠═31188a03-76ba-40cf-a333-4d339ce37711 # ╠═7524a9e8-1a6d-4851-b50e-19415f25a84b # ╟─5ddfd616-db20-451b-bc1e-2ad52e0e2777 # ╟─24e93923-eab9-4a7b-9bc7-8d8a1209a78f # ╟─09ddf4d9-5ccb-4530-bfab-d11b864e872a # ╟─d9e764db-94fc-44f7-8c2e-3d63f4809617 # ╟─99df99ad-aad5-4275-97d4-d1ceeb2f8d15 # ╟─2d665639-7274-495a-ae9d-f358a8219bb7 # ╟─f658a72d-871d-49b3-9b73-7efedafbd7a6 # ╠═230bafe2-aaa7-48f0-9fd1-b53956281684 # ╟─07eeb122-6706-4544-a007-1c8d6581eec8 # ╠═b48e2c08-a94a-4247-877d-949d92dde626 # ╟─c59b30b9-f702-41f1-bb2e-1736c8cd5ede # ╟─7feeee3a-3aec-47ce-b8d7-74a0d9b0b381 # ╠═921a130e-b028-4f91-b077-3bd79dcb6c6d # ╟─6d67f8a5-0e0c-4b6e-a267-96b34d580946 # ╟─7b8ab89b-bf56-4ddf-b220-b4881f4a2050 # ╟─56b28842-4a67-44d7-95e7-55d457a44fb1 # ╟─48ccd28a-060d-4214-9a39-f4c4e506d1aa # ╟─f10e31c0-1d2c-4727-aba5-dd676a10041b # ╟─34d86e02-dd34-4691-bb78-3023568a5d16 # ╟─3a99e22d-42d6-4b2d-9381-022b41b0e852 # ╟─831d84a6-1c71-4e68-8c7c-27d9093a82c4 # ╟─2ad11c73-4691-4283-8f98-3d2a87926b99 # ╟─5513ea3b-9498-426c-98cb-7dc23d32f72e # ╟─0c2d6da1-cad3-4c9f-93e9-922457083945 # ╟─84c87031-7733-4d1f-aa90-f8ab71506251 # ╟─8f265a33-3a2d-4508-9477-ca62e8ce3c12 # ╟─daf9ec12-2de1-11eb-3a8d-59d9c2753134 # ╠═0b50f6b2-8e85-4565-9f04-f99c913b4592 # ╠═59e94cb2-c2f9-4f6c-9562-45e8c15931af # ╠═c9d5d81c-b0b6-4d1a-b1de-96d3b3701700 # ╠═24389a0a-c3ac-4438-9dfe-1d14cd033d25 # ╠═9cbaaec2-709c-4769-886c-ec92b12c18bc # ╠═db75df12-2de1-11eb-0726-d1995cebd382 # ╠═dbc7f97a-2de1-11eb-362f-055a734d1a9e # ╠═67ade214-2de3-11eb-291d-135a397d629b # ╠═b8c58aa4-c24d-48a3-b2a8-7c01d50a3349 # ╠═5ab390f9-3b0c-4978-9e21-2aaa61db2ce4 # ╠═09f53db0-21ae-490b-86b5-414eba403d57 # ╟─59b46bfe-da74-43af-9c11-cb0bdb2c13a2 # ╟─200516da-8cfb-42fe-a6b9-cb4730168923 # ╟─76326e6c-b95a-4b2d-a78c-e283e5fadbe2 # ╟─95ff676d-73c8-44cb-ac35-af94418737e9 # ╠═664cd334-91c7-40dd-a2bf-0da720307cfc # ╠═b7fa5625-6178-4da8-a889-cd4f014f43ba # ╠═dbdd1df0-2de1-11eb-152f-8d1af1ad02fe # ╠═595fdfd4-3960-4fbd-956c-509c4cf03473 # ╟─3924953f-787a-4912-b6ee-9c9d3030f0f0 # ╠═80689881-1b7e-49b2-af97-9e3ab639d006 # ╠═fd22b6af-5fd2-428a-8291-53e223ea692c # ╠═bcd5059b-b0d2-49d8-a756-92349aa56aca # ╠═e7fd6bab-c114-4f3e-b9ad-1af2d1147770 # ╟─43c36ab7-e9ac-450a-8abe-435412f2be1d # ╟─2983f6d4-c1ca-4b66-a2d3-f858b0df2b4c # ╟─fdc427f0-dfe8-4114-beca-48fc15434534 # ╟─8c069015-d922-4c60-9340-8d65c80b1a06 # ╟─bc9a0822-1088-4ee7-8c79-98e06fd50f11 # ╟─1cf22fe6-4b58-4220-87a1-d7a18410b4e8 # ╟─ddf1090c-5239-41df-ae4d-70aeb3a75f2b # ╟─88009db3-f40e-4fd0-942a-c7f4a7eecb5a # ╟─ffb01ab4-e2e3-4fa4-8c0b-093d2899a536 # ╠═8188de75-ae6e-48aa-9495-111fd27ffd26 # ╠═d807195e-ba27-4015-92a7-c9294d458d47 # ╠═2e91a1a2-469c-4123-a0d7-3dcc49715738 # ╟─61b81430-d26e-493c-96da-b6818e58c882 # ╠═b8061c1b-dd03-4cd1-b275-90359ae2bb39 # ╠═aeab3363-08ba-47c2-bd33-04a004ed72c4 # ╟─62de3e79-4b4e-41df-8020-769c3c255c3e # ╟─c287009f-e864-45d2-a4d0-a525c988a6e0 # ╟─67a1ae27-f7df-4f84-8809-1cc6a9bcd1ce # ╟─c7de406d-ccfe-41cf-8388-6bd2d7c42d64 # ╠═b9cc11ae-394b-44b9-bfbe-541d7720ead0 # ╠═c3c675be-9178-4176-afe0-30501786b72c # ╠═02585c72-1d92-4526-98c2-1ca07aad87a3 # ╟─2d084dd1-240d-4443-a8a2-82ae6e0b8900 # ╟─3e05200f-071a-4ebe-b685-ff980f07cde7 # ╟─fa959806-3264-4dd5-9f94-ba369697689b # ╟─a9088341-647c-4fe1-ab85-d7da049513ae # ╟─dd312598-2de1-11eb-144c-f92ed6484f5d # ╠═d2af2a4b-8982-4e43-9fd7-0ecfdfb70511 # ╠═640663fc-06ba-491e-bd85-299514237651 # ╠═48a45941-2489-4666-b4e5-88d3f82e5145 # ╠═752b2da3-ff24-4758-8843-186368069888 # ╟─3e285076-1d97-4728-87cf-f71b22569e57 # ╠═d7ea6052-9d9f-48e3-92fb-250afd69e417 # ╠═dd87ca7e-2de1-11eb-2ec3-d5721c32f192 # ╟─c3e4738f-4568-4910-a211-6a46a9d447ee # ╟─a11e4082-4ff4-4c1b-9c74-c8fa7dcceaa6 # ╟─0f094932-10e5-40f9-a3fc-db27a85b4999 # ╟─be6b6fc4-e12a-4cef-81d8-d5115fda50b7 # ╠═6509d62e-77b6-499c-8dab-4a608e44720a # ╟─a560fdca-ee12-469c-bda5-62d7203235b8 # ╟─f1dde1bd-3fa4-48b7-91ed-b2f98680fcc1 # ╟─01e3417e-334e-4a8d-b086-4bddc42737b3 # ╟─f3ef354b-b480-4b48-8358-46dbf37e1d95 # ╠═ddaf5b66-2de1-11eb-3348-b905b94a984b # ╟─96a80a23-7c56-4c41-b489-15bc1c4e3700 # ╟─df41caa7-f0fc-4b0d-ab3d-ebdab4804040 # ╟─fac65755-2a2a-4a3c-b5a8-fc4f6d256754 # ╟─e55d1cea-2de1-11eb-0d0e-c95009eedc34 # ╠═3e07f976-6cd0-4841-9762-d40337bb0645 # ╠═e748600a-2de1-11eb-24be-d5f0ecab8fa4 # ╠═b05fcb88-3781-45d0-9f24-e88c339a72e5 # ╠═e7e8d076-2de1-11eb-0214-8160bb81370a # ╟─ee70e282-36d5-4772-8585-f50b9a67ca54 # ╟─1a26eed8-670c-43bf-9726-2db84b1afdab # ╟─0e1c6442-9040-49d9-b754-173583db7ba2 # ╟─7618aef7-1884-4e32-992d-0fd988e1ab20 # ╟─a3e8fe70-cbf5-4758-a0f2-d329d138728c ?/opt/julia/packages/Pluto/GVuR6/src/webserver/FirebaseyUtils.jl,### A Pluto.jl notebook ### # v0.19.9 using Markdown using InteractiveUtils # ╔═╡ 092c4b11-8b75-446f-b3ad-01fa858daebb # ╠═╡ show_logs = false # ╠═╡ skip_as_script = true #=╠═╡ # Only define this in Pluto using skip_as_script = true begin import Pkg Pkg.activate(mktempdir()) Pkg.add(Pkg.PackageSpec(name="PlutoTest")) using PlutoTest end ╠═╡ =# # ╔═╡ 058a3333-0567-43b7-ac5f-1f6688325a08 begin """ Mark an instance of a custom struct as immutable. The resulting object is also an `AbstractDict`, where the keys are the struct fields (converted to strings). """ struct ImmutableMarker{T} <: AbstractDict{String,Any} source::T end function Base.getindex(ldict::ImmutableMarker, key::String) Base.getfield(ldict.source, Symbol(key)) end # disabled because it's immutable! # Base.setindex!(ldict::ImmutableMarker, args...) = Base.setindex!(ldict.source, args...) # Base.delete!(ldict::ImmutableMarker, args...) = Base.delete!(ldict.source, args...) Base.keys(ldict::ImmutableMarker{T}) where T = String.(fieldnames(T)) # Base.values(ldict::ImmutableMarker) = Base.values(ldict.source) Base.length(ldict::ImmutableMarker) = nfields(ldict.source) Base.iterate(ldict::ImmutableMarker) = Base.iterate(ldict, 1) function Base.iterate(ldict::ImmutableMarker{T}, i) where T a = ldict.source if i <= nfields(a) name = fieldname(T, i) (String(name) => getfield(a, name), i + 1) end end end # ╔═╡ 55975e53-f70f-4b70-96d2-b144f74e7cde # ╠═╡ skip_as_script = true #=╠═╡ struct A x y z end ╠═╡ =# # ╔═╡ d7e0de85-5cb2-4036-a2e3-ca416ea83737 #=╠═╡ id1 = ImmutableMarker(A(1,"asdf",3)) ╠═╡ =# # ╔═╡ 08350326-526e-4c34-ab27-df9fbf69243e #=╠═╡ id2 = ImmutableMarker(A(1,"asdf",4)) ╠═╡ =# # ╔═╡ aa6192e8-410f-4924-8250-4775e21b1590 #=╠═╡ id1d, id2d = Dict(id1), Dict(id2) ╠═╡ =# # ╔═╡ 273c7c85-8178-44a7-99f0-581754aeb8c8 begin """ Mark a vector as being append-only: let Firebasey know that it can diff this array simply by comparing lengths, without looking at its contents. It was made specifically for logs: Logs are always appended, OR the whole log stream is reset. AppendonlyMarker is like SubArray (a view into another array) except we agree to only ever append to the source array. This way, firebase can just look at the index and diff based on that. """ struct AppendonlyMarker{T} <: AbstractVector{T} mutable_source::Vector{T} length_at_time_of_creation::Int end AppendonlyMarker(arr) = AppendonlyMarker(arr, length(arr)) # Poor mans vector-proxy # I think this is enough for Pluto to show, and for msgpack to pack function Base.size(arr::AppendonlyMarker) return (arr.length_at_time_of_creation,) end Base.getindex(arr::AppendonlyMarker, index::Int) = arr.mutable_source[index] Base.iterate(arr::AppendonlyMarker, args...) = Base.iterate(arr.mutable_source, args...) end # ╔═╡ ef7032d1-a666-48a6-a56e-df175f5ed832 # ╠═╡ skip_as_script = true #=╠═╡ md""" ## ImmutableMarker """ ╠═╡ =# # ╔═╡ 183cef1f-bfe9-42cd-8239-49e9ed00a7b6 # ╠═╡ skip_as_script = true #=╠═╡ md""" ## AppendonlyMarker(s) Example of how to solve performance problems with Firebasey: We make a new type with a specific diff function. It might be very specific per problem, but that's fine for performance problems (I think). It also keeps the performance solutions as separate modules/packages to whatever it is you're actually modeling. """ ╠═╡ =# # ╔═╡ 35d3bcd7-af51-466a-b4c4-cc055e74d01d # ╠═╡ skip_as_script = true #=╠═╡ appendonly_1, appendonly_2 = let array_1 = [1,2,3,4] appendonly_1 = AppendonlyMarker(array_1) push!(array_1, 5) appendonly_2 = AppendonlyMarker(array_1) appendonly_1, appendonly_2 end; ╠═╡ =# # ╔═╡ 1017f6cc-58ac-4c7b-a6d0-a03f5e387f1b # ╠═╡ skip_as_script = true #=╠═╡ appendonly_1_large, appendonly_2_large = let large_array_1 = [ Dict{String,Any}( "x" => 1, "y" => [1,2,3,4], "z" => "hi", ) for i in 1:10000 ]; appendonly_1 = AppendonlyMarker(large_array_1) push!(large_array_1, Dict("x" => 5)) appendonly_2 = AppendonlyMarker(large_array_1) appendonly_1, appendonly_2 end; ╠═╡ =# # ╔═╡ 06492e8d-4500-4efe-80ee-55bf1ee2348c #=╠═╡ @test length([AppendonlyMarker([1,2,3])...]) == 3 ╠═╡ =# # ╔═╡ 2284ae12-5b8c-4542-81fa-c4d34f2483e7 # @test length([AppendonlyMarker([1,2,3], 1)...]) == 1 # ╔═╡ dc5cd268-9cfb-49bf-87fb-5b7db4fa6e3c # ╠═╡ skip_as_script = true #=╠═╡ md"## Import Firebasey when running inside notebook" ╠═╡ =# # ╔═╡ 0c2f23d8-8e98-47b7-9c4f-5daa70a6c7fb # OH how I wish I would put in the time to refactor with fromFile or SOEMTGHINLAS LDKJ JULIA WHY ARE YOU LIKE THIS GROW UP if !@isdefined(Firebasey) Firebasey = let wrapper_module = Module() Core.eval(wrapper_module, :(module Firebasey include("Firebasey.jl") end )) wrapper_module.Firebasey end end # ╔═╡ 2903d17e-c6fd-4cea-8585-4db26a00b0e7 function Firebasey.diff(a::AppendonlyMarker, b::AppendonlyMarker) if a.mutable_source !== b.mutable_source [Firebasey.ReplacePatch([], b)] else if a.length_at_time_of_creation > b.length_at_time_of_creation throw(ErrorException("Not really supposed to diff AppendonlyMarker with the original being longer than the next version (you know, 'append only' and al)")) end map(a.length_at_time_of_creation+1:b.length_at_time_of_creation) do index Firebasey.AddPatch([index], b.mutable_source[index]) end end end # ╔═╡ 129dee79-61c0-4524-9bef-388837f035bb function Firebasey.diff(a::ImmutableMarker, b::ImmutableMarker) if a.source !== b.source Firebasey.diff(Dict(a), Dict(b)) # Firebasey.JSONPatch[Firebasey.ReplacePatch([], b)] else Firebasey.JSONPatch[] end end # ╔═╡ 138d2cc2-59ba-4f76-bf66-ecdb98cf4fd5 #=╠═╡ Firebasey.diff(id1, id2) ╠═╡ =# # ╔═╡ 8537488d-2ff9-42b7-8bfc-72d43fca713f #=╠═╡ @test Firebasey.diff(appendonly_1, appendonly_2) == [Firebasey.AddPatch([5], 5)] ╠═╡ =# # ╔═╡ 721e3c90-15ae-43f2-9234-57b38e3e6b69 # ╠═╡ skip_as_script = true #=╠═╡ md""" ## Track """ ╠═╡ =# # ╔═╡ e830792c-c809-4fde-ae55-8ae01b4c04b9 # ╠═╡ skip_as_script = true #=╠═╡ function prettytime(time_ns::Number) suffices = ["ns", "μs", "ms", "s"] current_amount = time_ns suffix = "" for current_suffix in suffices if current_amount >= 1000.0 current_amount = current_amount / 1000.0 else suffix = current_suffix break end end # const roundedtime = time_ns.toFixed(time_ns >= 100.0 ? 0 : 1) roundedtime = if current_amount >= 100.0 round(current_amount; digits=0) else round(current_amount; digits=1) end return "$(roundedtime) $(suffix)" end ╠═╡ =# # ╔═╡ 16b03608-0f5f-421a-bab4-89365528b0b4 # ╠═╡ skip_as_script = true #=╠═╡ begin Base.@kwdef struct Tracked expr value time bytes times_ran = 1 which = nothing code_info = nothing end function Base.show(io::IO, mime::MIME"text/html", value::Tracked) times_ran = if value.times_ran === 1 "" else """ ($(value.times_ran)×)""" end # method = sprint(show, MIME("text/plain"), value.which) code_info = if value.code_info ≠ nothing codelength = length(value.code_info.first.code) "$(codelength) frames in @code_typed" else "" end color = if value.time > 1 "red" elseif value.time > 0.001 "orange" elseif value.time > 0.0001 "blue" else "green" end show(io, mime, HTML("""
$(value.expr)
$(prettytime(value.time * 1e9 / value.times_ran)) $(times_ran)
$(code_info)
""")) end Tracked end ╠═╡ =# # ╔═╡ 875fd249-37cc-49da-8a7d-381fe0e21063 #=╠═╡ macro track(expr) times_ran_expr = :(1) expr_to_show = expr if expr.head == :for @assert expr.args[1].head == :(=) times_ran_expr = expr.args[1].args[2] expr_to_show = expr.args[2].args[2] end Tracked # reference so that baby Pluto understands quote local times_ran = length($(esc(times_ran_expr))) local value, time, bytes = @timed $(esc(expr)) local method = nothing local code_info = nothing try # Uhhh method = @which $(expr_to_show) code_info = @code_typed $(expr_to_show) catch nothing end Tracked( expr=$(QuoteNode(expr_to_show)), value=value, time=time, bytes=bytes, times_ran=times_ran, which=method, code_info=code_info ) end end ╠═╡ =# # ╔═╡ a5f43f47-6189-413f-95a0-d98f927bb7ce #=╠═╡ @track for _ in 1:1000 Firebasey.diff(id1, id1) end ╠═╡ =# # ╔═╡ ab5089cc-fec8-43b9-9aa4-d6fa96e231e0 #=╠═╡ @track for _ in 1:1000 Firebasey.diff(id1d, id1d) end ╠═╡ =# # ╔═╡ a84dcdc3-e9ed-4bf5-9bec-c9cbfc267c17 #=╠═╡ @track for _ in 1:1000 Firebasey.diff(id1, id2) end ╠═╡ =# # ╔═╡ f696bb85-0bbd-43c9-99ea-533816bc8e0d #=╠═╡ @track for _ in 1:1000 Firebasey.diff(id1d, id2d) end ╠═╡ =# # ╔═╡ 37fe8c10-09f0-4f72-8cfd-9ce044c78c13 #=╠═╡ @track for _ in 1:1000 Firebasey.diff(appendonly_1_large, appendonly_2_large) end ╠═╡ =# # ╔═╡ 9862ee48-48a0-4178-8ec4-306792827e17 #=╠═╡ @track sleep(0.1) ╠═╡ =# # ╔═╡ Cell order: # ╟─ef7032d1-a666-48a6-a56e-df175f5ed832 # ╠═058a3333-0567-43b7-ac5f-1f6688325a08 # ╠═129dee79-61c0-4524-9bef-388837f035bb # ╠═55975e53-f70f-4b70-96d2-b144f74e7cde # ╠═d7e0de85-5cb2-4036-a2e3-ca416ea83737 # ╠═08350326-526e-4c34-ab27-df9fbf69243e # ╠═138d2cc2-59ba-4f76-bf66-ecdb98cf4fd5 # ╠═aa6192e8-410f-4924-8250-4775e21b1590 # ╟─a5f43f47-6189-413f-95a0-d98f927bb7ce # ╟─ab5089cc-fec8-43b9-9aa4-d6fa96e231e0 # ╟─a84dcdc3-e9ed-4bf5-9bec-c9cbfc267c17 # ╟─f696bb85-0bbd-43c9-99ea-533816bc8e0d # ╟─183cef1f-bfe9-42cd-8239-49e9ed00a7b6 # ╠═273c7c85-8178-44a7-99f0-581754aeb8c8 # ╠═2903d17e-c6fd-4cea-8585-4db26a00b0e7 # ╠═35d3bcd7-af51-466a-b4c4-cc055e74d01d # ╠═1017f6cc-58ac-4c7b-a6d0-a03f5e387f1b # ╟─06492e8d-4500-4efe-80ee-55bf1ee2348c # ╠═2284ae12-5b8c-4542-81fa-c4d34f2483e7 # ╟─8537488d-2ff9-42b7-8bfc-72d43fca713f # ╟─37fe8c10-09f0-4f72-8cfd-9ce044c78c13 # ╟─dc5cd268-9cfb-49bf-87fb-5b7db4fa6e3c # ╠═0c2f23d8-8e98-47b7-9c4f-5daa70a6c7fb # ╠═092c4b11-8b75-446f-b3ad-01fa858daebb # ╟─721e3c90-15ae-43f2-9234-57b38e3e6b69 # ╟─9862ee48-48a0-4178-8ec4-306792827e17 # ╟─16b03608-0f5f-421a-bab4-89365528b0b4 # ╟─875fd249-37cc-49da-8a7d-381fe0e21063 # ╟─e830792c-c809-4fde-ae55-8ae01b4c04b9 :/opt/julia/packages/Pluto/GVuR6/src/webserver/REPLTools.jlimport FuzzyCompletions: complete_path, completion_text, score import Malt import .PkgCompat: package_completions using Markdown import REPL ### # RESPONSES FOR AUTOCOMPLETE & DOCS ### function format_path_completion(completion) replace(replace(completion_text(completion), "\\ " => " "), "\\\\" => "\\") end responses[:completepath] = function response_completepath(🙋::ClientRequest) path = 🙋.body["query"] pos = lastindex(path) results, loc, found = complete_path(path, pos) # too many candiates otherwise. -0.1 instead of 0 to enable autocompletions for paths: `/` or `/asdf/` isenough(x) = x ≥ -0.1 ishidden(path_completion) = let p = path_completion.path startswith(basename(isdirpath(p) ? dirname(p) : p), ".") end filter!(p -> !ishidden(p) && (isenough ∘ score)(p), results) start_utf8 = let # REPLCompletions takes into account that spaces need to be prefixed with `\` in the shell, so it subtracts the number of spaces in the filename from `start`: # https://github.com/JuliaLang/julia/blob/c54f80c785a3107ae411267427bbca05f5362b0b/stdlib/REPL/src/REPLCompletions.jl#L270 # we don't use prefixes, so we need to reverse this. # this is from the Julia source code: # https://github.com/JuliaLang/julia/blob/c54f80c785a3107ae411267427bbca05f5362b0b/stdlib/REPL/src/REPLCompletions.jl#L195-L204 if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path) # if the path is just "~", don't consider the expanded username as a prefix if path == "~" dir, prefix = homedir(), "" else dir, prefix = splitdir(homedir() * path[2:end]) end else dir, prefix = splitdir(path) end loc.start + count(isequal(' '), prefix) end stop_utf8 = nextind(path, pos) # advance one unicode char, js uses exclusive upper bound scores = [max(0.0, score(r)) for r in results] formatted = format_path_completion.(results) # sort on score. If a tie (e.g. both score 0.0), sort on dir/file. If a tie, sort alphabetically. perm = sortperm(collect(zip(.-scores, (!isdirpath).(formatted), formatted))) msg = UpdateMessage(:completion_result, Dict( :start => start_utf8 - 1, # 1-based index (julia) to 0-based index (js) :stop => stop_utf8 - 1, # idem :results => formatted[perm] ), 🙋.notebook, nothing, 🙋.initiator) putclientupdates!(🙋.session, 🙋.initiator, msg) end function package_name_to_complete(str) matches = match(r"(import|using) ([a-zA-Z0-9]+)$", str) matches === nothing ? nothing : matches[2] end responses[:complete] = function response_complete(🙋::ClientRequest) try require_notebook(🙋) catch; return; end query = 🙋.body["query"] pos = lastindex(query) # the query is cut at the cursor position by the front-end, so the cursor position is just the last legal index results, loc, found = if package_name_to_complete(query) !== nothing p = package_name_to_complete(query) cs = package_completions(p) |> sort [(c,"package",true) for c in cs], (nextind(query, pos-length(p)):pos), true else workspace = WorkspaceManager.get_workspace((🙋.session, 🙋.notebook); allow_creation=false) if will_run_code(🙋.notebook) && workspace isa WorkspaceManager.Workspace && isready(workspace.dowork_token) # we don't use eval_format_fetch_in_workspace because we don't want the output to be string-formatted. # This works in this particular case, because the return object, a `Completion`, exists in this scope too. Malt.remote_eval_fetch(workspace.worker, quote PlutoRunner.completion_fetcher( $query, $pos, getfield(Main, $(QuoteNode(workspace.module_name))), ) end) else # We can at least autocomplete general julia things: PlutoRunner.completion_fetcher(query, pos, Main) end end start_utf8 = loc.start stop_utf8 = nextind(query, pos) # advance one unicode char, js uses exclusive upper bound msg = UpdateMessage(:completion_result, Dict( :start => start_utf8 - 1, # 1-based index (julia) to 0-based index (js) :stop => stop_utf8 - 1, # idem :results => results ), 🙋.notebook, nothing, 🙋.initiator) putclientupdates!(🙋.session, 🙋.initiator, msg) end responses[:complete_symbols] = function response_complete_symbols(🙋::ClientRequest) msg = UpdateMessage(:completion_result, Dict( :latex => REPL.REPLCompletions.latex_symbols, :emoji => REPL.REPLCompletions.emoji_symbols, ), 🙋.notebook, nothing, 🙋.initiator) putclientupdates!(🙋.session, 🙋.initiator, msg) end responses[:docs] = function response_docs(🙋::ClientRequest) require_notebook(🙋) query = 🙋.body["query"] # Expand string macro calls to their macro form: # `html"` should yield `@html_str` and # `Markdown.md"` should yield `@Markdown.md_str`. (Ideally `Markdown.@md_str` but the former is easier) if endswith(query, '"') && query != "\"" query = string("@", SubString(query, firstindex(query), prevind(query, lastindex(query))), "_str") end workspace = WorkspaceManager.get_workspace((🙋.session, 🙋.notebook); allow_creation=false) query_as_symbol = Symbol(query) base_binding = Docs.Binding(Base, query_as_symbol) doc_md = Docs.doc(base_binding) doc_html, status = if doc_md isa Markdown.MD && haskey(doc_md.meta, :results) && !isempty(doc_md.meta[:results]) # available in Base, no need to ask worker PlutoRunner.improve_docs!(doc_md, query_as_symbol, base_binding) (repr(MIME("text/html"), doc_md), :👍) else if will_run_code(🙋.notebook) && workspace isa WorkspaceManager.Workspace && isready(workspace.dowork_token) Malt.remote_eval_fetch(workspace.worker, quote PlutoRunner.doc_fetcher( $query, getfield(Main, $(QuoteNode(workspace.module_name))), ) end) else (nothing, :⌛) end end msg = UpdateMessage(:doc_result, Dict( :status => status, :doc => doc_html, ), 🙋.notebook, nothing, 🙋.initiator) putclientupdates!(🙋.session, 🙋.initiator, msg) end :/opt/julia/packages/Pluto/GVuR6/src/webserver/WebServer.jl9?import MsgPack import UUIDs: UUID import HTTP import Sockets import .PkgCompat function open_in_default_browser(url::AbstractString)::Bool try if Sys.isapple() Base.run(`open $url`) true elseif Sys.iswindows() || detectwsl() Base.run(`powershell.exe Start "'$url'"`) true elseif Sys.islinux() Base.run(`xdg-open $url`, devnull, devnull, devnull) true else false end catch ex false end end function swallow_exception(f, exception_type::Type{T}) where {T} try f() catch e isa(e, T) || rethrow(e) end end """ Pluto.run() Start Pluto! ## Keyword arguments You can configure some of Pluto's more technical behaviour using keyword arguments, but this is mostly meant to support testing and strange setups like Docker. If you want to do something exciting with Pluto, you can probably write a creative notebook to do it! Pluto.run(; kwargs...) For the full list, see the [`Pluto.Configuration`](@ref) module. Some **common parameters**: - `launch_browser`: Optional. Whether to launch the system default browser. Disable this on SSH and such. - `host`: Optional. The default `host` is `"127.0.0.1"`. For wild setups like Docker and heroku, you might need to change this to `"0.0.0.0"`. - `port`: Optional. The default `port` is `1234`. - `auto_reload_from_file`: Reload when the `.jl` file is modified. The default is `false`. ## Technobabble This will start the static HTTP server and a WebSocket server. The server runs _synchronously_ (i.e. blocking call) on `http://[host]:[port]/`. Pluto notebooks can be started from the main menu in the web browser. """ function run(; kwargs...) options = Configuration.from_flat_kwargs(; kwargs...) run(options) end function run(options::Configuration.Options) session = ServerSession(; options) run(session) end # Deprecation errors function run(host::String, port::Union{Nothing,Integer} = nothing; kwargs...) @error """run(host, port) is deprecated in favor of: run(;host="$host", port=$port) """ end function run(port::Integer; kwargs...) @error "Oopsie! This is the old command to launch Pluto. The new command is: Pluto.run() without the port as argument - it will choose one automatically. If you need to specify the port, use: Pluto.run(port=$port) " end const is_first_run = Ref(true) "Return a port and serversocket to use while taking into account the `favourite_port`." function port_serversocket(hostIP::Sockets.IPAddr, favourite_port, port_hint) local port, serversocket if favourite_port === nothing port, serversocket = Sockets.listenany(hostIP, UInt16(port_hint)) else port = UInt16(favourite_port) try serversocket = Sockets.listen(hostIP, port) catch e error("Cannot listen on port $port. It may already be in use, or you may not have sufficient permissions. Use Pluto.run() to automatically select an available port.") end end return port, serversocket end struct RunningPlutoServer http_server initial_registry_update_task::Task end function Base.close(ssc::RunningPlutoServer) close(ssc.http_server) wait(ssc.http_server) wait(ssc.initial_registry_update_task) end function Base.wait(ssc::RunningPlutoServer) try # create blocking call and switch the scheduler back to the server task, so that interrupts land there while isopen(ssc.http_server) sleep(.1) end catch e println() println() Base.close(ssc) (e isa InterruptException) || rethrow(e) end nothing end """ run(session::ServerSession) Specifiy the [`Pluto.ServerSession`](@ref) to run the web server on, which includes the configuration. Passing a session as argument allows you to start the web server with some notebooks already running. See [`SessionActions`](@ref) to learn more about manipulating a `ServerSession`. """ function run(session::ServerSession) Base.wait(run!(session)) end function run!(session::ServerSession) if is_first_run[] is_first_run[] = false @info "Loading..." end if VERSION < v"1.6.2" @warn("\nPluto is running on an old version of Julia ($(VERSION)) that is no longer supported. Visit https://julialang.org/downloads/ for more information about upgrading Julia.") end pluto_router = http_router_for(session) store_session_middleware = create_session_context_middleware(session) app = pluto_router |> auth_middleware |> store_session_middleware let n = session.options.server.notebook SessionActions.open.((session,), n === nothing ? [] : n isa AbstractString ? [n] : n; run_async=true, ) end host = session.options.server.host hostIP = parse(Sockets.IPAddr, host) favourite_port = session.options.server.port port_hint = session.options.server.port_hint local port, serversocket = port_serversocket(hostIP, favourite_port, port_hint) on_shutdown() = @sync begin # Triggered by HTTP.jl @info("\nClosing Pluto... Restart Julia for a fresh session. \n\nHave a nice day! 🎈\n\n") # TODO: put do_work tokens back @async swallow_exception(() -> close(serversocket), Base.IOError) for client in values(session.connected_clients) @async swallow_exception(() -> close(client.stream), Base.IOError) end empty!(session.connected_clients) for nb in values(session.notebooks) @asynclog SessionActions.shutdown(session, nb; keep_in_session=false, async=false, verbose=false) end end server = HTTP.listen!(hostIP, port; stream=true, server=serversocket, on_shutdown, verbose=-1) do http::HTTP.Stream # messy messy code so that we can use the websocket on the same port as the HTTP server if HTTP.WebSockets.isupgrade(http.message) secret_required = let s = session.options.security s.require_secret_for_access || s.require_secret_for_open_links end if !secret_required || is_authenticated(session, http.message) try HTTP.WebSockets.upgrade(http) do clientstream if HTTP.WebSockets.isclosed(clientstream) return end try for message in clientstream # This stream contains data received over the WebSocket. # It is formatted and MsgPack-encoded by send(...) in PlutoConnection.js local parentbody = nothing local did_read = false try parentbody = unpack(message) let lag = session.options.server.simulated_lag (lag > 0) && sleep(lag * (0.5 + rand())) # sleep(0) would yield to the process manager which we dont want end did_read = true process_ws_message(session, parentbody, clientstream) catch ex if ex isa InterruptException || ex isa HTTP.WebSockets.WebSocketError || ex isa EOFError # that's fine! else bt = catch_backtrace() if did_read @warn "Processing message failed for unknown reason:" parentbody exception = (ex, bt) else @warn "Reading WebSocket client stream failed for unknown reason:" parentbody exception = (ex, bt) end end end end catch ex if ex isa InterruptException || ex isa HTTP.WebSockets.WebSocketError || ex isa EOFError || (ex isa Base.IOError && occursin("connection reset", ex.msg)) # that's fine! else bt = stacktrace(catch_backtrace()) @warn "Reading WebSocket client stream failed for unknown reason:" exception = (ex, bt) end end end catch ex if ex isa InterruptException # that's fine! elseif ex isa Base.IOError # that's fine! elseif ex isa ArgumentError && occursin("stream is closed", ex.msg) # that's fine! else bt = stacktrace(catch_backtrace()) @warn "HTTP upgrade failed for unknown reason" exception = (ex, bt) end end else try HTTP.setstatus(http, 403) HTTP.startwrite(http) HTTP.closewrite(http) catch e if !(e isa Base.IOError) rethrow(e) end end end else # then it's a regular HTTP request, not a WS upgrade request::HTTP.Request = http.message request.body = read(http) # HTTP.closeread(http) # If a "token" url parameter is passed in from binder, then we store it to add to every URL (so that you can share the URL to collaborate). params = HTTP.queryparams(HTTP.URI(request.target)) if haskey(params, "token") && params["token"] ∉ ("null", "undefined", "") && session.binder_token === nothing session.binder_token = params["token"] end response_body = app(request) request.response::HTTP.Response = response_body request.response.request = request try HTTP.setheader(http, "Content-Length" => string(length(request.response.body))) # https://github.com/fonsp/Pluto.jl/pull/722 HTTP.setheader(http, "Referrer-Policy" => "same-origin") # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#:~:text=is%202%20minutes.-,14.38%20Server HTTP.setheader(http, "Server" => "Pluto.jl/$(PLUTO_VERSION_STR[2:end]) Julia/$(JULIA_VERSION_STR[2:end])") HTTP.startwrite(http) write(http, request.response.body) catch e if isa(e, Base.IOError) || isa(e, ArgumentError) # @warn "Attempted to write to a closed stream at $(request.target)" else rethrow(e) end end end end server_running() = try HTTP.get("http://$(hostIP):$(port)$(session.options.server.base_url)ping"; status_exception = false, retry = false, connect_timeout = 10, readtimeout = 10).status == 200 catch false end # Wait for the server to start up before opening the browser. We have a 5 second grace period for allowing the connection, and then 10 seconds for the server to write data. WorkspaceManager.poll(server_running, 5.0, 1.0) address = pretty_address(session, hostIP, port) if session.options.server.launch_browser && open_in_default_browser(address) @info("\nOpening $address in your default browser... ~ have fun!") else @info("\nGo to $address in your browser to start writing ~ have fun!") end @info("\nPress Ctrl+C in this terminal to stop Pluto\n\n") # Trigger ServerStartEvent with server details try_event_call(session, ServerStartEvent(address, port)) if PLUTO_VERSION >= v"0.17.6" && frontend_directory() == "frontend" @info("It looks like you are developing the Pluto package, using the unbundled frontend...") end # Start this in the background, so that the first notebook launch (which will trigger registry update) will be faster initial_registry_update_task = @asynclog withtoken(pkg_token) do will_update = !PkgCompat.check_registry_age() PkgCompat.update_registries(; force = false) will_update && println(" Updating registry done ✓") end return RunningPlutoServer(server, initial_registry_update_task) end precompile(run, (ServerSession, HTTP.Handlers.Router{Symbol("##001")})) function pretty_address(session::ServerSession, hostIP, port) root = if session.options.server.root_url !== nothing @assert endswith(session.options.server.root_url, "/") replace(session.options.server.root_url, "{PORT}" => string(Int(port))) elseif haskey(ENV, "JH_APP_URL") "$(ENV["JH_APP_URL"])proxy/$(Int(port))/" else host_str = string(hostIP) host_pretty = if isa(hostIP, Sockets.IPv6) if host_str == "::1" "localhost" else "[$(host_str)]" end elseif host_str == "127.0.0.1" # Assuming the other alternative is IPv4 "localhost" else host_str end port_pretty = Int(port) base_url = session.options.server.base_url "http://$(host_pretty):$(port_pretty)$(base_url)" end url_params = Dict{String,String}() if session.options.security.require_secret_for_access url_params["secret"] = session.secret end fav_notebook = let n = session.options.server.notebook n isa AbstractVector ? (isempty(n) ? nothing : first(n)) : n end new_root = if fav_notebook !== nothing # since this notebook already started running, this will get redicted to that session url_params["path"] = string(fav_notebook) root * "open" else root end string(HTTP.URI(HTTP.URI(new_root); query = url_params)) end "All messages sent over the WebSocket get decoded+deserialized and end up here." function process_ws_message(session::ServerSession, parentbody::Dict, clientstream) client_id = Symbol(parentbody["client_id"]) client = get!(session.connected_clients, client_id ) do ClientSession(client_id, clientstream, session.options.server.simulated_lag) end client.stream = clientstream # it might change when the same client reconnects messagetype = Symbol(parentbody["type"]) request_id = Symbol(parentbody["request_id"]) notebook = if haskey(parentbody, "notebook_id") && parentbody["notebook_id"] !== nothing notebook = let notebook_id = UUID(parentbody["notebook_id"]) get(session.notebooks, notebook_id, nothing) end if messagetype === :connect if notebook === nothing messagetype === :connect || @warn "Remote notebook not found locally!" else client.connected_notebook = notebook end end notebook else nothing end body = parentbody["body"] if haskey(responses, messagetype) responsefunc = responses[messagetype] try responsefunc(ClientRequest(session, notebook, body, Initiator(client, request_id))) catch ex @warn "Response function to message of type $(repr(messagetype)) failed" rethrow(ex) end else @warn "Message of type $(messagetype) not recognised" end end 1/opt/julia/packages/Pluto/GVuR6/src/precompile.jldusing PrecompileTools: PrecompileTools PrecompileTools.@compile_workload begin nb = Pluto.Notebook([ Pluto.Cell("""md"Hello *world*" """) Pluto.Cell("""[f(x)]""") Pluto.Cell("""x = 1""") Pluto.Cell( """ function f(z::Integer) z / 123 end """) Pluto.Cell( """ "asdf" begin while false local p = 123 try [(x,a...) for x in (a for a in b)] A.B.hello() do z @gensym z (z) -> z/:(z / z) end catch end end end """ ) ]) let topology = Pluto.updated_topology(nb.topology, nb, nb.cells) # Our reactive sorting algorithm. Pluto.topological_order(topology, topology.cell_order) end # let # io = IOBuffer() # # Notebook file format. # Pluto.save_notebook(io, nb) # seekstart(io) # Pluto.load_notebook_nobackup(io, "whatever.jl") # end let state1 = Pluto.notebook_to_js(nb) state2 = Pluto.notebook_to_js(nb) # MsgPack Pluto.unpack(Pluto.pack(state1)) # State diffing Pluto.Firebasey.diff(state1, state2) end s = Pluto.ServerSession(; options=Pluto.Configuration.from_flat_kwargs( disable_writing_notebook_files=true, workspace_use_distributed=false, auto_reload_from_file=false, run_notebook_on_load=false, lazy_workspace_creation=true, capture_stdout=false, ) ) end using PrecompileSignatures: @precompile_signatures @precompile_signatures(Pluto) o@