diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cbc026e816..ae55240c39 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,7 +15,7 @@ Thank you for reporting an issue about Pluto! Let's get it fixed! 3. 🤕 Try to clearly explain what the problem is, it might not be obvious to others! Instead of saying: "This does not work.", try to say: "I expected ..., but instead I am seeing ..." -🙋 But my issue is really simple, I don't want to make a screen recording / notebook! +🙋 But my issue is really simple, I don't want to make a screen recording / notebook! And why do you need a notebook file? > **Please do it anyways!** It is really difficult to know exactly what information we will need to solve the issue, and a video recording can save a lot of follow-up questions. > Similarly, a notebook file means that we can start testing the problem immediately, saving Pluto's developers a lot of time. diff --git a/.github/workflows/Bundle.yml b/.github/workflows/Bundle.yml index ff2c46bcec..d7a218e8f7 100644 --- a/.github/workflows/Bundle.yml +++ b/.github/workflows/Bundle.yml @@ -20,7 +20,7 @@ jobs: trigger: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # We use that PAT token instead of GITHUB_TOKEN because we are triggering another github action on the 'release' event. # Triggering a workflow from a workflow is only allowed if the relaying event is signed with a PAT. # See https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token @@ -42,11 +42,11 @@ jobs: git reset --hard $GITHUB_SHA # if this is a PR. then just checkout without fanciness - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: github.event_name == 'pull_request' # Do the actual bundling - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 17.x cache: "npm" diff --git a/.github/workflows/FrontendTest.yml b/.github/workflows/FrontendTest.yml index 99db72eeee..1c6ec28220 100644 --- a/.github/workflows/FrontendTest.yml +++ b/.github/workflows/FrontendTest.yml @@ -23,7 +23,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Makes thes `julia` command available - uses: julia-actions/setup-julia@v1 @@ -34,7 +34,7 @@ jobs: run: | julia --project=$GITHUB_WORKSPACE -e "using Pkg; Pkg.instantiate()" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.x" @@ -83,7 +83,7 @@ jobs: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" PUPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: test-screenshot-artifacts diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index 06f747e7fd..aa9c032acf 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -38,14 +38,14 @@ jobs: - { user: JuliaPluto, repo: PlutoSliderServer.jl } steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.julia-version }} arch: x64 - uses: julia-actions/julia-buildpkg@v1 - name: Clone Downstream - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} path: downstream diff --git a/.github/workflows/LaunchTest.yml b/.github/workflows/LaunchTest.yml index 7bac5abb94..b8340f50d2 100644 --- a/.github/workflows/LaunchTest.yml +++ b/.github/workflows/LaunchTest.yml @@ -18,7 +18,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Makes thes `julia` command available - uses: julia-actions/setup-julia@v1 diff --git a/.github/workflows/ReleaseTriggers.yml b/.github/workflows/ReleaseTriggers.yml index cad0e668c3..814c39dda8 100644 --- a/.github/workflows/ReleaseTriggers.yml +++ b/.github/workflows/ReleaseTriggers.yml @@ -14,7 +14,7 @@ jobs: matrix: repository: ['fonsp/pluto-on-binder', 'JuliaPluto/sample-notebook-previews'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | curl \ -X POST \ diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 359a6a6149..69287c8e97 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -34,12 +34,12 @@ jobs: fail-fast: false matrix: # We test quite a lot of versions because we do some OS and version specific things unfortunately - julia-version: ["1.6", "1.8", "1.10", "nightly"] # "~1.11.0-0"] + julia-version: ["1.6", "1.10", "~1.11.0-0"] #, "nightly"] # "~1.12.0-0"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Makes the `julia` command available - uses: julia-actions/setup-julia@v1 @@ -51,3 +51,9 @@ jobs: - uses: julia-actions/julia-runtest@v1 with: coverage: false + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-snapshots-${{ matrix.julia-version }}-${{ matrix.os }} + path: ${{ github.workspace }}/test/snapshots/*.html diff --git a/.github/workflows/TestBundledExport.yml b/.github/workflows/TestBundledExport.yml index 6cc8a31339..0ab892fd34 100644 --- a/.github/workflows/TestBundledExport.yml +++ b/.github/workflows/TestBundledExport.yml @@ -10,7 +10,7 @@ jobs: runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: diff --git a/.github/workflows/TypeScriptCheck.yml b/.github/workflows/TypeScriptCheck.yml index bf8b115bc2..83c99083be 100644 --- a/.github/workflows/TypeScriptCheck.yml +++ b/.github/workflows/TypeScriptCheck.yml @@ -20,13 +20,13 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "21.x" - - run: npm install typescript@5.0.4 -g + - run: npm install typescript@5.4.3 -g - run: npm install working-directory: frontend diff --git a/.gitignore b/.gitignore index 4f073338b4..ebc55d33c5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ frontend/package-lock.json # PProf profile.pb.gz + +test/snapshots diff --git a/Project.toml b/Project.toml index c9aa8629ef..16c58d7c8d 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "Pluto" uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781" license = "MIT" authors = ["Fons van der Plas "] -version = "0.19.39" +version = "0.19.40" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -42,7 +42,7 @@ Dates = "0, 1" Downloads = "1" ExpressionExplorer = "0.5, 0.6, 1" FileWatching = "1" -FuzzyCompletions = "0.3, 0.4, 0.5" +FuzzyCompletions = "=0.5.4" HTTP = "^1.5.2" HypertextLiteral = "0.7, 0.8, 0.9" InteractiveUtils = "1" @@ -55,7 +55,7 @@ MsgPack = "1.1" Pkg = "1" PlutoDependencyExplorer = "~1.0" PrecompileSignatures = "3" -PrecompileTools = "1" +PrecompileTools = "=1.2.1" REPL = "1" RegistryInstances = "0.1" RelocatableFolders = "0.1, 0.2, 0.3, 1" diff --git a/frontend/common/parse_launch_params.js b/frontend/common/parse_launch_params.js new file mode 100644 index 0000000000..fe801ec0d9 --- /dev/null +++ b/frontend/common/parse_launch_params.js @@ -0,0 +1,38 @@ +/** + * + * @return {import("../components/Editor.js").LaunchParameters} + */ +export const parse_launch_params = () => { + const url_params = new URLSearchParams(window.location.search) + + return { + //@ts-ignore + notebook_id: url_params.get("id") ?? window.pluto_notebook_id, + //@ts-ignore + statefile: url_params.get("statefile") ?? window.pluto_statefile, + //@ts-ignore + statefile_integrity: url_params.get("statefile_integrity") ?? window.pluto_statefile_integrity, + //@ts-ignore + notebookfile: url_params.get("notebookfile") ?? window.pluto_notebookfile, + //@ts-ignore + notebookfile_integrity: url_params.get("notebookfile_integrity") ?? window.pluto_notebookfile_integrity, + //@ts-ignore + disable_ui: !!(url_params.get("disable_ui") ?? window.pluto_disable_ui), + //@ts-ignore + preamble_html: url_params.get("preamble_html") ?? window.pluto_preamble_html, + //@ts-ignore + isolated_cell_ids: url_params.has("isolated_cell_id") ? url_params.getAll("isolated_cell_id") : window.pluto_isolated_cell_ids, + //@ts-ignore + binder_url: url_params.get("binder_url") ?? window.pluto_binder_url, + //@ts-ignore + pluto_server_url: url_params.get("pluto_server_url") ?? window.pluto_pluto_server_url, + //@ts-ignore + slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url, + //@ts-ignore + recording_url: url_params.get("recording_url") ?? window.pluto_recording_url, + //@ts-ignore + recording_url_integrity: url_params.get("recording_url_integrity") ?? window.pluto_recording_url_integrity, + //@ts-ignore + recording_audio_url: url_params.get("recording_audio_url") ?? window.pluto_recording_audio_url, + } +} diff --git a/frontend/common/useEventListener.js b/frontend/common/useEventListener.js index 94b8911846..86fe78156f 100644 --- a/frontend/common/useEventListener.js +++ b/frontend/common/useEventListener.js @@ -1,7 +1,7 @@ import { useCallback, useEffect } from "../imports/Preact.js" export const useEventListener = ( - /** @type {Document | HTMLElement | Window | null} */ element, + /** @type {Document | HTMLElement | Window | EventSource | MediaQueryList | null} */ element, /** @type {string} */ event_name, /** @type {EventListenerOrEventListenerObject} */ handler, /** @type {any[] | undefined} */ deps diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index b48f265df6..3bdff6358d 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -63,6 +63,7 @@ import { moveLineDown } from "../imports/CodemirrorPlutoSetup.js" export const ENABLE_CM_MIXED_PARSER = window.localStorage.getItem("ENABLE_CM_MIXED_PARSER") === "true" export const ENABLE_CM_SPELLCHECK = window.localStorage.getItem("ENABLE_CM_SPELLCHECK") === "true" +export const ENABLE_CM_AUTOCOMPLETE_ON_TYPE = window.localStorage.getItem("ENABLE_CM_AUTOCOMPLETE_ON_TYPE") === "true" if (ENABLE_CM_MIXED_PARSER) { console.log(`YOU ENABLED THE CODEMIRROR MIXED LANGUAGE PARSER @@ -85,6 +86,12 @@ window.PLUTO_TOGGLE_CM_SPELLCHECK = (val = !ENABLE_CM_SPELLCHECK) => { window.location.reload() } +// @ts-ignore +window.PLUTO_TOGGLE_CM_AUTOCOMPLETE_ON_TYPE = (val = !ENABLE_CM_AUTOCOMPLETE_ON_TYPE) => { + window.localStorage.setItem("ENABLE_CM_AUTOCOMPLETE_ON_TYPE", String(val)) + window.location.reload() +} + export const pluto_syntax_colors = HighlightStyle.define( [ /* The following three need a specific version of the julia parser, will add that later (still messing with it 😈) */ @@ -414,15 +421,17 @@ export const CellInput = ({ }, [on_change]) ) - useLayoutEffect(() => { + useLayoutEffect(function cellinput_setup_codemirror() { if (dom_node_ref.current == null) return - const keyMapSubmit = () => { + const keyMapSubmit = (/** @type {EditorView} */ cm) => { + autocomplete.closeCompletion(cm) on_submit() return true } let run = async (fn) => await fn() const keyMapRun = (/** @type {EditorView} */ cm) => { + autocomplete.closeCompletion(cm) run(async () => { // we await to prevent an out-of-sync issue await on_add_after() @@ -742,6 +751,7 @@ export const CellInput = ({ results: message.results, } }, + request_special_symbols: () => pluto_actions.send("complete_symbols").then(({ message }) => message), on_update_doc_query: on_update_doc_query, }), diff --git a/frontend/components/CellInput/LiveDocsFromCursor.js b/frontend/components/CellInput/LiveDocsFromCursor.js index 6e89f344f5..a2cd0ffdbf 100644 --- a/frontend/components/CellInput/LiveDocsFromCursor.js +++ b/frontend/components/CellInput/LiveDocsFromCursor.js @@ -244,8 +244,7 @@ export let get_selected_doc_from_state = (/** @type {EditorState} */ state, verb if ( cursor.name === "Identifier" && parent.name === "ArgumentList" && - (parent.parent.parent.name === "FunctionAssignmentExpression" || - parent.parent.name === "FunctionDefinition") + (parent.parent.parent.name === "FunctionAssignmentExpression" || parent.parent.name === "FunctionDefinition") ) { continue } @@ -297,7 +296,6 @@ export let get_selected_doc_from_state = (/** @type {EditorState} */ state, verb if (VALID_DOCS_TYPES.includes(cursor.name) || keywords_that_have_docs_and_are_cool.includes(cursor.name)) { if (!is_docs_searchable(cursor)) { - console.log("NOT DOCS SEARCHABLE") return undefined } diff --git a/frontend/components/CellInput/cell_movement_plugin.js b/frontend/components/CellInput/cell_movement_plugin.js index 1e752cfbe6..26b4de2d94 100644 --- a/frontend/components/CellInput/cell_movement_plugin.js +++ b/frontend/components/CellInput/cell_movement_plugin.js @@ -1,8 +1,5 @@ import { EditorView, autocomplete, EditorState, keymap } from "../../imports/CodemirrorPlutoSetup.js" -// Why am I like this? -let completionState = autocomplete.autocompletion()[0] - /** * Cell movement plugin! * @@ -148,8 +145,8 @@ export let prevent_holding_a_key_from_doing_things_across_cells = EditorView.dom // Because of the "hacky" way this works, we need to check if autocompletion is open... // else we'll block the ability to press ArrowDown for autocomplete.... - // Adopted from https://github.com/codemirror/autocomplete/blob/a53f7ff19dc3a0412f3ce6e2751b08b610e1d762/src/view.ts#L15 - let autocompletion_open = view.state.field(completionState, false)?.open ?? false + + let autocompletion_open = autocomplete.completionStatus(view.state) === "active" // If we have a cursor instead of a multicharacter selection: if (event.key === "ArrowUp" && !autocompletion_open) { diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index 7b9f5f8ad3..0df4c5b6c4 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -1,45 +1,17 @@ import _ from "../../imports/lodash.js" -import { utf8index_to_ut16index } from "../../common/UnicodeTools.js" - -import { - EditorState, - EditorSelection, - EditorView, - keymap, - indentMore, - autocomplete, - syntaxTree, - StateField, - StateEffect, -} from "../../imports/CodemirrorPlutoSetup.js" +import { EditorView, keymap, autocomplete, syntaxTree, StateField, StateEffect, Transaction } from "../../imports/CodemirrorPlutoSetup.js" import { get_selected_doc_from_state } from "./LiveDocsFromCursor.js" import { cl } from "../../common/ClassTable.js" import { ScopeStateField } from "./scopestate_statefield.js" import { open_bottom_right_panel } from "../BottomRightPanel.js" +import { ENABLE_CM_AUTOCOMPLETE_ON_TYPE } from "../CellInput.js" +import { GlobalDefinitionsFacet } from "./go_to_definition_plugin.js" let { autocompletion, completionKeymap, completionStatus, acceptCompletion } = autocomplete -// Option.source is now the source, we find to find the corresponding ActiveResult -// https://github.com/codemirror/autocomplete/commit/6d9f24115e9357dc31bc265cd3da7ce2287fdcbd -const getActiveResult = (view, source) => view.state.field(completionState).active.find((a) => a.source == source) - // These should be imported from @codemirror/autocomplete, but they are not exported. -let completionState = autocompletion()[0] -let applyCompletion = (/** @type {EditorView} */ view, option) => { - let apply = option.completion.apply || option.completion.label - let result = getActiveResult(view, option.source) - if (!result?.from) return - if (typeof apply == "string") { - view.dispatch({ - changes: { from: result.from, to: result.to, insert: apply }, - selection: { anchor: result.from + apply.length }, - userEvent: "input.complete", - }) - } else { - apply(view, option.completion, result.from, result.to) - } -} +const completionState = autocompletion()[1] /** @type {any} */ const TabCompletionEffect = StateEffect.define() @@ -48,19 +20,21 @@ const tabCompletionState = StateField.define({ return false }, - update(value, tr) { + update(value, /** @type {Transaction} */ tr) { // Tab was pressed for (let effect of tr.effects) { if (effect.is(TabCompletionEffect)) return true } + if (!value) return false + + let previous_selected = autocomplete.selectedCompletion(tr.startState) + let current_selected = autocomplete.selectedCompletion(tr.state) + // Autocomplete window was closed - if (tr.startState.field(completionState, false)?.open != null && tr.state.field(completionState, false)?.open == null) { + if (previous_selected != null && current_selected == null) { return false } - if ( - tr.startState.field(completionState, false).open != null && - tr.startState.field(completionState, false) !== tr.state.field(completionState, false) - ) { + if (previous_selected != null && previous_selected !== current_selected) { return false } return value @@ -82,11 +56,15 @@ const tab_completion_command = (cm) => { } let selection = cm.state.selection.main + if (!selection.empty) return false + let last_char = cm.state.sliceDoc(selection.from - 1, selection.from) + let last_line = cm.state.sliceDoc(cm.state.doc.lineAt(selection.from).from, selection.from) - if (!selection.empty) return false // Some exceptions for when to trigger tab autocomplete - if (/^(\t| |\n|\=|\)|)$/.test(last_char)) return false + if ("\t \n=".includes(last_char)) return false + // ?([1,2], 3) should trigger autocomplete + if (last_char === ")" && !last_line.includes("?")) return false cm.dispatch({ effects: TabCompletionEffect.of(10), @@ -97,8 +75,7 @@ const tab_completion_command = (cm) => { // Remove this if we find that people actually need the `?` in their queries, but I very much doubt it. // (Also because the ternary operator does require a space before the ?, thanks Julia!) let open_docs_if_autocomplete_is_open_command = (cm) => { - let autocompletion_open = cm.state.field(completionState, false)?.open ?? false - if (autocompletion_open) { + if (autocomplete.completionStatus(cm.state) != null) { open_bottom_right_panel("docs") return true } @@ -135,7 +112,9 @@ let update_docs_from_autocomplete_selection = (on_update_doc_query) => { let text_to_apply = selected_option.completion.apply ?? selected_option.completion.label if (typeof text_to_apply !== "string") return - const active_result = getActiveResult(update.view, selected_option.source) + // Option.source is now the source, we find to find the corresponding ActiveResult + // https://github.com/codemirror/autocomplete/commit/6d9f24115e9357dc31bc265cd3da7ce2287fdcbd + const active_result = update.view.state.field(completionState).active.find((a) => a.source == selected_option.source) if (!active_result?.from) return // not an ActiveResult instance const from = active_result.from, @@ -161,11 +140,9 @@ let update_docs_from_autocomplete_selection = (on_update_doc_query) => { } /** Are we matching something like `\lambd...`? */ -let match_latex_complete = (ctx) => ctx.matchBefore(/\\[^\s"'.`]*/) +let match_special_symbol_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/\\[\d\w_:]*/) /** Are we matching something like `:writing_a_symbo...`? */ -let match_symbol_complete = (ctx) => ctx.matchBefore(/\.\:[^\s"'`()\[\].]*/) -/** Are we matching exactly `~/`? */ -let match_expanduser_complete = (ctx) => ctx.matchBefore(/~\//) +let match_symbol_complete = (/** @type {autocomplete.CompletionContext} */ ctx) => ctx.matchBefore(/\.\:[^\s"'`()\[\].]*/) /** Are we matching inside a string */ function match_string_complete(ctx) { const tree = syntaxTree(ctx.state) @@ -176,168 +153,211 @@ function match_string_complete(ctx) { return true } -/** Use the completion results from the Julia server to create CM completion objects, but only for path completions (TODO: broken) and latex completions. */ -let julia_special_completions_to_cm = (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (ctx) => { - let to_complete = ctx.state.sliceDoc(0, ctx.pos) - - let found = await request_autocomplete({ text: to_complete }) - if (!found) return null - let { start, stop, results } = found - - let should_apply_unicode_completion = !match_string_complete(ctx) +let override_text_to_apply_in_field_expression = (text) => { + return !/^[@\p{L}\p{Sc}\d_][\p{L}\p{Nl}\p{Sc}\d_!]*"?$/u.test(text) ? (text === ":" ? `:(${text})` : `:${text}`) : null +} - return { - from: start, - to: stop, - // This is an important one when you not only complete, but also replace something. - // @codemirror/autocomplete automatically filters out results otherwise >:( - filter: false, - options: results.map(([text, _, __, ___, ____, detail]) => { - return { - label: text, - apply: detail && should_apply_unicode_completion ? detail : text, - detail: detail ?? undefined, - } - }), - // TODO Do something docs_prefix ish when we also have the apply text - } +const section_regular = { + name: "Suggestions", + header: () => document.createElement("div"), + rank: 0, } -let override_text_to_apply_in_field_expression = (text) => { - return !/^[@a-zA-Z_][a-zA-Z0-9!_]*\"?$/.test(text) ? (text === ":" ? `:(${text})` : `:${text}`) : null +const section_operators = { + name: "Operators", + rank: 1, } -/** - * @param {Map} definitions - * @param {Set} proposed - * @param {number} context_pos - */ -const generate_scopestate_completions = function* (definitions, proposed, context_pos) { - let i = 0 - for (let [name, { valid_from }] of definitions.entries()) { - if (!proposed.has(name) && valid_from < context_pos) { - yield { - label: name, - type: "c_Any", - boost: 99 - i, - } - i += 1 - } - } +const field_rank_heuristic = (text, is_exported) => is_exported * 3 + (/^\p{Ll}/u.test(text) ? 2 : /^\p{Lu}/u.test(text) ? 1 : 0) + +const julia_commit_characters = [".", ",", "(", "[", "{"] +const endswith_keyword_regex = + /(baremodule|begin|break|catch|const|continue|do|else|elseif|end|export|false|finally|for|function|global|if|import|let|local|macro|module|quote|return|struct|true|try|using|while)$/ + +const validFor = (text) => { + let expected_char = /[\p{L}\p{Nl}\p{Sc}\d_!]*$/u.test(text) + + return expected_char && !endswith_keyword_regex.test(text) } /** Use the completion results from the Julia server to create CM completion objects. */ -const julia_code_completions_to_cm = (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (ctx) => { - let to_complete = ctx.state.sliceDoc(0, ctx.pos) - - // Another rough hack... If it detects a `.:`, we want to cut out the `:` so we get all results from julia, - // but then codemirror will put the `:` back in filtering - let is_symbol_completion = match_symbol_complete(ctx) - if (is_symbol_completion) { - to_complete = to_complete.slice(0, is_symbol_completion.from + 1) + to_complete.slice(is_symbol_completion.from + 2) - } +const julia_code_completions_to_cm = + (/** @type {PlutoRequestAutocomplete} */ request_autocomplete) => async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (writing_variable_name_or_keyword(ctx)) return null + if (match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + + let to_complete = /** @type {String} */ (ctx.state.sliceDoc(0, ctx.pos)) + + // Another rough hack... If it detects a `.:`, we want to cut out the `:` so we get all results from julia, + // but then codemirror will put the `:` back in filtering + let is_symbol_completion = match_symbol_complete(ctx) + if (is_symbol_completion) { + to_complete = to_complete.slice(0, is_symbol_completion.from + 1) + to_complete.slice(is_symbol_completion.from + 2) + } - let found = await request_autocomplete({ text: to_complete }) - if (!found) return null - let { start, stop, results } = found + // no path autocompletions + if (ctx.tokenBefore(["String"]) != null) return null - if (is_symbol_completion) { - // If this is a symbol completion thing, we need to add the `:` back in by moving the end a bit furher - stop = stop + 1 - } + const globals = ctx.state.facet(GlobalDefinitionsFacet) + const is_already_a_global = (text) => text != null && Object.keys(globals).includes(text) - const definitions = ctx.state.field(ScopeStateField).definitions - const proposed = new Set() + let found = await request_autocomplete({ text: to_complete }) + if (!found) return null + let { start, stop, results } = found - let to_complete_onto = to_complete.slice(0, start) - let is_field_expression = to_complete_onto.slice(-1) === "." - return { - from: start, - to: stop, - - // This tells codemirror to not query this function again as long as the string - // we are completing has the same prefix as we complete now, and there is no weird characters (subjective) - // e.g. Base.ab, will create a regex like /^ab[^weird]*$/, so when now typing `s`, - // we'll get `Base.abs`, it finds the `abs` matching our span, and it will filter the existing results. - // If we backspace however, to `Math.a`, `a` does no longer match! So it will re-query this function. - // span: RegExp(`^${_.escapeRegExp(ctx.state.sliceDoc(start, stop))}[^\\s"'()\\[\\].{}]*`), - options: [ - ...results.map(([text, type_description, is_exported, is_from_notebook, completion_type], i) => { - // (quick) fix for identifiers that need to be escaped - // Ideally this is done with Meta.isoperator on the julia side - let text_to_apply = is_field_expression ? override_text_to_apply_in_field_expression(text) ?? text : text - - if (definitions.has(text)) proposed.add(text) - - return { - label: text, - apply: text_to_apply, - type: - cl({ - c_notexported: !is_exported, - [`c_${type_description}`]: type_description != null, - [`completion_${completion_type}`]: completion_type != null, - c_from_notebook: is_from_notebook, - }) ?? undefined, - boost: 50 - i / results.length, - } - }), - // This is a small thing that I really want: - // You want to see what fancy symbols a module has? Pluto will show these at the very end of the list, - // for Base there is no way you're going to find them! With this you can type `.:` and see all the fancy symbols. - // TODO This whole block shouldn't use `override_text_to_apply_in_field_expression` but the same - // `Meta.isoperator` thing mentioned above - ...results - .filter(([text]) => is_field_expression && override_text_to_apply_in_field_expression(text) != null) - .map(([text, type_description, is_exported], i) => { - let text_to_apply = override_text_to_apply_in_field_expression(text) ?? "" - - return { - label: text_to_apply, - apply: text_to_apply, - type: (is_exported ? "" : "c_notexported ") + (type_description == null ? "" : "c_" + type_description), - boost: -99 - i / results.length, // Display below all normal results - // Non-standard - is_not_exported: !is_exported, - } - }), - - ...Array.from(generate_scopestate_completions(definitions, proposed, ctx.pos)), - ], - } -} - -const pluto_completion_fetcher = (request_autocomplete) => { - const unicode_completions = julia_special_completions_to_cm(request_autocomplete) - const code_completions = julia_code_completions_to_cm(request_autocomplete) + if (is_symbol_completion) { + // If this is a symbol completion thing, we need to add the `:` back in by moving the end a bit furher + stop = stop + 1 + } - return (ctx) => { - let unicode_match = match_latex_complete(ctx) || match_expanduser_complete(ctx) - if (unicode_match === null) { - return code_completions(ctx) - } else { - return unicode_completions(ctx) + // const definitions = ctx.state.field(ScopeStateField).definitions + // console.debug({ definitions }) + // const proposed = new Set() + + let to_complete_onto = to_complete.slice(0, start) + let is_field_expression = to_complete_onto.endsWith(".") + let is_listing_all_fields_of_a_module = is_field_expression && start === stop + + return { + from: start, + to: stop, + + // This tells codemirror to not query this function again as long as the string matches the regex. + + // see `is_wc_cat_id_start` in Julia's source for a complete list + // validFor: /[\p{L}\p{Nl}\p{Sc}\d_!]*$/u, + validFor, + + commitCharacters: julia_commit_characters, + + options: [ + ...results + .filter(([text, _1, _2, is_from_notebook]) => !(is_from_notebook && is_already_a_global(text))) + .map(([text, value_type, is_exported, is_from_notebook, completion_type, _ignored], i) => { + // (quick) fix for identifiers that need to be escaped + // Ideally this is done with Meta.isoperator on the julia side + let text_to_apply = + completion_type === "method" ? to_complete : is_field_expression ? override_text_to_apply_in_field_expression(text) ?? text : text + + value_type = value_type === "Function" && text.startsWith("@") ? "Macro" : value_type + + return { + label: text, + apply: text_to_apply, + type: + cl({ + c_notexported: !is_exported, + [`c_${value_type}`]: true, + [`completion_${completion_type}`]: true, + c_from_notebook: is_from_notebook, + }) ?? undefined, + section: section_regular, + // detail: completion_type, + boost: + completion_type === "keyword_argument" ? 7 : is_field_expression ? field_rank_heuristic(text_to_apply, is_exported) : undefined, + // boost: 50 - i / results.length, + } + }), + // This is a small thing that I really want: + // You want to see what fancy symbols a module has? Pluto will show these at the very end of the list, + // for Base there is no way you're going to find them! With this you can type `.:` and see all the fancy symbols. + // TODO This whole block shouldn't use `override_text_to_apply_in_field_expression` but the same + // `Meta.isoperator` thing mentioned above + ...results + .filter(([text]) => is_field_expression && override_text_to_apply_in_field_expression(text) != null) + .map(([text, value_type, is_exported], i) => { + let text_to_apply = override_text_to_apply_in_field_expression(text) ?? "" + + return { + label: text_to_apply, + apply: text_to_apply, + type: (is_exported ? "" : "c_notexported ") + (value_type == null ? "" : "c_" + value_type), + // boost: -99 - i / results.length, // Display below all normal results + section: section_operators, + // Non-standard + is_not_exported: !is_exported, + } + }), + ], } } -} -const complete_anyword = async (ctx) => { +const complete_anyword = async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (writing_variable_name_or_keyword(ctx)) return null + if (match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + const results_from_cm = await autocomplete.completeAnyWord(ctx) if (results_from_cm === null) return null + const last_token = ctx.tokenBefore(["Identifier", "Number"]) + if (last_token == null || last_token.type?.name === "Number") return null + return { from: results_from_cm.from, + commitCharacters: julia_commit_characters, + options: results_from_cm.options.map(({ label }, i) => ({ // See https://github.com/codemirror/codemirror.next/issues/788 about `type: null` label, apply: label, type: undefined, - boost: 0 - i, + section: section_regular, + // boost: 0 - i, })), } } -const local_variables_completion = (ctx) => { +const from_notebook_type = "c_from_notebook completion_module c_Any" + +/** + * Are we currently writing a variable name? In that case we don't want autocomplete. + * + * E.g. `const hel` should not autocomplete. + */ +const writing_variable_name_or_keyword = (/** @type {autocomplete.CompletionContext} */ ctx) => { + let just_finished_a_keyword = ctx.matchBefore(endswith_keyword_regex) + + let after_keyword = ctx.matchBefore(/(catch|local|module|abstract type|struct|macro|const|for|function|let|do) [@\p{L}\p{Nl}\p{Sc}\d_!]*$/u) + + let inside_do_argument_expression = ctx.matchBefore(/do [\(\), \p{L}\p{Nl}\p{Sc}\d_!]*$/u) + + return just_finished_a_keyword || after_keyword || inside_do_argument_expression +} + +/** @returns {Promise} */ +const global_variables_completion = async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (writing_variable_name_or_keyword(ctx)) return null + if (match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number", "Comment", "String", "TripleString"]) != null) return null + + const globals = ctx.state.facet(GlobalDefinitionsFacet) + + // see `is_wc_cat_id_start` in Julia's source for a complete list + const there_is_a_dot_before = ctx.matchBefore(/\.[\p{L}\p{Nl}\p{Sc}\d_!]*$/u) + if (there_is_a_dot_before) return null + + const from_cm = await autocomplete.completeFromList( + Object.keys(globals).map((label) => { + return { + label, + apply: label, + type: from_notebook_type, + section: section_regular, + } + }) + )(ctx) + return from_cm == null + ? null + : { + ...from_cm, + validFor, + commitCharacters: julia_commit_characters, + } +} + +const local_variables_completion = (/** @type {autocomplete.CompletionContext} */ ctx) => { let scopestate = ctx.state.field(ScopeStateField) let unicode = ctx.tokenBefore(["Identifier"]) @@ -348,6 +368,7 @@ const local_variables_completion = (ctx) => { return { from, to, + commitCharacters: julia_commit_characters, options: scopestate.locals .filter( ({ validity, name }) => @@ -362,21 +383,95 @@ const local_variables_completion = (ctx) => { })), } } +const special_latex_examples = ["\\sqrt", "\\pi", "\\approx"] +const special_emoji_examples = ["🐶", "🐱", "🐭", "🐰", "🐼", "🐨", "🐸", "🐔", "🐧"] + +const special_symbols_completion = (/** @type {() => Promise} */ request_special_symbols) => { + let found = null + + const get_special_symbols = async () => { + if (found == null) { + let data = await request_special_symbols().catch((e) => { + console.warn("Failed to fetch special symbols", e) + return null + }) + + if (data != null) { + const { latex, emoji } = data + found = [true, false].map((is_inside_string) => + [true, false].flatMap((is_emoji) => + Object.entries(is_emoji ? emoji : latex).map(([label, value]) => { + return { + label, + apply: value != null && (!is_inside_string || is_emoji) ? value : label, + detail: value ?? undefined, + type: "c_special_symbol", + boost: label === "\\in" ? 3 : special_latex_examples.includes(label) ? 2 : special_emoji_examples.includes(value) ? 1 : 0, + } + }) + ) + ) + } + } + return found + } + + return async (/** @type {autocomplete.CompletionContext} */ ctx) => { + if (writing_variable_name_or_keyword(ctx)) return null + if (!match_special_symbol_complete(ctx)) return null + if (ctx.tokenBefore(["Number", "Comment"]) != null) return null + + const result = await get_special_symbols() + + let is_inside_string = match_string_complete(ctx) + return await autocomplete.completeFromList(is_inside_string ? result[0] : result[1])(ctx) + } +} + +const continue_completing_path = EditorView.updateListener.of((update) => { + for (let transaction of update.transactions) { + let picked_completion = transaction.annotation(autocomplete.pickedCompletion) + if (picked_completion) { + if ( + typeof picked_completion.apply === "string" && + picked_completion.apply.endsWith("/") && + picked_completion.type?.match(/(^| )completion_path( |$)/) + ) { + autocomplete.startCompletion(update.view) + } + } + } +}) /** + * + * @typedef PlutoAutocompleteResult + * @type {[ + * text: string, + * value_type: string, + * is_exported: boolean, + * is_from_notebook: boolean, + * completion_type: string, + * special_symbol: string | null, + * ]} + * * @typedef PlutoAutocompleteResults - * @type {{ start: number, stop: number, results: Array<[string, (string | null), boolean, boolean, (string | null), (string | null)]> }} + * @type {{ start: number, stop: number, results: Array }} * * @typedef PlutoRequestAutocomplete * @type {(options: { text: string }) => Promise} + * + * @typedef SpecialSymbols + * @type {{emoji: Record, latex: Record}} */ /** * @param {object} props * @param {PlutoRequestAutocomplete} props.request_autocomplete + * @param {() => Promise} props.request_special_symbols * @param {(query: string) => void} props.on_update_doc_query */ -export let pluto_autocomplete = ({ request_autocomplete, on_update_doc_query }) => { +export let pluto_autocomplete = ({ request_autocomplete, request_special_symbols, on_update_doc_query }) => { let last_query = null let last_result = null /** @@ -400,11 +495,11 @@ export let pluto_autocomplete = ({ request_autocomplete, on_update_doc_query }) return [ tabCompletionState, autocompletion({ - activateOnTyping: false, + activateOnTyping: ENABLE_CM_AUTOCOMPLETE_ON_TYPE, override: [ - pluto_completion_fetcher(memoize_last_request_autocomplete), - // julia_special_completions_to_cm(memoize_last_request_autocomplete), - // julia_code_completions_to_cm(memoize_last_request_autocomplete), + global_variables_completion, + special_symbols_completion(request_special_symbols), + julia_code_completions_to_cm(memoize_last_request_autocomplete), complete_anyword, // TODO: Disabled because of performance problems, see https://github.com/fonsp/Pluto.jl/pull/1925. Remove `complete_anyword` once fixed. See https://github.com/fonsp/Pluto.jl/pull/2013 // local_variables_completion, @@ -414,38 +509,7 @@ export let pluto_autocomplete = ({ request_autocomplete, on_update_doc_query }) optionClass: (c) => c.type ?? "", }), - // If there is just one autocomplete result, apply it directly - EditorView.updateListener.of((update) => { - // AGAIN, can't use this here again, because the currentCompletions *do not contain all the info to apply the completion* - // let open_completions = autocomplete.currentCompletions(update.state) - let autocompletion_state = update.state.field(completionState, false) - let is_tab_completion = update.state.field(tabCompletionState, false) - - if ( - autocompletion_state?.open != null && - is_tab_completion && - completionStatus(update.state) === "active" && - autocompletion_state.open.options.length === 1 - ) { - // We can't use `acceptCompletion` here because that function has a minimum delay of 75ms between creating the completion options and applying one. - applyCompletion(update.view, autocompletion_state.open.options[0]) - } - }), - - EditorView.updateListener.of((update) => { - for (let transaction of update.transactions) { - let picked_completion = transaction.annotation(autocomplete.pickedCompletion) - if (picked_completion) { - if ( - typeof picked_completion.apply === "string" && - picked_completion.apply.endsWith("/") && - picked_completion.type?.match(/(^| )completion_path( |$)/) - ) { - autocomplete.startCompletion(update.view) - } - } - } - }), + // continue_completing_path, update_docs_from_autocomplete_selection(on_update_doc_query), diff --git a/frontend/components/CellInput/scopestate_statefield.js b/frontend/components/CellInput/scopestate_statefield.js index 0a22b182ce..2478d5ba65 100644 --- a/frontend/components/CellInput/scopestate_statefield.js +++ b/frontend/components/CellInput/scopestate_statefield.js @@ -563,13 +563,13 @@ export let explore_variable_usage = ( // Don't ask me why, but currently `do (x, y)` is parsed as `DoClauseArguments(ArgumentList(x, y))` // while an actual argumentslist, `do x, y` is parsed as `DoClauseArguments(BareTupleExpression(x, y))` let do_args_actually = do_args.firstChild - if (do_args_actually.name === "Identifier") { + if (do_args_actually?.name === "Identifier") { inner_scope = scopestate_add_definition(inner_scope, doc, do_args_actually) - } else if (do_args_actually.name === "ArgumentList") { + } else if (do_args_actually?.name === "ArgumentList") { for (let child of child_nodes(do_args_actually)) { inner_scope = explorer_function_definition_argument(child, doc, inner_scope) } - } else if (do_args_actually.name === "BareTupleExpression") { + } else if (do_args_actually?.name === "BareTupleExpression") { for (let child of child_nodes(do_args_actually)) { inner_scope = explorer_function_definition_argument(child, doc, inner_scope) } diff --git a/frontend/components/CellOutput.js b/frontend/components/CellOutput.js index b2a6244b88..77af503d88 100644 --- a/frontend/components/CellOutput.js +++ b/frontend/components/CellOutput.js @@ -410,6 +410,11 @@ const execute_scripttags = async ({ root_node, script_nodes, previous_results_ma // @ts-ignore getPublishedObject: (id) => cell.getPublishedObject(id), + _internal_getJSLinkResponse: (cell_id, link_id) => (input) => + pluto_actions.request_js_link_response(cell_id, link_id, input).then(([success, result]) => { + if (success) return result + throw result + }), getBoundElementValueLikePluto: get_input_value, setBoundElementValueLikePluto: set_input_value, getBoundElementEventNameLikePluto: eventof, diff --git a/frontend/components/DiscreteProgressBar.js b/frontend/components/DiscreteProgressBar.js index 729de4b38c..69d8dd84d8 100644 --- a/frontend/components/DiscreteProgressBar.js +++ b/frontend/components/DiscreteProgressBar.js @@ -1,7 +1,7 @@ import { cl } from "../common/ClassTable.js" import { html, useEffect, useRef, useState } from "../imports/Preact.js" -export const DiscreteProgressBar = ({ total, done, busy }) => { +export const DiscreteProgressBar = ({ total, done, busy, failed_indices }) => { total = Math.max(1, total) return html` @@ -18,6 +18,7 @@ export const DiscreteProgressBar = ({ total, done, busy }) => { return html`
= done && i < done + busy, })} >
` diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 83cdf33b63..7d390faba4 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -145,6 +145,7 @@ const first_true_key = (obj) => { * @typedef StatusEntryData * @type {{ * name: string, + * success?: boolean, * started_at: number?, * finished_at: number?, * timing?: "remote" | "local", @@ -662,6 +663,19 @@ export class Editor extends Component { false ) }, + request_js_link_response: (cell_id, link_id, input) => { + return this.client + .send( + "request_js_link_response", + { + cell_id, + link_id, + input, + }, + { notebook_id: this.state.notebook.notebook_id } + ) + .then((r) => r.message) + }, /** This actions avoids pushing selected cells all the way down, which is too heavy to handle! */ get_selected_cells: (cell_id, /** @type {boolean} */ allow_other_selected_cells) => allow_other_selected_cells ? this.state.selected_cells : [cell_id], @@ -1089,6 +1103,9 @@ patch: ${JSON.stringify( ]) } finally { this.pending_local_updates-- + // this property is used to tell our frontend tests that the updates are done + //@ts-ignore + document.body._update_is_ongoing = this.pending_local_updates > 0 } }) last_update_notebook_task = new_task.catch(console.error) @@ -1303,10 +1320,14 @@ patch: ${JSON.stringify( if (!in_textarea_or_input()) { const serialized = this.serialize_selected() if (serialized) { - navigator.clipboard.writeText(serialized).catch((err) => { - console.error("Error copying cells", e, err) - alert(`Error copying cells: ${err}`) - }) + e.preventDefault() + // wait one frame to get transient user activation + requestAnimationFrame(() => + navigator.clipboard.writeText(serialized).catch((err) => { + console.error("Error copying cells", e, err, navigator.userActivation) + alert(`Error copying cells: ${err?.message ?? err}`) + }) + ) } } }) @@ -1401,10 +1422,6 @@ patch: ${JSON.stringify( document.title = "🎈 " + new_state.notebook.shortpath + " — Pluto.jl" } - // this property is used to tell our frontend tests that the updates are done - //@ts-ignore - document.body._update_is_ongoing = this.pending_local_updates > 0 - this.send_queued_bond_changes() if (old_state.backend_launch_phase !== this.state.backend_launch_phase && this.state.backend_launch_phase != null) { diff --git a/frontend/components/ExportBanner.js b/frontend/components/ExportBanner.js index a013995680..d0a8810e1d 100644 --- a/frontend/components/ExportBanner.js +++ b/frontend/components/ExportBanner.js @@ -64,10 +64,11 @@ export const ExportBanner = ({ notebook_id, print_title, open, onClose, notebook useEventListener( window, "beforeprint", - () => { - console.log("beforeprint") - print_old_title_ref.current = document.title - document.title = print_title.replace(/\.jl$/, "").replace(/\.plutojl$/, "") + (e) => { + if (!e.detail?.fake) { + print_old_title_ref.current = document.title + document.title = print_title.replace(/\.jl$/, "").replace(/\.plutojl$/, "") + } }, [print_title] ) diff --git a/frontend/components/LiveDocsTab.js b/frontend/components/LiveDocsTab.js index b26c1e27bc..96e41dc75c 100644 --- a/frontend/components/LiveDocsTab.js +++ b/frontend/components/LiveDocsTab.js @@ -47,7 +47,7 @@ export let LiveDocsTab = ({ focus_on_open, desired_doc_query, on_update_doc_quer live_doc_search_ref.current.focus({ preventScroll: true }) live_doc_search_ref.current.select() } - }, [focus_on_open, live_doc_search_ref.current]) + }, [focus_on_open]) let fetch_docs = (new_query) => { update_state((state) => { diff --git a/frontend/components/Notebook.js b/frontend/components/Notebook.js index c7938be760..7ada590c0d 100644 --- a/frontend/components/Notebook.js +++ b/frontend/components/Notebook.js @@ -159,6 +159,7 @@ export const Notebook = ({ }, render_cell_outputs_delay(notebook.cell_order.length)) } }, [cell_outputs_delayed, notebook.cell_order.length]) + let global_definition_locations = useMemo( () => Object.fromEntries( diff --git a/frontend/components/ProcessTab.js b/frontend/components/ProcessTab.js index 8da46878e2..c398a6c04c 100644 --- a/frontend/components/ProcessTab.js +++ b/frontend/components/ProcessTab.js @@ -170,7 +170,10 @@ const StatusItem = ({ status_tree, path, my_clock_is_ahead_by, nbpkg, backend_la let busy = kids.reduce((acc, x) => acc + (is_busy(x) ? 1 : 0), 0) let total = kids.length - return html`<${DiscreteProgressBar} busy=${busy} done=${done} total=${total} />` + let failed_indices = kids.reduce((acc, x, i) => (x.success === false ? [...acc, i] : acc), []) + console.log({ kids }) + + return html`<${DiscreteProgressBar} busy=${busy} done=${done} total=${total} failed_indices=${failed_indices} />` } const inner = is_open @@ -198,6 +201,7 @@ const StatusItem = ({ status_tree, path, my_clock_is_ahead_by, nbpkg, backend_la data-depth=${path.length} class=${cl({ started, + failed: mystatus.success === false, finished, busy, is_open, diff --git a/frontend/dark_color.css b/frontend/dark_color.css index 229b17544e..701aa5ac19 100644 --- a/frontend/dark_color.css +++ b/frontend/dark_color.css @@ -159,6 +159,7 @@ --process-busy: #ffcd70; --process-finished: hsl(126deg 30% 60%); --process-undefined: rgb(151, 151, 151); + --process-failed: hsl(4, 30%, 60%); --process-notify-bg: hsl(0 0% 21%); /*footer*/ diff --git a/frontend/editor.css b/frontend/editor.css index 37408c1371..c2048c9d86 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -74,7 +74,6 @@ main { padding-bottom: 4rem; padding-left: 25px; padding-right: 6px; - align-content: center; width: 100%; } @@ -1373,7 +1372,6 @@ pluto-input .cm-editor { border-bottom-right-radius: 4px; border: 1px solid var(--normal-cell-color); border-left: none; - transition: border-color 0.15s ease-in-out; /* Make sure that scrolling an editor into view gives some breathing room */ scroll-margin-block: 20vh; @@ -1542,7 +1540,6 @@ body:not(.___) pluto-cell.code_folded > pluto-trafficlight { @media screen and (any-pointer: fine) { body:not(.disable_ui) pluto-cell:hover > pluto-trafficlight { background: var(--normal-cell-color); - transition: background 0.05s ease-in; } } @@ -1762,11 +1759,13 @@ pluto-input > .input_context_menu ul { pluto-input { position: relative; + display: block; } pluto-input > div.input_context_menu { left: 100%; top: -8px; position: absolute; + z-index: 1400; } @media screen and (min-width: 921px) { pluto-input > div.input_context_menu { @@ -1774,9 +1773,6 @@ pluto-input > div.input_context_menu { } } @media screen and (max-width: 920px) { - pluto-input > div.input_context_menu { - z-index: 1400; - } pluto-input > div.input_context_menu { right: 0px; left: unset; @@ -2646,6 +2642,9 @@ pl-status.busy { pl-status.finished { --status-color: var(--process-finished); } +pl-status.failed { + --status-color: var(--process-failed); +} pl-status.can_open { cursor: auto; @@ -2747,6 +2746,9 @@ pl-status .status-time { .discrete-progress-bar > div.busy { background: var(--process-busy); } +.discrete-progress-bar > div.failed { + background: var(--process-failed); +} .discrete-progress-bar.mid { gap: 1px; @@ -3026,7 +3028,6 @@ pluto-log-dot-positioner { --accent-color: var(--pluto-logs-info-accent-color); --icon-image: unset; background: var(--bg-color); - color: var(--accent-color); margin: 2px; border-radius: 6px; /* border: 2px solid var(--accent-color); */ @@ -3036,8 +3037,8 @@ pluto-log-dot-positioner { background-size: 200% 100%; } -pluto-log-dot-positioner:last-child { - /* border-bottom: none; */ +pluto-log-dot > pre { + color: var(--accent-color); } pluto-log-truncated { @@ -3490,6 +3491,22 @@ pluto-cell.errored .cm-editor .cm-lineNumbers .cm-gutterElement::after { color: var(--cm-string-color); } +.cm-completionIcon-completion_property::before { + color: var(--cm-property-color); +} + +.cm-completionIcon-completion_keyword::before { + color: var(--cm-keyword-color); +} + +li.completion_keyword_argument .cm-completionLabel { + font-style: italic; + font-weight: bold; +} +.cm-completionIcon-completion_keyword_argument::before { + color: var(--cm-number-color); +} + .cm-completionIcon-c_Any::before, pluto-output > assignee, pluto-popup code.auto_disabled_variable { @@ -3500,6 +3517,9 @@ pluto-popup code.auto_disabled_variable { .cm-completionIcon-c_Function::before { color: var(--cm-function-color); } +.cm-completionIcon-c_Macro::before { + color: var(--cm-macro-color); +} .cm-completionIcon-c_Array::before { color: var(--cm-bracket-color); diff --git a/frontend/editor.html b/frontend/editor.html index d1009de57f..3f68559b00 100644 --- a/frontend/editor.html +++ b/frontend/editor.html @@ -49,7 +49,9 @@
- + + +
diff --git a/frontend/editor.js b/frontend/editor.js index 42a73b6300..58058c1419 100644 --- a/frontend/editor.js +++ b/frontend/editor.js @@ -6,6 +6,7 @@ import { FetchProgress, read_Uint8Array_with_progress } from "./components/Fetch import { unpack } from "./common/MsgPack.js" import { RawHTMLContainer } from "./components/CellOutput.js" import { ProcessStatus } from "./common/ProcessStatus.js" +import { parse_launch_params } from "./common/parse_launch_params.js" const url_params = new URLSearchParams(window.location.search) @@ -25,40 +26,7 @@ export const set_disable_ui_css = (val) => { ///////////// // the rest: -/** - * - * @type {import("./components/Editor.js").LaunchParameters} - */ -const launch_params = { - //@ts-ignore - notebook_id: url_params.get("id") ?? window.pluto_notebook_id, - //@ts-ignore - statefile: url_params.get("statefile") ?? window.pluto_statefile, - //@ts-ignore - statefile_integrity: url_params.get("statefile_integrity") ?? window.pluto_statefile_integrity, - //@ts-ignore - notebookfile: url_params.get("notebookfile") ?? window.pluto_notebookfile, - //@ts-ignore - notebookfile_integrity: url_params.get("notebookfile_integrity") ?? window.pluto_notebookfile_integrity, - //@ts-ignore - disable_ui: !!(url_params.get("disable_ui") ?? window.pluto_disable_ui), - //@ts-ignore - preamble_html: url_params.get("preamble_html") ?? window.pluto_preamble_html, - //@ts-ignore - isolated_cell_ids: url_params.has("isolated_cell_id") ? url_params.getAll("isolated_cell_id") : window.pluto_isolated_cell_ids, - //@ts-ignore - binder_url: url_params.get("binder_url") ?? window.pluto_binder_url, - //@ts-ignore - pluto_server_url: url_params.get("pluto_server_url") ?? window.pluto_pluto_server_url, - //@ts-ignore - slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url, - //@ts-ignore - recording_url: url_params.get("recording_url") ?? window.pluto_recording_url, - //@ts-ignore - recording_url_integrity: url_params.get("recording_url_integrity") ?? window.pluto_recording_url_integrity, - //@ts-ignore - recording_audio_url: url_params.get("recording_audio_url") ?? window.pluto_recording_audio_url, -} +const launch_params = parse_launch_params() const truthy = (x) => x === "" || x === "true" const falsey = (x) => x === "false" @@ -217,6 +185,7 @@ class PlutoEditorComponent extends HTMLElement { const new_launch_params = Object.fromEntries(Object.entries(launch_params).map(([k, v]) => [k, from_attribute(this, k) ?? v])) console.log("Launch parameters: ", new_launch_params) + document.querySelector(".delete-me-when-live")?.remove() render(html`<${EditorLoader} launch_params=${new_launch_params} />`, this) } } diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index b2af50ea68..04f50f5260 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -400,7 +400,7 @@ declare class SelectionRange { /** Compare this range to another range. */ - eq(other: SelectionRange): boolean; + eq(other: SelectionRange, includeAssoc?: boolean): boolean; /** Return a JSON-serializable object representing the range. */ @@ -432,9 +432,12 @@ declare class EditorSelection { */ map(change: ChangeDesc, assoc?: number): EditorSelection; /** - Compare this selection to another selection. + Compare this selection to another selection. By default, ranges + are compared only by position. When `includeAssoc` is true, + cursor ranges must also have the same + [`assoc`](https://codemirror.net/6/docs/ref/#state.SelectionRange.assoc) value. */ - eq(other: EditorSelection): boolean; + eq(other: EditorSelection, includeAssoc?: boolean): boolean; /** Get the primary selection range. Usually, you should make sure your code applies to _all_ ranges, by using methods like @@ -574,7 +577,7 @@ declare class Facet implements FacetReader { */ static of(ranges: readonly Range[] | Range, sort?: boolean): RangeSet; /** + Join an array of range sets into a single set. + */ + static join(sets: readonly RangeSet[]): RangeSet; + /** The empty set of ranges. */ static empty: RangeSet; @@ -1854,6 +1861,18 @@ declare class NodeProp { */ static group: NodeProp; /** + Attached to nodes to indicate these should be + [displayed](https://codemirror.net/docs/ref/#language.syntaxTree) + in a bidirectional text isolate, so that direction-neutral + characters on their sides don't incorrectly get associated with + surrounding text. You'll generally want to set this for nodes + that contain arbitrary text, like strings and comments, and for + nodes that appear _inside_ arbitrary text, like HTML tags. When + not given a value, in a grammar declaration, defaults to + `"auto"`. + */ + static isolate: NodeProp<"rtl" | "ltr" | "auto">; + /** The hash of the [context](#lr.ContextTracker.constructor) that the node was parsed in, if any. Used to limit reuse of contextual nodes. @@ -2534,7 +2553,6 @@ declare class TreeCursor implements SyntaxNodeRef { private bufferNode; private yieldNode; private yieldBuf; - private yield; /** Move the cursor to this node's first child. When this returns false, the node has no child, and the cursor has not been moved. @@ -3057,9 +3075,12 @@ interface MarkDecorationSpec { /** When using sets of decorations in [`bidiIsolatedRanges`](https://codemirror.net/6/docs/ref/##view.EditorView^bidiIsolatedRanges), - this property provides the direction of the isolates. + this property provides the direction of the isolates. When null + or not given, it indicates the range has `dir=auto`, and its + direction should be derived from the first strong directional + character in it. */ - bidiIsolate?: Direction; + bidiIsolate?: Direction | null; /** Decoration specs allow extra properties, which can be retrieved through the decoration's [`spec`](https://codemirror.net/6/docs/ref/#view.Decoration.spec) @@ -3323,6 +3344,17 @@ apply to the editor, and if it can, perform it as a side effect transaction) and return `true`. */ type Command = (target: EditorView) => boolean; +declare class ScrollTarget { + readonly range: SelectionRange; + readonly y: ScrollStrategy; + readonly x: ScrollStrategy; + readonly yMargin: number; + readonly xMargin: number; + readonly isSnapshot: boolean; + constructor(range: SelectionRange, y?: ScrollStrategy, x?: ScrollStrategy, yMargin?: number, xMargin?: number, isSnapshot?: boolean); + map(changes: ChangeDesc): ScrollTarget; + clip(state: EditorState): ScrollTarget; +} /** This is the interface plugin objects conform to. */ @@ -3339,6 +3371,13 @@ interface PluginValue extends Object { */ update?(update: ViewUpdate): void; /** + Called when the document view is updated (due to content, + decoration, or viewport changes). Should not try to immediately + start another view update. Often useful for calling + [`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure). + */ + docViewUpdate?(view: EditorView): void; + /** Called when the plugin is no longer going to be used. Should revert any changes the plugin made to the DOM. */ @@ -3413,7 +3452,7 @@ interface MeasureRequest { write?(measure: T, view: EditorView): void; /** When multiple requests with the same key are scheduled, only the - last one will actually be ran. + last one will actually be run. */ key?: any; } @@ -3582,6 +3621,13 @@ interface EditorViewConfig extends EditorStateConfig { */ root?: Document | ShadowRoot; /** + Pass an effect created with + [`EditorView.scrollIntoView`](https://codemirror.net/6/docs/ref/#view.EditorView^scrollIntoView) or + [`EditorView.scrollSnapshot`](https://codemirror.net/6/docs/ref/#view.EditorView.scrollSnapshot) + here to set an initial scroll position. + */ + scrollTo?: StateEffect; + /** Override the way transactions are [dispatched](https://codemirror.net/6/docs/ref/#view.EditorView.dispatch) for this editor view. Your implementation, if provided, should probably call the @@ -3719,6 +3765,7 @@ declare class EditorView { */ setState(newState: EditorState): void; private updatePlugins; + private docViewUpdate; /** Get the CSS classes for the currently active editor themes. */ @@ -3823,6 +3870,13 @@ declare class EditorView { */ moveByGroup(start: SelectionRange, forward: boolean): SelectionRange; /** + Get the cursor position visually at the start or end of a line. + Note that this may differ from the _logical_ position at its + start or end (which is simply at `line.from`/`line.to`) if text + at the start or end goes against the line's base text direction. + */ + visualLineSide(line: Line$1, end: boolean): SelectionRange; + /** Move to the next line boundary in the given direction. If `includeWrap` is true, line wrapping is on, and there is a further wrap point on the current line, the wrap point will be @@ -3981,15 +4035,30 @@ declare class EditorView { /** Extra vertical distance to add when moving something into view. Not used with the `"center"` strategy. Defaults to 5. + Must be less than the height of the editor. */ yMargin?: number; /** Extra horizontal distance to add. Not used with the `"center"` - strategy. Defaults to 5. + strategy. Defaults to 5. Must be less than the width of the + editor. */ xMargin?: number; }): StateEffect; /** + Return an effect that resets the editor to its current (at the + time this method was called) scroll position. Note that this + only affects the editor's own scrollable element, not parents. + See also + [`EditorViewConfig.scrollTo`](https://codemirror.net/6/docs/ref/#view.EditorViewConfig.scrollTo). + + The effect should be used with a document identical to the one + it was created for. Failing to do so is not an error, but may + not scroll to the expected position. You can + [map](https://codemirror.net/6/docs/ref/#state.StateEffect.map) the effect to account for changes. + */ + scrollSnapshot(): StateEffect; + /** Facet to add a [style module](https://github.com/marijnh/style-mod#documentation) to an editor view. The view will ensure that the module is @@ -4032,6 +4101,23 @@ declare class EditorView { */ static inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean, readonly ((view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean)[]>; /** + Scroll handlers can override how things are scrolled into view. + If they return `true`, no further handling happens for the + scrolling. If they return false, the default scroll behavior is + applied. Scroll handlers should never initiate editor updates. + */ + static scrollHandler: Facet<(view: EditorView, range: SelectionRange, options: { + x: ScrollStrategy; + y: ScrollStrategy; + xMargin: number; + yMargin: number; + }) => boolean, readonly ((view: EditorView, range: SelectionRange, options: { + x: ScrollStrategy; + y: ScrollStrategy; + xMargin: number; + yMargin: number; + }) => boolean)[]>; + /** This facet can be used to provide functions that create effects to be dispatched when the editor's focus state changes. */ @@ -4104,6 +4190,16 @@ declare class EditorView { */ static decorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; /** + Facet that works much like + [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its + inputs at the very bottom of the precedence stack, meaning mark + decorations provided here will only be split by other, partially + overlapping \`outerDecorations\` ranges, and wrap around all + regular decorations. Use this for mark elements that should, as + much as possible, remain in one piece. + */ + static outerDecorations: Facet DecorationSet), readonly (DecorationSet | ((view: EditorView) => DecorationSet))[]>; + /** Used to provide ranges that should be treated as atoms as far as cursor motion is concerned. This causes methods like [`moveByChar`](https://codemirror.net/6/docs/ref/#view.EditorView.moveByChar) and @@ -5634,6 +5730,11 @@ interface Completion { */ type?: string; /** + When this option is selected, and one of these characters is + typed, insert the completion before typing the character. + */ + commitCharacters?: readonly string[]; + /** When given, should be a number from -99 to 99 that adjusts how this completion is ranked compared to other completions that match the input as well as this one. A negative number moves it @@ -5835,6 +5936,20 @@ interface CompletionResult { completion still applies in the new state. */ update?: (current: CompletionResult, from: number, to: number, context: CompletionContext) => CompletionResult | null; + /** + When results contain position-dependent information in, for + example, `apply` methods, you can provide this method to update + the result for transactions that happen after the query. It is + not necessary to update `from` and `to`—those are tracked + automatically. + */ + map?: (current: CompletionResult, changes: ChangeDesc) => CompletionResult | null; + /** + Set a default set of [commit + characters](https://codemirror.net/6/docs/ref/#autocomplete.Completion.commitCharacters) for all + options in this result. + */ + commitCharacters?: readonly string[]; } /** This annotation is added to transactions that are produced by @@ -5855,6 +5970,14 @@ interface CompletionConfig { */ activateOnTyping?: boolean; /** + The amount of time to wait for further typing before querying + completion sources via + [`activateOnTyping`](https://codemirror.net/6/docs/ref/#autocomplete.autocompletion^config.activateOnTyping). + Defaults to 100, which should be fine unless your completion + source is very slow and/or doesn't use `validFor`. + */ + activateOnTypingDelay?: number; + /** By default, when completion opens, the first option is selected and can be confirmed with [`acceptCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.acceptCompletion). When this @@ -5920,7 +6043,7 @@ interface CompletionConfig { 80. */ addToOptions?: { - render: (completion: Completion, state: EditorState) => Node | null; + render: (completion: Completion, state: EditorState, view: EditorView) => Node | null; position: number; }[]; /** @@ -5943,6 +6066,13 @@ interface CompletionConfig { */ compareCompletions?: (a: Completion, b: Completion) => number; /** + When set to true (the default is false), turn off fuzzy matching + of completions and only show those that start with the text the + user typed. Only takes effect for results where + [`filter`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.filter) isn't false. + */ + filterStrict?: boolean; + /** By default, commands relating to an open completion only take effect 75 milliseconds after the completion opened, so that key presses made before the user is aware of the tooltip don't go to @@ -6268,7 +6398,7 @@ Default search-related key bindings. - Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel) - F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext) - Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious) - - Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine) + - Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine) - Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence) */ declare const searchKeymap: readonly KeyBinding[]; @@ -6580,7 +6710,7 @@ interface Action { */ apply: (view: EditorView, from: number, to: number) => void; } -type DiagnosticFilter = (diagnostics: readonly Diagnostic[]) => Diagnostic[]; +type DiagnosticFilter = (diagnostics: readonly Diagnostic[], state: EditorState) => Diagnostic[]; interface LintConfig { /** Time to wait (in milliseconds) after a change before running @@ -6617,9 +6747,10 @@ type LintSource = (view: EditorView) => readonly Diagnostic[] | Promise - - + diff --git a/frontend/light_color.css b/frontend/light_color.css index 4f96c6d541..f575aa03d3 100644 --- a/frontend/light_color.css +++ b/frontend/light_color.css @@ -80,7 +80,6 @@ --pluto-logs-progress-fill: #ffffff; --pluto-logs-progress-bg: #e7e7e7; --pluto-logs-progress-border: hsl(210deg 16% 74%); - --pluto-logs-info-color: white; --pluto-logs-info-accent-color: inherit; --pluto-logs-warn-color: rgb(236 234 213); @@ -157,11 +156,11 @@ --helpbox-text-color: black; --code-section-bg-color: whitesmoke; --code-section-bg-color: #f3f3f3; - --process-item-bg: #f2f2f2; --process-busy: #ffcd70; --process-finished: hsl(126deg 30% 60%); --process-undefined: rgb(151, 151, 151); + --process-failed: hsl(0deg 72.62% 64.6%); --process-notify-bg: hsl(44.86deg 50% 94%); /*footer*/ diff --git a/frontend/treeview.css b/frontend/treeview.css index 2c7663c79b..a668c4f9de 100644 --- a/frontend/treeview.css +++ b/frontend/treeview.css @@ -341,6 +341,7 @@ jlerror li .frame-line-preview pre:not(.asdfdsaf) { border-radius: var(--br); overflow: hidden; position: relative; + display: block; } jlerror li:not(.from_this_cell) .frame-line-preview pre::after { @@ -353,6 +354,9 @@ jlerror li:not(.from_this_cell) .frame-line-preview pre::after { opacity: 0.6; } +jlerror li .frame-line-preview pre > code { + padding: 0; +} jlerror li .frame-line-preview pre > code:not(:only-child).frame-line { background: var(--cm-highlighted); } diff --git a/src/analysis/DependencyCache.jl b/src/analysis/DependencyCache.jl index 3673ee2cc7..0f7601e227 100644 --- a/src/analysis/DependencyCache.jl +++ b/src/analysis/DependencyCache.jl @@ -6,7 +6,7 @@ Note that only direct dependents are given here, not indirect dependents. """ function downstream_cells_map(cell::Cell, topology::NotebookTopology)::Dict{Symbol,Vector{Cell}} defined_symbols = let node = topology.nodes[cell] - node.definitions ∪ node.funcdefs_without_signatures + node.definitions ∪ Iterators.filter(!_is_anon_function_name, node.funcdefs_without_signatures) end return Dict{Symbol,Vector{Cell}}( sym => PlutoDependencyExplorer.where_referenced(topology, Set([sym])) @@ -15,6 +15,8 @@ function downstream_cells_map(cell::Cell, topology::NotebookTopology)::Dict{Symb end @deprecate downstream_cells_map(cell::Cell, notebook::Notebook) downstream_cells_map(cell, notebook.topology) +_is_anon_function_name(s::Symbol) = startswith(String(s), "__ExprExpl_anon__") + """ Gets a dictionary of all symbols and the respective cells on which the given cell depends. diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index 7a8e4c4bd2..f5237014e2 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -141,8 +141,9 @@ function run_reactive_core!( new_errable = keys(new_order.errable) to_delete_vars = union!(to_delete_vars, defined_variables(new_topology, new_errable)...) to_delete_funcs = union!(to_delete_funcs, defined_functions(new_topology, new_errable)...) - + cells_to_macro_invalidate = Set{UUID}(c.cell_id for c in cells_with_deleted_macros(old_topology, new_topology)) + cells_to_js_link_invalidate = Set{UUID}(c.cell_id for c in union!(Set{Cell}(), to_run, new_errable, indirectly_deactivated)) module_imports_to_move = reduce(all_cells(new_topology); init=Set{Expr}()) do module_imports_to_move, c c ∈ to_run && return module_imports_to_move @@ -156,7 +157,7 @@ function run_reactive_core!( if will_run_code(notebook) to_delete_funcs_simple = Set{Tuple{UUID,Tuple{Vararg{Symbol}}}}((id, name.parts) for (id,name) in to_delete_funcs) - deletion_hook((session, notebook), old_workspace_name, nothing, to_delete_vars, to_delete_funcs_simple, module_imports_to_move, cells_to_macro_invalidate; to_run) # `deletion_hook` defaults to `WorkspaceManager.move_vars` + deletion_hook((session, notebook), old_workspace_name, nothing, to_delete_vars, to_delete_funcs_simple, module_imports_to_move, cells_to_macro_invalidate, cells_to_js_link_invalidate; to_run) # `deletion_hook` defaults to `WorkspaceManager.move_vars` end foreach(v -> delete!(notebook.bonds, v), to_delete_vars) @@ -172,7 +173,7 @@ function run_reactive_core!( cell.logs = Vector{Dict{String,Any}}() send_notebook_changes_throttled() - if any_interrupted || notebook.wants_to_interrupt || !will_run_code(notebook) + if (skip = any_interrupted || notebook.wants_to_interrupt || !will_run_code(notebook)) relay_reactivity_error!(cell, InterruptException()) else run = run_single!( @@ -192,7 +193,7 @@ function run_reactive_core!( end cell.running = false - Status.report_business_finished!(cell_status, Symbol(i)) + Status.report_business_finished!(cell_status, Symbol(i), !skip && !run.errored) defined_macros_in_cell = defined_macros(new_topology, cell) |> Set{Symbol} diff --git a/src/evaluation/RunBonds.jl b/src/evaluation/RunBonds.jl index 2ccc82c190..7ec4e191b6 100644 --- a/src/evaluation/RunBonds.jl +++ b/src/evaluation/RunBonds.jl @@ -41,7 +41,7 @@ function set_bond_values_reactive(; bond_value_pairs = zip(syms_to_set, new_values) syms_to_set_set = Set{Symbol}(syms_to_set) - function custom_deletion_hook((session, notebook)::Tuple{ServerSession,Notebook}, old_workspace_name, new_workspace_name, to_delete_vars::Set{Symbol}, methods_to_delete, module_imports_to_move, cells_to_macro_invalidate; to_run) + function custom_deletion_hook((session, notebook)::Tuple{ServerSession,Notebook}, old_workspace_name, new_workspace_name, to_delete_vars::Set{Symbol}, methods_to_delete, module_imports_to_move, cells_to_macro_invalidate, cells_to_js_link_invalidate; to_run) to_delete_vars = union(to_delete_vars, syms_to_set_set) # also delete the bound symbols WorkspaceManager.move_vars( (session, notebook), @@ -51,6 +51,7 @@ function set_bond_values_reactive(; methods_to_delete, module_imports_to_move, cells_to_macro_invalidate, + cells_to_js_link_invalidate, syms_to_set_set, ) set_bond_value_pairs!(session, notebook, zip(syms_to_set, new_values)) diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index f77bbf0c83..0fde721707 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -65,76 +65,82 @@ function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false end @debug "Creating workspace process" notebook.path length(notebook.cells) - worker = create_workspaceprocess(WorkerType; compiler_options=_merge_notebook_compiler_options(notebook, session.options.compiler)) - - Status.report_business_finished!(workspace_business, :create_process) - init_status = Status.report_business_started!(workspace_business, :init_process) - Status.report_business_started!(init_status, Symbol(1)) - Status.report_business_planned!(init_status, Symbol(2)) - Status.report_business_planned!(init_status, Symbol(3)) - Status.report_business_planned!(init_status, Symbol(4)) - - let s = session.options.evaluation.workspace_custom_startup_expr - s === nothing || Malt.remote_eval_wait(worker, Meta.parseall(s)) - end + try + worker = create_workspaceprocess(WorkerType; compiler_options=_merge_notebook_compiler_options(notebook, session.options.compiler), status=create_status) + + Status.report_business_finished!(workspace_business, :create_process) + init_status = Status.report_business_started!(workspace_business, :init_process) + Status.report_business_started!(init_status, Symbol(1)) + Status.report_business_planned!(init_status, Symbol(2)) + Status.report_business_planned!(init_status, Symbol(3)) + Status.report_business_planned!(init_status, Symbol(4)) + + let s = session.options.evaluation.workspace_custom_startup_expr + s === nothing || Malt.remote_eval_wait(worker, Meta.parseall(s)) + end - Malt.remote_eval_wait(worker, quote - PlutoRunner.notebook_id[] = $(notebook.notebook_id) - end) + Malt.remote_eval_wait(worker, quote + PlutoRunner.notebook_id[] = $(notebook.notebook_id) + end) - remote_log_channel = Malt.worker_channel(worker, quote - channel = Channel{Any}(10) - Main.PlutoRunner.setup_plutologger( - $(notebook.notebook_id), + remote_log_channel = Malt.worker_channel(worker, quote + channel = Channel{Any}(10) + Main.PlutoRunner.setup_plutologger( + $(notebook.notebook_id), + channel + ) channel - ) - channel - end) + end) - run_channel = Malt.worker_channel(worker, :(Main.PlutoRunner.run_channel)) + run_channel = Malt.worker_channel(worker, :(Main.PlutoRunner.run_channel)) - module_name = create_emptyworkspacemodule(worker) + module_name = create_emptyworkspacemodule(worker) - original_LOAD_PATH, original_ACTIVE_PROJECT = Malt.remote_eval_fetch(worker, :(Base.LOAD_PATH, Base.ACTIVE_PROJECT[])) + original_LOAD_PATH, original_ACTIVE_PROJECT = Malt.remote_eval_fetch(worker, :(Base.LOAD_PATH, Base.ACTIVE_PROJECT[])) - workspace = Workspace(; - worker, - notebook_id=notebook.notebook_id, - remote_log_channel, - module_name, - original_LOAD_PATH, - original_ACTIVE_PROJECT, - is_offline_renderer, - ) - - - Status.report_business_finished!(init_status, Symbol(1)) - Status.report_business_started!(init_status, Symbol(2)) + workspace = Workspace(; + worker, + notebook_id=notebook.notebook_id, + remote_log_channel, + module_name, + original_LOAD_PATH, + original_ACTIVE_PROJECT, + is_offline_renderer, + ) + + + Status.report_business_finished!(init_status, Symbol(1)) + Status.report_business_started!(init_status, Symbol(2)) - @async start_relaying_logs((session, notebook), remote_log_channel) - @async start_relaying_self_updates((session, notebook), run_channel) - cd_workspace(workspace, notebook.path) - - Status.report_business_finished!(init_status, Symbol(2)) - Status.report_business_started!(init_status, Symbol(3)) - - use_nbpkg_environment((session, notebook), workspace) - - Status.report_business_finished!(init_status, Symbol(3)) - Status.report_business_started!(init_status, Symbol(4)) - - # TODO: precompile 1+1 with display - # sleep(3) - eval_format_fetch_in_workspace(workspace, Expr(:toplevel, LineNumberNode(-1), :(1+1)), uuid1(); code_is_effectful=false) - - Status.report_business_finished!(init_status, Symbol(4)) - Status.report_business_finished!(workspace_business, :init_process) - Status.report_business_finished!(workspace_business) + @async start_relaying_logs((session, notebook), remote_log_channel) + @async start_relaying_self_updates((session, notebook), run_channel) + cd_workspace(workspace, notebook.path) + + Status.report_business_finished!(init_status, Symbol(2)) + Status.report_business_started!(init_status, Symbol(3)) + + use_nbpkg_environment((session, notebook), workspace) + + Status.report_business_finished!(init_status, Symbol(3)) + Status.report_business_started!(init_status, Symbol(4)) + + # TODO: precompile 1+1 with display + # sleep(3) + eval_format_fetch_in_workspace(workspace, Expr(:toplevel, LineNumberNode(-1), :(1+1)), uuid1(); code_is_effectful=false) + + Status.report_business_finished!(init_status, Symbol(4)) + Status.report_business_finished!(workspace_business, :init_process) + Status.report_business_finished!(workspace_business) - is_offline_renderer || if notebook.process_status == ProcessStatus.starting - notebook.process_status = ProcessStatus.ready + is_offline_renderer || if notebook.process_status == ProcessStatus.starting + notebook.process_status = ProcessStatus.ready + end + return workspace + catch e + Status.report_business_finished!(workspace_business, false) + notebook.process_status = ProcessStatus.no_process + rethrow(e) end - return workspace end function use_nbpkg_environment((session, notebook)::SN, workspace=nothing) @@ -288,13 +294,13 @@ function create_workspaceprocess(WorkerType; compiler_options=CompilerOptions(), end else - Status.report_business_started!(status, Symbol(1)) - Status.report_business_planned!(status, Symbol(2)) + Status.report_business_started!(status, Symbol("Starting process")) + Status.report_business_planned!(status, Symbol("Loading notebook boot environment")) worker = WorkerType(; exeflags=_convert_to_flags(compiler_options)) - Status.report_business_finished!(status, Symbol(1)) - Status.report_business_started!(status, Symbol(2)) + Status.report_business_finished!(status, Symbol("Starting process")) + Status.report_business_started!(status, Symbol("Loading notebook boot environment")) Malt.remote_eval_wait(worker, process_preamble()) @@ -555,6 +561,7 @@ function move_vars( 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}=Set{Symbol}(); kwargs... ) @@ -570,6 +577,7 @@ function move_vars( $methods_to_delete, $module_imports_to_move, $cells_to_macro_invalidate, + $cells_to_js_link_invalidate, $keep_registered, ) end) @@ -580,16 +588,18 @@ function move_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_macro_invalidate::Set{UUID}, + cells_to_js_link_invalidate::Set{UUID}; kwargs... ) move_vars( session_notebook, bump_workspace_module(session_notebook)..., - to_delete, + to_delete, methods_to_delete, module_imports_to_move, - cells_to_macro_invalidate; + cells_to_macro_invalidate, + cells_to_js_link_invalidate; kwargs... ) end diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index 0a34c59857..858081d9c3 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -44,8 +44,8 @@ Base.@kwdef mutable struct Notebook # per notebook compiler options # nothing means to use global session compiler options compiler_options::Union{Nothing,Configuration.CompilerOptions}=nothing - # nbpkg_ctx::Union{Nothing,PkgContext}=nothing - nbpkg_ctx::Union{Nothing,PkgContext}=PkgCompat.create_empty_ctx() + nbpkg_ctx::Union{Nothing,PkgContext}=nothing + # nbpkg_ctx::Union{Nothing,PkgContext}=PkgCompat.create_empty_ctx() nbpkg_ctx_instantiated::Bool=false nbpkg_restart_recommended_msg::Union{Nothing,String}=nothing nbpkg_restart_required_msg::Union{Nothing,String}=nothing diff --git a/src/packages/Packages.jl b/src/packages/Packages.jl index 2e74e7cbaa..fb486f6a11 100644 --- a/src/packages/Packages.jl +++ b/src/packages/Packages.jl @@ -375,6 +375,7 @@ function sync_nbpkg(session, notebook, old_topology::NotebookTopology, new_topol end catch e bt = catch_backtrace() + Status.report_business_finished!(notebook.status_tree, :pkg, false) old_packages = try String.(keys(PkgCompat.project(notebook.nbpkg_ctx).dependencies)); catch; ["unknown"] end new_packages = try String.(external_package_names(new_topology)); catch; ["unknown"] end @warn """ diff --git a/src/packages/PkgCompat.jl b/src/packages/PkgCompat.jl index f0e857fc00..d3b4675c62 100644 --- a/src/packages/PkgCompat.jl +++ b/src/packages/PkgCompat.jl @@ -2,11 +2,21 @@ module PkgCompat export package_versions, package_completions +import REPL import Pkg import Pkg.Types: VersionRange import RegistryInstances import ..Pluto + + + +@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...)...) @@ -171,7 +181,7 @@ _get_registries() = RegistryInstances.reachable_registries() # (✅ "Public" API using RegistryInstances) "The cached output value of `_get_registries`." -const _parsed_registries = Ref(_get_registries()) +const _parsed_registries = Ref(RegistryInstances.RegistryInstance[]) # (✅ "Public" API using RegistryInstances) "Re-parse the installed registries from disk." @@ -179,6 +189,7 @@ 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 @@ -264,7 +275,13 @@ end # ⚠️ Internal API with fallback is_stdlib(package_name::AbstractString) = package_name ∈ _stdlibs() -global_ctx = PkgContext() + + +# Initial fill of registry cache +function __init__() + refresh_registry_cache() + global global_ctx=PkgContext() +end ### # Package names @@ -282,10 +299,10 @@ end function _registered_package_completions(partial_name::AbstractString)::Vector{String} # compat try - @static if hasmethod(Pkg.REPLMode.complete_remote_package, (String,)) - Pkg.REPLMode.complete_remote_package(partial_name) + @static if hasmethod(REPLMode.complete_remote_package, (String,)) + REPLMode.complete_remote_package(partial_name) else - Pkg.REPLMode.complete_remote_package(partial_name, 1, length(partial_name))[1] + 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()) diff --git a/src/precompile.jl b/src/precompile.jl index d7bbb8bd6b..d9373a20cd 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -36,13 +36,13 @@ PrecompileTools.@compile_workload begin 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 + # 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) diff --git a/src/runner/Loader.jl b/src/runner/Loader.jl index 73a65e68f3..d1f1094ed4 100644 --- a/src/runner/Loader.jl +++ b/src/runner/Loader.jl @@ -31,7 +31,7 @@ begin try Pkg.develop([Pkg.PackageSpec(; path)]; io=devnull) catch - # if it failed, do it again without suppressing io + @warn "Something went wrong while initializing the notebook boot environment... Trying again and showing you the output." Pkg.develop([Pkg.PackageSpec(; path)]) end @@ -39,7 +39,7 @@ begin try Pkg.resolve(; io=devnull) # supress IO catch - # if it failed, do it again without suppressing io + @warn "Something went wrong while initializing the notebook boot environment... Trying again and showing you the output." try Pkg.resolve() catch e diff --git a/src/runner/PlutoRunner/Project.toml b/src/runner/PlutoRunner/Project.toml index 58ac9e3d71..ec024fe8ed 100644 --- a/src/runner/PlutoRunner/Project.toml +++ b/src/runner/PlutoRunner/Project.toml @@ -1,7 +1,7 @@ name = "PlutoRunner" uuid = "dc6b355a-2368-4481-ae6d-ae0351418d79" authors = ["Michiel Dral ", "Fons van der Plas ", "Paul Berg "] -version = "29.12.98" +version = "29.14.98" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -18,5 +18,6 @@ Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] -FuzzyCompletions = "0.3,0.4,0.5" -PrecompileTools = "1" +# these compat entries should match those for the same packages in Project.toml of Pluto.jl itself. +FuzzyCompletions = "=0.5.4" +PrecompileTools = "=1.2.1" diff --git a/src/runner/PlutoRunner/src/PlutoRunner.jl b/src/runner/PlutoRunner/src/PlutoRunner.jl index c8e726f3d5..0435a4ec5c 100644 --- a/src/runner/PlutoRunner/src/PlutoRunner.jl +++ b/src/runner/PlutoRunner/src/PlutoRunner.jl @@ -26,7 +26,7 @@ import InteractiveUtils using Markdown import Markdown: html, htmlinline, LaTeX, withtag, htmlesc import Base64 -import FuzzyCompletions: Completion, BslashCompletion, ModuleCompletion, PropertyCompletion, FieldCompletion, PathCompletion, DictCompletion, completions, completion_text, score +import FuzzyCompletions: FuzzyCompletions, Completion, BslashCompletion, ModuleCompletion, PropertyCompletion, FieldCompletion, PathCompletion, DictCompletion, completion_text, score import Base: show, istextmime import UUIDs: UUID, uuid4 import Dates: DateTime @@ -283,14 +283,9 @@ function try_macroexpand(mod::Module, notebook_id::UUID, cell_id::UUID, expr; ca Expr(:block, expr) end - 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 + capture_logger = CaptureLogger(nothing, get_cell_logger(notebook_id, cell_id), Dict[]) - capture_logger = CaptureLogger(nothing, logger, Dict[]) - - expanded_expr, elapsed_ns = with_logger_and_io_to_logs(capture_logger; capture_stdout, stdio_loglevel=stdout_log_level) do + 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 @@ -531,10 +526,7 @@ function run_expression( old_currently_running_cell_id = currently_running_cell_id[] currently_running_cell_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 = get_cell_logger(notebook_id, cell_id) # reset published objects cell_published_objects[cell_id] = Dict{String,Any}() @@ -542,6 +534,9 @@ function run_expression( # 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... @@ -593,7 +588,7 @@ function run_expression( throw("Expression still contains macro calls!!") end - result, runtime = with_logger_and_io_to_logs(logger; capture_stdout, stdio_loglevel=stdout_log_level) do # about 200ns + 3ms overhead + 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) @@ -691,6 +686,7 @@ function move_vars( 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) @@ -701,7 +697,8 @@ function move_vars( 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)))) @@ -803,35 +800,40 @@ function delete_toplevel_methods(f::Function, cell_id::UUID)::Bool 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) && getfield(method, deleted_world) == alive_world_val + 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 `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) + + 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 - 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 + # 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 - ccall(:jl_method_table_insert, Cvoid, (Any, Any, Ptr{Cvoid}), methods_table, method, C_NULL) # i dont like doing this either! end end return !isempty(methods(f).ms) @@ -859,10 +861,7 @@ function try_delete_toplevel_methods(workspace::Module, (cell_id, name_parts)::T end end -# these deal with some inconsistencies in Julia's internal (undocumented!) variable names -const primary_world = filter(in(fieldnames(Method)), [:primary_world, :min_world]) |> first # Julia v1.3 and v1.0 resp. -const deleted_world = filter(in(fieldnames(Method)), [:deleted_world, :max_world]) |> first # Julia v1.3 and v1.0 resp. -const alive_world_val = getfield(methods(Base.sqrt).ms[1], deleted_world) # typemax(UInt) in Julia v1.3, Int(-1) in Julia 1.0 +const alive_world_val = methods(Base.sqrt).ms[1].deleted_world # typemax(UInt) in Julia v1.3, Int(-1) in Julia 1.0 @@ -929,8 +928,7 @@ function formatted_result_of( errored = ans isa CapturedException output_formatted = if (!ends_with_semicolon || errored) - logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id) - with_logger_and_io_to_logs(logger; capture_stdout, stdio_loglevel=stdout_log_level) do + 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, @@ -1002,6 +1000,7 @@ const default_iocontext = IOContext(devnull, :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, @@ -1350,7 +1349,7 @@ end function show_richest_withreturned(context::IOContext, @nospecialize(args)) buffer = IOBuffer(; sizehint=0) val = show_richest(IOContext(buffer, context), args) - return (resize!(buffer.data, buffer.size), val) + return (take!(buffer), val) end "Super important thing don't change." @@ -1757,6 +1756,9 @@ const integrations = Integration[ 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, @@ -1926,22 +1928,22 @@ function basic_completion_priority((s, description, exported, from_notebook)) end end -completed_object_description(x::Function) = "Function" -completed_object_description(x::Number) = "Number" -completed_object_description(x::AbstractString) = "String" -completed_object_description(x::Module) = "Module" -completed_object_description(x::AbstractArray) = "Array" -completed_object_description(x::Any) = "Any" +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_description(c::ModuleCompletion) = try - completed_object_description(getfield(c.parent, Symbol(c.mod))) +completion_value_type(c::ModuleCompletion) = try + completion_value_type_inner(getfield(c.parent, Symbol(c.mod)))::Symbol catch - nothing + :unknown end -completion_description(::Completion) = nothing +completion_value_type(::Completion) = :unknown -completion_detail(::Completion) = nothing -completion_detail(completion::BslashCompletion) = +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) ? @@ -1983,12 +1985,15 @@ function is_pluto_controlled(m::Module) end function completions_exported(cs::Vector{<:Completion}) - completed_modules = (c.parent for c in cs if c isa ModuleCompletion) - completed_modules_exports = Dict(m => string.(names(m, all=is_pluto_workspace(m), imported=true)) for m in completed_modules) + 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 - c.mod ∈ completed_modules_exports[c.parent] + Symbol(c.mod) ∈ completed_modules_exports[c.parent] else true end @@ -2002,38 +2007,56 @@ completion_from_notebook(c::ModuleCompletion) = !startswith(c.mod, "#") completion_from_notebook(c::Completion) = false -only_special_completion_types(::PathCompletion) = :path -only_special_completion_types(::DictCompletion) = :dict -only_special_completion_types(::Completion) = nothing +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 = completions(query, pos, workspace) - if endswith(query, '.') + 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(query, '/') + elseif endswith(partial, '/') filter!(is_path_completion, results) sort!(results; by=(r -> completion_text(r))) - elseif endswith(query, '[') + elseif endswith(partial, '[') filter!(is_dict_completion, results) sort!(results; by=(r -> completion_text(r))) else isenough(x) = x ≥ 0 - filter!(isenough ∘ score, results) # too many candiates otherwise + 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 = [ - (completion_text(result), - completion_description(result), - rexported, - completion_from_notebook(result), - only_special_completion_types(result), - completion_detail(result)) - for (result, rexported) in zip(results, exported) - ] + 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) @@ -2050,11 +2073,14 @@ end is_dot_completion(::Union{ModuleCompletion,PropertyCompletion,FieldCompletion}) = true is_dot_completion(::Completion) = false -is_path_completion(::Union{PathCompletion}) = true -is_path_completion(::Completion) = false +is_path_completion(::PathCompletion) = true +is_path_completion(::Completion) = false -is_dict_completion(::Union{DictCompletion}) = true -is_dict_completion(::Completion) = false +is_dict_completion(::DictCompletion) = true +is_dict_completion(::Completion) = false + +is_kwarg_completion(::FuzzyCompletions.KeywordArgumentCompletion) = true +is_kwarg_completion(::Completion) = false """ @@ -2181,7 +2207,7 @@ function improve_docs!(doc_md::Markdown.MD, query::Symbol, binding::Docs.Binding perm = sortperm(suggestions_scores; rev=true) permute!(suggestions, perm) - links = map(s -> Suggestion(s, symbol), @view(suggestions[begin:min(end,DOC_SUGGESTION_LIMIT)])) + links = map(s -> Suggestion(string(s), symbol), Iterators.take(suggestions, DOC_SUGGESTION_LIMIT)) if length(links) > 0 push!(doc_md.content, @@ -2540,6 +2566,73 @@ 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 ### @@ -2581,6 +2674,14 @@ 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 @@ -2743,7 +2844,7 @@ function with_io_to_logs(f::Function; enabled::Bool=true, loglevel::Logging.LogL result end -function with_logger_and_io_to_logs(f, logger; capture_stdout=true, stdio_loglevel=Logging.LogLevel(1)) +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 diff --git a/src/runner/PlutoRunner/src/precompile.jl b/src/runner/PlutoRunner/src/precompile.jl index 16bd57b9b1..69865f924b 100644 --- a/src/runner/PlutoRunner/src/precompile.jl +++ b/src/runner/PlutoRunner/src/precompile.jl @@ -17,4 +17,7 @@ PrecompileTools.@compile_workload begin 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 diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 3708bc9e0c..4bb2c72e25 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -526,6 +526,29 @@ responses[:reshow_cell] = function response_reshow_cell(🙋::ClientRequest) 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"]) diff --git a/src/webserver/REPLTools.jl b/src/webserver/REPLTools.jl index 7da202f6ca..19be0a3c12 100644 --- a/src/webserver/REPLTools.jl +++ b/src/webserver/REPLTools.jl @@ -109,6 +109,16 @@ responses[:complete] = function response_complete(🙋::ClientRequest) 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"] diff --git a/src/webserver/Status.jl b/src/webserver/Status.jl index 8586659d90..c84dd74c18 100644 --- a/src/webserver/Status.jl +++ b/src/webserver/Status.jl @@ -4,6 +4,7 @@ _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}() @@ -15,6 +16,7 @@ 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}( @@ -26,6 +28,7 @@ tojs(b::Business) = Dict{String,Any}( function report_business_started!(business::Business) lock(business.lock) do + business.success = nothing business.started_at = time() business.finished_at = nothing @@ -38,7 +41,10 @@ end -function report_business_finished!(business::Business) +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()) @@ -48,7 +54,7 @@ function report_business_finished!(business::Business) # also finish all subtasks (this can't be inside the same lock) for v in values(business.subtasks) - report_business_finished!(v) + report_business_finished!(v, success) end business.update_listener_ref[]() @@ -66,7 +72,7 @@ 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) = get_child(parent, name) |> report_business_finished! +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) diff --git a/test/Bonds.jl b/test/Bonds.jl index bf1b028e09..1789ddfa3a 100644 --- a/test/Bonds.jl +++ b/test/Bonds.jl @@ -352,7 +352,7 @@ import Malt @test noerror(notebook.cells[36]) @test notebook.cells[37].output.body == "\"xx\"" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false @@ -488,7 +488,7 @@ import Malt - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end end diff --git a/test/Configuration.jl b/test/Configuration.jl index 452a3a2839..5738731ed7 100644 --- a/test/Configuration.jl +++ b/test/Configuration.jl @@ -59,7 +59,7 @@ end @testset "Authentication" begin basic_nb_path = Pluto.project_relative_path("sample", "Basic.jl") - port = 1238 + port = 23832 options = Pluto.Configuration.from_flat_kwargs(; port, launch_browser=false, workspace_use_distributed=false) 🍭 = Pluto.ServerSession(; options) host = 🍭.options.server.host @@ -107,15 +107,15 @@ end end - nb = SessionActions.open(🍭, basic_nb_path; as_sample=true) + notebook = SessionActions.open(🍭, basic_nb_path; as_sample=true) simple_routes = [ ("", "GET"), - ("edit?id=$(nb.notebook_id)", "GET"), + ("edit?id=$(notebook.notebook_id)", "GET"), ("editor.html", "GET"), - ("notebookfile?id=$(nb.notebook_id)", "GET"), - ("notebookexport?id=$(nb.notebook_id)", "GET"), - ("statefile?id=$(nb.notebook_id)", "GET"), + ("notebookfile?id=$(notebook.notebook_id)", "GET"), + ("notebookexport?id=$(notebook.notebook_id)", "GET"), + ("statefile?id=$(notebook.notebook_id)", "GET"), ] function tempcopy(x) @@ -209,15 +209,15 @@ end @test shares_secret(r) # see reasoning in of https://github.com/fonsp/Pluto.jl/commit/20515dd46678a49ca90e042fcfa3eab1e5c8e162 new_ids = collect(keys(🍭.notebooks)) - nb = 🍭.notebooks[only(setdiff(new_ids, old_ids))] + notebook = 🍭.notebooks[only(setdiff(new_ids, old_ids))] if any(x -> occursin(x, suffix), ["new", "execution_allowed", "sample/Basic.jl"]) - @test Pluto.will_run_code(nb) - @test Pluto.will_run_pkg(nb) + @test Pluto.will_run_code(notebook) + @test Pluto.will_run_pkg(notebook) else - @test !Pluto.will_run_code(nb) - @test !Pluto.will_run_pkg(nb) - @test nb.process_status === Pluto.ProcessStatus.waiting_for_permission + @test !Pluto.will_run_code(notebook) + @test !Pluto.will_run_pkg(notebook) + @test notebook.process_status === Pluto.ProcessStatus.waiting_for_permission end end @@ -232,18 +232,18 @@ end PlutoRunner.is_mime_enabled(m::MIME"application/vnd.pluto.tree+object") = false """ - nb = Pluto.Notebook([ + notebook = Pluto.Notebook([ Pluto.Cell("x = [1, 2]") Pluto.Cell("struct Foo; x; end") Pluto.Cell("Foo(x)") ]) - Pluto.update_run!(🍭, nb, nb.cells) - @test nb.cells[1].output.body == repr(MIME"text/plain"(), [1,2]) - @test nb.cells[1].output.mime isa MIME"text/plain" - @test nb.cells[3].output.mime isa MIME"text/plain" + Pluto.update_run!(🍭, notebook, notebook.cells) + @test notebook.cells[1].output.body == repr(MIME"text/plain"(), [1,2]) + @test notebook.cells[1].output.mime isa MIME"text/plain" + @test notebook.cells[3].output.mime isa MIME"text/plain" - Pluto.WorkspaceManager.unmake_workspace((🍭, nb)) + cleanup(🍭, notebook) end end diff --git a/test/Dynamic.jl b/test/Dynamic.jl index 1d8675c740..2dda0f5289 100644 --- a/test/Dynamic.jl +++ b/test/Dynamic.jl @@ -313,6 +313,6 @@ end @test isempty(notebook.cells[2].published_objects) @test !isempty(notebook.cells[5].published_objects) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end end diff --git a/test/ExpressionExplorer.jl b/test/ExpressionExplorer.jl index 5409701069..f83c17be67 100644 --- a/test/ExpressionExplorer.jl +++ b/test/ExpressionExplorer.jl @@ -63,10 +63,10 @@ function testee(expr::Any, expected_references, expected_definitions, expected_f error("\n== The expression explorer modified the expression. Don't do that! ==\n") end - # Anonymous function are given a random name, which looks like anon67387237861123 + # Anonymous function are given a random name, which looks like __ExprExpl_anon__67387237861123 # To make testing easier, we rename all such functions to anon new_name(fn::FunctionName) = FunctionName(map(new_name, fn.parts)...) - new_name(sym::Symbol) = startswith(string(sym), "anon") ? :anon : sym + new_name(sym::Symbol) = startswith(string(sym), "__ExprExpl_anon__") ? :anon : sym result.assignments = Set(new_name.(result.assignments)) result.funcdefs = let diff --git a/test/Firebasey.jl b/test/Firebasey.jl index f7ee3a24e0..0f69e13681 100644 --- a/test/Firebasey.jl +++ b/test/Firebasey.jl @@ -16,5 +16,5 @@ import Pluto: ServerSession, update_run!, WorkspaceManager # and also that Pluto can figure out the execution order on its own @test all(noerror, notebook.cells) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end diff --git a/test/Logging.jl b/test/Logging.jl index b744763e67..8eb274bb54 100644 --- a/test/Logging.jl +++ b/test/Logging.jl @@ -166,5 +166,5 @@ using Pluto.WorkspaceManager: poll end end - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end diff --git a/test/MacroAnalysis.jl b/test/MacroAnalysis.jl index 3d60dd71a7..3869ab443f 100644 --- a/test/MacroAnalysis.jl +++ b/test/MacroAnalysis.jl @@ -27,6 +27,8 @@ import Memoize: @memoize @test cell(3) |> noerror @test :Fruit ∈ notebook.topology.nodes[cell(3)].references + + cleanup(🍭, notebook) end @testset "User defined macro 1" begin @@ -48,6 +50,8 @@ import Memoize: @memoize # Works on second time because of old workspace @test :x ∈ notebook.topology.nodes[cell(2)].definitions @test Symbol("@my_assign") ∈ notebook.topology.nodes[cell(2)].references + + cleanup(🍭, notebook) end @testset "User defined macro 2" begin @@ -77,6 +81,8 @@ import Memoize: @memoize @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror + + cleanup(🍭, notebook) end @testset "User defined macro 3" begin @@ -100,6 +106,8 @@ import Memoize: @memoize update_run!(🍭, notebook, cell(1)) @test cell(2) |> noerror + + cleanup(🍭, notebook) end @testset "User defined macro 4" begin @@ -114,6 +122,7 @@ import Memoize: @memoize update_run!(🍭, notebook, notebook.cells) @test Symbol("@my_assign") ∈ notebook.topology.nodes[cell(2)].references + cleanup(🍭, notebook) end @testset "User defined macro 5" begin @@ -130,6 +139,7 @@ import Memoize: @memoize @test :a ∉ references(2) @test :b ∉ references(2) @test :c ∉ references(2) + cleanup(🍭, notebook) end @testset "User defined macro 6" begin @@ -150,6 +160,7 @@ import Memoize: @memoize @test [Symbol("@my_macro"), :x, :y] ⊆ notebook.topology.nodes[cell(2)].references @test cell(3).output.body == "3" + cleanup(🍭, notebook) end @testset "Function docs" begin @@ -164,12 +175,13 @@ import Memoize: @memoize temp_topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) |> Pluto.static_resolve_topology - @test :f ∈ temp_topology.nodes[cell(1)].funcdefs_without_signatures + # @test :f ∈ temp_topology.nodes[cell(1)].funcdefs_without_signatures update_run!(🍭, notebook, notebook.cells) @test :f ∈ notebook.topology.nodes[cell(1)].funcdefs_without_signatures @test :f ∈ notebook.topology.nodes[cell(2)].references + cleanup(🍭, notebook) end @testset "Expr sanitization" begin @@ -215,6 +227,7 @@ import Memoize: @memoize @test cell(2).output.body == "true" @test all(noerror, notebook.cells) + cleanup(🍭, notebook) end @testset "Reverse order" begin @@ -242,6 +255,7 @@ import Memoize: @memoize @test cell(2) |> noerror @test cell(3) |> noerror @test cell(1).output.body == "\"yay\"" + cleanup(🍭, notebook) end @testset "@a defines @b" begin @@ -276,6 +290,7 @@ import Memoize: @memoize @test cell(3) |> noerror @test cell(4) |> noerror @test cell(1).output.body == "42" + cleanup(🍭, notebook) end @testset "Removing macros undefvar errors dependent cells" begin @@ -297,6 +312,7 @@ import Memoize: @memoize @test notebook.cells[end].errored @test expecterror(UndefVarError(Symbol("@m")), notebook.cells[end]; strict=VERSION >= v"1.7") + cleanup(🍭, notebook) end @testset "Redefines macro with new SymbolsState" begin @@ -346,6 +362,7 @@ import Memoize: @memoize # See Run.jl#resolve_topology. @test cell(4).output.body == "42" @test cell(3).errored == true + cleanup(🍭, notebook) end @testset "Reactive macro update does not invalidate the macro calls" begin @@ -382,6 +399,7 @@ import Memoize: @memoize @test cell(4).output.body != "42" @test cell(4).errored == true @test cell(5) |> noerror + cleanup(🍭, notebook) end @testset "Explicitely running macrocalls updates the reactive node" begin @@ -413,6 +431,7 @@ import Memoize: @memoize @test cell(4).errored == true @test cell(5) |> noerror + cleanup(🍭, notebook) end @testset "Implicitely running macrocalls updates the reactive node" begin @@ -451,6 +470,7 @@ import Memoize: @memoize # an explicit run of @b() must be done. @test cell(4).output.body == output_1 @test cell(5).errored == true + cleanup(🍭, notebook) end @testset "Weird behavior" begin @@ -478,6 +498,7 @@ import Memoize: @memoize @test cell(3) |> noerror @test cell(3).output.body == "1234" + cleanup(🍭, notebook) end @@ -494,6 +515,7 @@ import Memoize: @memoize # x ("@b(x)") was run. Should it? Maybe set a higher precedence to cells that define # macros inside the notebook. @test_broken noerror(notebook.cells[1]; verbose=false) + cleanup(🍭, notebook) end @testset "@a defines @b initial loading" begin @@ -517,6 +539,7 @@ import Memoize: @memoize @test cell(3) |> noerror @test cell(4) |> noerror @test cell(1).output.body == "42" + cleanup(🍭, notebook) end @testset "Macro with long compile time gets function wrapped" begin @@ -563,6 +586,7 @@ import Memoize: @memoize @test cell(1) |> noerror @test output_3 != cell(1).output.body + cleanup(🍭, notebook) end @testset "Macro Prefix" begin @@ -595,7 +619,7 @@ import Memoize: @memoize @test cell(1) |> noerror - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end @@ -624,6 +648,7 @@ import Memoize: @memoize @test cell(1) |> noerror @test cell(2) |> noerror + cleanup(🍭, notebook) end @testset "Package macro 2" begin @@ -677,7 +702,7 @@ import Memoize: @memoize @test cell(1) |> noerror @test cell(2) |> noerror - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end @@ -703,6 +728,7 @@ import Memoize: @memoize module_from_cell3 = cell(3).output.body @test module_from_cell2 == module_from_cell3 + cleanup(🍭, notebook) end @testset "Definitions" begin @@ -729,6 +755,7 @@ import Memoize: @memoize @test ":world" == cell(3).output.body @test ":world" == cell(4).output.body + cleanup(🍭, notebook) end @testset "Is just text macros" begin @@ -743,6 +770,7 @@ import Memoize: @memoize update_run!(🍭, notebook, notebook.cells) @test isempty(notebook.topology.unresolved_cells) + cleanup(🍭, notebook) end @testset "Macros using import" begin @@ -761,6 +789,7 @@ import Memoize: @memoize @test :option_type ∈ notebook.topology.nodes[cell(1)].references @test cell(1) |> noerror + cleanup(🍭, notebook) end @testset "GlobalRefs in macros should be respected" begin @@ -785,6 +814,7 @@ import Memoize: @memoize @test all(cell.([1,2,3]) .|> noerror) @test cell(3).output.body == "20" + cleanup(🍭, notebook) end @testset "GlobalRefs shouldn't break unreached undefined references" begin @@ -810,6 +840,7 @@ import Memoize: @memoize @test all(cell.([1,2]) .|> noerror) @test cell(2).output.body == ":this_should_be_returned" + cleanup(🍭, notebook) end @testset "Doc strings" begin @@ -882,6 +913,7 @@ import Memoize: @memoize update_run!(🍭, notebook, bool) @test !occursin("An empty conjugate", bool.output.body) @test occursin("complex conjugate", bool.output.body) + cleanup(🍭, notebook) end @testset "Delete methods from macros" begin @@ -926,5 +958,6 @@ import Memoize: @memoize @test expecterror(UndefVarError(:custom_func), cell(4)) @test :memoized_func ∉ notebook.topology.nodes[cell(5)].funcdefs_without_signatures @test expecterror(UndefVarError(:memoized_func), cell(6)) + cleanup(🍭, notebook) end end diff --git a/test/Notebook.jl b/test/Notebook.jl index 8d4d83e531..c3f7f64e00 100644 --- a/test/Notebook.jl +++ b/test/Notebook.jl @@ -457,7 +457,7 @@ end @testset "$(name)" for (name, nb) in nbs if name ∉ expect_error @test nb_is_runnable(🍭, nb) - WorkspaceManager.unmake_workspace((🍭, nb)) + cleanup(🍭, nb) end end end diff --git a/test/React.jl b/test/React.jl index 0516883d0f..4aa0bac071 100644 --- a/test/React.jl +++ b/test/React.jl @@ -300,7 +300,7 @@ import Pluto.Configuration: Options, EvaluationOptions @test notebook.cells[4] |> noerror @test notebook.cells[1].output.body == "\"double_december = 24\"" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end diff --git a/test/RichOutput.jl b/test/RichOutput.jl index 69e1eccff9..cf099457b8 100644 --- a/test/RichOutput.jl +++ b/test/RichOutput.jl @@ -189,7 +189,7 @@ import Pluto: update_run!, WorkspaceManager, ClientSession, ServerSession, Noteb @test occursin("102", s) @test occursin("103", s) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end @@ -322,7 +322,7 @@ import Pluto: update_run!, WorkspaceManager, ClientSession, ServerSession, Noteb # TODO: test lazy loading more rows/cols - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end diff --git a/test/WorkspaceManager.jl b/test/WorkspaceManager.jl index fc7138e4a9..77a7c5a6db 100644 --- a/test/WorkspaceManager.jl +++ b/test/WorkspaceManager.jl @@ -94,6 +94,6 @@ import Malt update_run!(🍭, notebook, notebook.cells[5]) @test noerror(notebook.cells[5]) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end end diff --git a/test/cell_disabling.jl b/test/cell_disabling.jl index 8865e57f6b..cbe0f910f7 100644 --- a/test/cell_disabling.jl +++ b/test/cell_disabling.jl @@ -236,7 +236,7 @@ using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook, set_disa update_run!(🍭, notebook, c([12])) @test c(14).output.body == "3" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @@ -343,7 +343,7 @@ end update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == [] - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @testset "Disabled cells should stay in the topology (#2676)" begin @@ -386,5 +386,5 @@ end notebook2 = Pluto.load_notebook_nobackup(io, "mynotebook.jl") @test length(notebook2.cells) == length(notebook.cells) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end diff --git a/test/frontend/__tests__/javascript_api.js b/test/frontend/__tests__/javascript_api.js index 252132f37d..7c0b9b239d 100644 --- a/test/frontend/__tests__/javascript_api.js +++ b/test/frontend/__tests__/javascript_api.js @@ -45,7 +45,7 @@ describe("JavaScript API", () => { page, `# ╔═╡ 90cfa9a0-114d-49bf-8dea-e97d58fa2442 html"""""" @@ -53,7 +53,7 @@ describe("JavaScript API", () => { ) await runAllChanged(page) await waitForPlutoToCalmDown(page, { polling: 100 }) - const initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected) + const initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output find-me`, expected) expect(initialLastCellContent).toBe(expected) }) @@ -69,7 +69,7 @@ describe("JavaScript API", () => { ) await runAllChanged(page) await waitForPlutoToCalmDown(page, { polling: 100 }) - let initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected) + let initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output span`, expected) expect(initialLastCellContent).toBe(expected) await paste( @@ -84,7 +84,7 @@ describe("JavaScript API", () => { ) await runAllChanged(page) await waitForPlutoToCalmDown(page, { polling: 100 }) - initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected) + initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output span`, expected) expect(initialLastCellContent).toBe(expected) }) diff --git a/test/frontend/__tests__/slide_controls.js b/test/frontend/__tests__/slide_controls.js index 15a50e07ea..b4949ad828 100644 --- a/test/frontend/__tests__/slide_controls.js +++ b/test/frontend/__tests__/slide_controls.js @@ -45,7 +45,7 @@ describe("slideControls", () => { await importNotebook(page, "slides.jl", { permissionToRunCode: false }) const plutoCellIds = await getCellIds(page) const content = await waitForContent(page, `pluto-cell[id="${plutoCellIds[1]}"] pluto-output`) - expect(content).toBe("Slide 2") + expect(content).toBe("Slide 2\n") const slide_1_title = await page.$(`pluto-cell[id="${plutoCellIds[0]}"] pluto-output h1`) const slide_2_title = await page.$(`pluto-cell[id="${plutoCellIds[1]}"] pluto-output h1`) diff --git a/test/frontend/__tests__/wind_directions.js b/test/frontend/__tests__/wind_directions.js index b7a06bb688..b848e2b39d 100644 --- a/test/frontend/__tests__/wind_directions.js +++ b/test/frontend/__tests__/wind_directions.js @@ -95,14 +95,14 @@ describe("wind_directions", () => { ).toBe(expected) } - await expect_chosen_directions('chosen_directions_copy\n"North"') + await expect_chosen_directions('chosen_directions_copyString1"North"') expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(0))).toBe(true) await page.click(checkbox_selector(2)) await waitForPlutoToCalmDown(page) - await expect_chosen_directions('chosen_directions_copy\n"North"\n"South"') + await expect_chosen_directions('chosen_directions_copyString1"North"2"South"') expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(0))).toBe(true) expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(1))).toBe(false) diff --git a/test/frontend/__tests__/with_js_link.js b/test/frontend/__tests__/with_js_link.js new file mode 100644 index 0000000000..c4f2c96216 --- /dev/null +++ b/test/frontend/__tests__/with_js_link.js @@ -0,0 +1,194 @@ +import puppeteer from "puppeteer" +import { saveScreenshot, createPage, waitForContentToBecome, getTextContent } from "../helpers/common" +import { + importNotebook, + getPlutoUrl, + shutdownCurrentNotebook, + setupPlutoBrowser, + getLogs, + getLogSelector, + writeSingleLineInPlutoInput, + runAllChanged, + waitForPlutoToCalmDown, +} from "../helpers/pluto" + +describe("with_js_link", () => { + /** + * Launch a shared browser instance for all tests. + * I don't use jest-puppeteer because it takes away a lot of control and works buggy for me, + * so I need to manually create the shared browser. + * @type {puppeteer.Browser} + */ + let browser = null + /** @type {puppeteer.Page} */ + let page = null + beforeAll(async () => { + browser = await setupPlutoBrowser() + page = await createPage(browser) + await page.goto(getPlutoUrl(), { waitUntil: "networkidle0" }) + + await importNotebook(page, "with_js_link.jl", { timeout: 120 * 1000 }) + }) + beforeEach(async () => {}) + afterEach(async () => { + await saveScreenshot(page) + }) + afterAll(async () => { + await shutdownCurrentNotebook(page) + await page.close() + page = null + await browser.close() + browser = null + }) + + const submit_ev_input = (id, value) => + page.evaluate( + (id, value) => { + document.querySelector(`.function_evaluator#${id} input`).value = value + + document.querySelector(`.function_evaluator#${id} input[type="submit"]`).click() + }, + id, + value + ) + + const ev_output_sel = (id) => `.function_evaluator#${id} textarea` + + const expect_ev_output = async (id, expected) => { + expect(await waitForContentToBecome(page, ev_output_sel(id), expected)).toBe(expected) + } + + it("basic", async () => { + ////// BASIC + await expect_ev_output("sqrt", "30") + await submit_ev_input("sqrt", "25") + await expect_ev_output("sqrt", "5") + }) + + // TODO test refresh + + // TODO RERUN cELL + + // TODO invalidation + + it("LOGS AND ERRORS", async () => { + ////// + let log_id = "33a2293c-6202-47ca-80d1-4a9e261cae7f" + const logs1 = await getLogs(page, log_id) + expect(logs1).toEqual([{ class: "Info", description: "you should see this log 4", kwargs: {} }]) + await submit_ev_input("logs1", "90") + + // TODO + await page.waitForFunction( + (sel) => { + return document.querySelector(sel).textContent.includes("90") + }, + { polling: 100 }, + getLogSelector(log_id) + ) + const logs2 = await getLogs(page, log_id) + expect(logs2).toEqual([ + { class: "Info", description: "you should see this log 4", kwargs: {} }, + { class: "Info", description: "you should see this log 90", kwargs: {} }, + ]) + }) + it("LOGS AND ERRORS 2", async () => { + const logs3 = await getLogs(page, "480aea45-da00-4e89-b43a-38e4d1827ec2") + expect(logs3.length).toEqual(2) + expect(logs3[0]).toEqual({ class: "Warn", description: "You should see the following error:", kwargs: {} }) + expect(logs3[1].class).toEqual("Error") + expect(logs3[1].description).toContain("with_js_link") + expect(logs3[1].kwargs.input).toEqual('"coOL"') + expect(logs3[1].kwargs.exception).toContain("You should see this error COOL") + }) + it("LOGS AND ERRORS 3: assertpackable", async () => { + const logs = await getLogs(page, "b310dd30-dddd-4b75-81d2-aaf35c9dd1d3") + expect(logs.length).toEqual(2) + expect(logs[0]).toEqual({ class: "Warn", description: "You should see the assertpackable fail after this log", kwargs: {} }) + expect(logs[1].class).toEqual("Error") + expect(logs[1].description).toContain("with_js_link") + expect(logs[1].kwargs.input).toEqual('"4"') + expect(logs[1].kwargs.exception).toContain("Only simple objects can be shared with JS") + }) + + it("globals", async () => { + await expect_ev_output("globals", "54") + }) + it("multiple in one cell", async () => { + await expect_ev_output("uppercase", "ΠΑΝΑΓΙΏΤΗΣ") + await expect_ev_output("lowercase", "παναγιώτης") + + await submit_ev_input("uppercase", "wOw") + + await expect_ev_output("uppercase", "WOW") + await expect_ev_output("lowercase", "παναγιώτης") + + await submit_ev_input("lowercase", "drOEF") + + await expect_ev_output("uppercase", "WOW") + await expect_ev_output("lowercase", "droef") + }) + it("repeated", async () => { + await expect_ev_output(`length[cellid="40031867-ee3c-4aa9-884f-b76b5a9c4dec"]`, "7") + await expect_ev_output(`length[cellid="7f6ada79-8e3b-40b7-b477-ce05ae79a668"]`, "7") + + await submit_ev_input(`length[cellid="40031867-ee3c-4aa9-884f-b76b5a9c4dec"]`, "yay") + + await expect_ev_output(`length[cellid="40031867-ee3c-4aa9-884f-b76b5a9c4dec"]`, "3") + await expect_ev_output(`length[cellid="7f6ada79-8e3b-40b7-b477-ce05ae79a668"]`, "7") + }) + + it("concurrency", async () => { + await expect_ev_output("c1", "C1") + await expect_ev_output("c2", "C2") + + await submit_ev_input("c1", "cc1") + await submit_ev_input("c2", "cc2") + + await page.waitForTimeout(4000) + + // NOT + // they dont run in parallel so right now only cc1 should be finished + // expect(await page.evaluate((s) => document.querySelector(s).textContent, ev_output_sel("c1"))).toBe("CC1") + // expect(await page.evaluate((s) => document.querySelector(s).textContent, ev_output_sel("c2"))).toBe("C2") + + // await expect_ev_output("c1", "CC1") + // await expect_ev_output("c2", "CC2") + + // they should run in parallel: after 4 seconds both should be finished + expect(await page.evaluate((s) => document.querySelector(s).textContent, ev_output_sel("c1"))).toBe("CC1") + expect(await page.evaluate((s) => document.querySelector(s).textContent, ev_output_sel("c2"))).toBe("CC2") + }) + + const expect_jslog = async (expected) => { + expect(await waitForContentToBecome(page, "#checkme", expected)).toBe(expected) + } + it("js errors", async () => { + await waitForPlutoToCalmDown(page) + await page.waitForTimeout(100) + await expect_jslog("hello!") + await page.click("#jslogbtn") + await page.waitForTimeout(500) + await page.click("#jslogbtn") + await page.waitForTimeout(100) + + // We clicked twice, but sometimes it only registers one click for some reason. I don't care, so let's check for either. + let prefix = await Promise.race([ + waitForContentToBecome(page, "#checkme", "hello!clickyay KRATJE"), + waitForContentToBecome(page, "#checkme", "hello!clickclickyay KRATJEyay KRATJE"), + ]) + + const yolotriggerid = "8782cc14-eb1a-48a8-a114-2f71f77be275" + await page.click(`pluto-cell[id="${yolotriggerid}"] pluto-output input[type="button"]`) + await expect_jslog(`${prefix}hello!`) + await page.click("#jslogbtn") + await expect_jslog(`${prefix}hello!clicknee exception in Julia callback:ErrorException("bad")`) + + await page.click("#jslogbtn") + await page.waitForTimeout(500) + + await page.click(`pluto-cell[id="${yolotriggerid}"] .runcell`) + + await expect_jslog(`${prefix}hello!clicknee exception in Julia callback:ErrorException("bad")clickhello!nee link not found`) + }) +}) diff --git a/test/frontend/fixtures/slides.jl b/test/frontend/fixtures/slides.jl index d903c7dce1..fe0f3262e7 100644 --- a/test/frontend/fixtures/slides.jl +++ b/test/frontend/fixtures/slides.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.11.14 +# v0.19.40 using Markdown using InteractiveUtils diff --git a/test/frontend/fixtures/wind_directions.jl b/test/frontend/fixtures/wind_directions.jl index 7f3d48f700..a3f805598c 100644 --- a/test/frontend/fixtures/wind_directions.jl +++ b/test/frontend/fixtures/wind_directions.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.19.31 +# v0.19.40 using Markdown using InteractiveUtils @@ -679,6 +679,7 @@ version = "1.2.0" [[ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.1" [[Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" @@ -698,6 +699,11 @@ git-tree-sha1 = "532c4185d3c9037c0237546d817858b23cf9e071" uuid = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" version = "0.8.12" +[[CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.0.5+1" + [[Crayons]] git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" @@ -708,8 +714,12 @@ deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" [[Downloads]] -deps = ["ArgTools", "LibCURL", "NetworkOptions"] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.6.0" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" [[FixedPointNumbers]] deps = ["Statistics"] @@ -748,24 +758,32 @@ version = "0.21.4" [[LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" [[LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.4.0+0" [[LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +[[LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.6.4+0" + [[LibSSH2_jll]] deps = ["Artifacts", "Libdl", "MbedTLS_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.0+1" [[Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" [[LinearAlgebra]] -deps = ["Libdl"] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[Logging]] @@ -789,15 +807,23 @@ version = "0.1.1" [[MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.2+1" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" [[MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2023.1.10" [[NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" + +[[OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.23+2" [[Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] @@ -806,8 +832,9 @@ uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.7.2" [[Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.10.0" [[PlutoUI]] deps = ["AbstractPlutoDingetjes", "Base64", "ColorTypes", "Dates", "FixedPointNumbers", "Hyperscript", "HypertextLiteral", "IOCapture", "InteractiveUtils", "JSON", "Logging", "MIMEs", "Markdown", "Random", "Reexport", "URIs", "UUIDs"] @@ -836,7 +863,7 @@ deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[Random]] -deps = ["Serialization"] +deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [[Reexport]] @@ -846,6 +873,7 @@ version = "1.2.2" [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" [[Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -854,20 +882,29 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" uuid = "6462fe0b-24de-5631-8697-dd941f90decc" [[SparseArrays]] -deps = ["LinearAlgebra", "Random"] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.10.0" [[Statistics]] deps = ["LinearAlgebra", "SparseArrays"] uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.10.0" + +[[SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "7.2.1+1" [[TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" [[Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" [[Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] @@ -893,14 +930,22 @@ uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [[Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.13+1" + +[[libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.8.0+1" [[nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.52.0+1" [[p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.4.0+2" """ # ╔═╡ Cell order: diff --git a/test/frontend/fixtures/with_js_link.jl b/test/frontend/fixtures/with_js_link.jl new file mode 100644 index 0000000000..6db0b7db03 --- /dev/null +++ b/test/frontend/fixtures/with_js_link.jl @@ -0,0 +1,431 @@ +### A Pluto.jl notebook ### +# v0.19.40 + +using Markdown +using InteractiveUtils + +# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). +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 + +# ╔═╡ b0f2a778-885f-11ee-3d28-939ca4069ee8 +begin + import Pkg + Pkg.activate(temp=true) + Pkg.add([ + Pkg.PackageSpec(name="AbstractPlutoDingetjes") + Pkg.PackageSpec(name="HypertextLiteral") + Pkg.PackageSpec(name="PlutoUI") + ]) + + using AbstractPlutoDingetjes + using PlutoUI + using HypertextLiteral +end + +# ╔═╡ 5e42ea32-a1ce-49db-b55f-5e252c8c3f57 +using Dates + +# ╔═╡ 37aacc7f-61fd-4c4b-b24d-42361d508e8d +@htl(""" + +""") + +# ╔═╡ 30d7c350-f792-47e9-873a-01adf909bc84 +md""" +If you change `String` to `AbstractString` here then you get some back logs: +""" + +# ╔═╡ 75752f77-1e3f-4997-869b-8bee2c12a2cb +function cool(x::String) + uppercase(x) +end + +# ╔═╡ 3098e16a-4730-4564-a484-02a6b0278930 +# function cool() +# end + +# ╔═╡ 37fc039e-7a4d-4d2d-80f3-d409a9ee096d +# ╠═╡ disabled = true +#=╠═╡ +# let +# function f(x) +# cool(x) +# end +# @htl(""" +# +# """) +# end + ╠═╡ =# + +# ╔═╡ 977c59f7-9f3a-40ae-981d-2a8a48e08349 + + +# ╔═╡ b3186d7b-8fd7-4575-bccf-8e89ce611010 +md""" +# Benchmark + +We call the `next` function from JavaScript in a loop until `max` is reached, to calculate the time of each round trip. +""" + +# ╔═╡ 82c7a083-c84d-4924-bad2-776d3cdad797 +next(x) = x + 1; + +# ╔═╡ e8abaff9-f629-47c6-8009-066bcdf67693 +max = 250; + +# ╔═╡ bf9861e0-be91-4041-aa61-8ac2ef6cb719 +@htl(""" +
+

+

Current value:

+

Past values:

+

Time per round trip:

+ +
+""") + +# ╔═╡ ebf79ee4-2590-4b5a-a957-213ed03a5921 +md""" +# Concurrency +""" + +# ╔═╡ 60444c4c-5705-4b92-8eac-2c102f14f395 + + +# ╔═╡ 07c832c1-fd8f-44de-bdfa-389048c1e4e9 +md""" +## With a function in the closure +""" + +# ╔═╡ 10d80b00-f7ab-4bd7-9ea7-cca98c089e9c +coolthing(x) = x + +# ╔═╡ bf7a885e-4d0a-408d-b6d5-d3289d794240 +try + sqrt(-1) +catch e + sprint(showerror, e) +end + +# ╔═╡ 0eff37d6-9cd5-42bb-b274-de364ca7ed53 + + +# ╔═╡ 663e5a70-4d07-4d6a-8725-dc9a2b26b65d +md""" +# Tests +""" + +# ╔═╡ 1d32fd55-9ca0-45c8-97f5-23cb29eaa8b3 +md""" +Test a closure +""" + +# ╔═╡ 5f3c590e-07f2-4dea-b6d1-e9d90f501fda +some_other_global = rand(100) + +# ╔═╡ 3d836ff3-995e-4353-807e-bf2cd78920e2 +some_global = 51:81 + +# ╔═╡ 2461d75e-81dc-4e00-99e3-bbc44000579f +AbstractPlutoDingetjes.Display.with_js_link(x -> x) + +# ╔═╡ 12e64b86-3866-4e21-9af5-0e546452b4e1 +function function_evaluator(f::Function, default=""; id=string(f)) + @htl(""" +
+

Input:
+  

+ +

Output:
+ + + +

+ """) +end + +# ╔═╡ 4b80dda0-74b6-4a0e-a50e-61c5380111a4 +function_evaluator(900; id="sqrt") do input + num = parse(Float64, input) + sqrt(num) +end + +# ╔═╡ a399cb12-39d4-43c0-a0a7-05cb683dffbd +function_evaluator("c1"; id="c1") do input + @info "start" Dates.now() + sleep(3) + # peakflops(3000) + + + @warn "end" Dates.now() + uppercase(input) + +end + +# ╔═╡ 2bff3975-5918-40fe-9761-eb7b47f16df2 +function_evaluator("c2"; id="c2") do input + @info "start" Dates.now() + sleep(3) + # peakflops(3000) + + @warn "end" Dates.now() + uppercase(input) +end + +# ╔═╡ 53e60352-3a56-4b5c-9568-1ac58b758497 +function_evaluator("hello") do str + sleep(5) + result = coolthing(str) + @info result + result +end + +# ╔═╡ 2b5cc4b1-ca57-4cb6-a42a-dcb331ed2c26 +let + thing = function_evaluator("1") do str + some_other_global[1:parse(Int,str)] + end + if false + fff() = 123 + end + thing +end + +# ╔═╡ 85e9daf1-d8e3-4fc0-8acd-10d863e724d0 +let + x = rand(100) + function_evaluator("1") do str + x[parse(Int,str)] + end +end + +# ╔═╡ abb24301-357c-40f0-832e-86f26404d3d9 +function_evaluator("THIS IN LOWERCASE") do input + "you should see $(lowercase(input))" +end + +# ╔═╡ 33a2293c-6202-47ca-80d1-4a9e261cae7f +function_evaluator(4; id="logs1") do input + @info "you should see this log $(input)" + println("(not currently supported) you should see this print $(input)") + + rand(parse(Int, input)) +end + +# ╔═╡ 480aea45-da00-4e89-b43a-38e4d1827ec2 +function_evaluator("coOL") do input + @warn("You should see the following error:") + + error("You should see this error $(uppercase(input))") +end + +# ╔═╡ b310dd30-dddd-4b75-81d2-aaf35c9dd1d3 +function_evaluator(4) do input + @warn("You should see the assertpackable fail after this log") + + :(@heyyy cant msgpack me) +end + +# ╔═╡ 58999fba-6631-4482-a811-12bf2412d65e +function_evaluator(4; id="globals") do input + some_global[parse(Int, input)] +end + +# ╔═╡ 9e5c0f8d-6ac1-4aee-a00d-938f17eec146 +md""" +You should be able to use `with_js_link` multiple times within one cell, and they should work independently of eachother: +""" + +# ╔═╡ 306d03da-cd50-4b0c-a5dd-7ec1a278cde1 +@htl(""" +
+ $(function_evaluator(uppercase, "Παναγιώτης")) + $(function_evaluator(lowercase, "Παναγιώτης")) +
+""") + +# ╔═╡ 2cf033a7-bcd7-434d-9faf-ea761897fb64 +md""" +You should be able to set up a `with_js_link` in one cell, and use it in another. This example is a bit trivial though... +""" + +# ╔═╡ 40031867-ee3c-4aa9-884f-b76b5a9c4dec +fe = function_evaluator(length, "Alberto") + +# ╔═╡ 7f6ada79-8e3b-40b7-b477-ce05ae79a668 +fe + +# ╔═╡ f344c4cb-8226-4145-ab92-a37542f697dd +md""" +You should see a warning message when `with_js_link` is not used inside an HTML renderer that supports it: +""" + +# ╔═╡ 8bbd32f8-56f7-4f29-aea8-6906416f6cfd +let + html_repr = repr(MIME"text/html"(), fe) + HTML(html_repr) +end + +# ╔═╡ 8782cc14-eb1a-48a8-a114-2f71f77be275 +@bind yolotrigger CounterButton() + +# ╔═╡ e5df2451-f4b9-4511-b25f-1a5e463f3eb2 +name = yolotrigger > 0 ? "krat" : "kratje" + +# ╔═╡ 3c5c1325-ad3e-4c54-8d29-c17939bb8529 +function useme(x) + length(x) > 5 ? uppercase(x) : error("bad") +end + +# ╔═╡ 6c5f79b9-598d-41ad-800d-0a9ff63d6f6c +@htl(""" + + +""") + +# ╔═╡ Cell order: +# ╠═b0f2a778-885f-11ee-3d28-939ca4069ee8 +# ╠═4b80dda0-74b6-4a0e-a50e-61c5380111a4 +# ╠═37aacc7f-61fd-4c4b-b24d-42361d508e8d +# ╟─30d7c350-f792-47e9-873a-01adf909bc84 +# ╠═75752f77-1e3f-4997-869b-8bee2c12a2cb +# ╠═3098e16a-4730-4564-a484-02a6b0278930 +# ╠═37fc039e-7a4d-4d2d-80f3-d409a9ee096d +# ╠═977c59f7-9f3a-40ae-981d-2a8a48e08349 +# ╟─b3186d7b-8fd7-4575-bccf-8e89ce611010 +# ╠═82c7a083-c84d-4924-bad2-776d3cdad797 +# ╠═e8abaff9-f629-47c6-8009-066bcdf67693 +# ╟─bf9861e0-be91-4041-aa61-8ac2ef6cb719 +# ╟─ebf79ee4-2590-4b5a-a957-213ed03a5921 +# ╠═a399cb12-39d4-43c0-a0a7-05cb683dffbd +# ╠═5e42ea32-a1ce-49db-b55f-5e252c8c3f57 +# ╠═60444c4c-5705-4b92-8eac-2c102f14f395 +# ╠═2bff3975-5918-40fe-9761-eb7b47f16df2 +# ╟─07c832c1-fd8f-44de-bdfa-389048c1e4e9 +# ╠═10d80b00-f7ab-4bd7-9ea7-cca98c089e9c +# ╠═53e60352-3a56-4b5c-9568-1ac58b758497 +# ╠═bf7a885e-4d0a-408d-b6d5-d3289d794240 +# ╠═0eff37d6-9cd5-42bb-b274-de364ca7ed53 +# ╟─663e5a70-4d07-4d6a-8725-dc9a2b26b65d +# ╟─1d32fd55-9ca0-45c8-97f5-23cb29eaa8b3 +# ╠═5f3c590e-07f2-4dea-b6d1-e9d90f501fda +# ╠═2b5cc4b1-ca57-4cb6-a42a-dcb331ed2c26 +# ╠═85e9daf1-d8e3-4fc0-8acd-10d863e724d0 +# ╠═abb24301-357c-40f0-832e-86f26404d3d9 +# ╠═33a2293c-6202-47ca-80d1-4a9e261cae7f +# ╠═480aea45-da00-4e89-b43a-38e4d1827ec2 +# ╠═b310dd30-dddd-4b75-81d2-aaf35c9dd1d3 +# ╠═3d836ff3-995e-4353-807e-bf2cd78920e2 +# ╠═58999fba-6631-4482-a811-12bf2412d65e +# ╠═2461d75e-81dc-4e00-99e3-bbc44000579f +# ╠═12e64b86-3866-4e21-9af5-0e546452b4e1 +# ╟─9e5c0f8d-6ac1-4aee-a00d-938f17eec146 +# ╠═306d03da-cd50-4b0c-a5dd-7ec1a278cde1 +# ╟─2cf033a7-bcd7-434d-9faf-ea761897fb64 +# ╠═40031867-ee3c-4aa9-884f-b76b5a9c4dec +# ╠═7f6ada79-8e3b-40b7-b477-ce05ae79a668 +# ╟─f344c4cb-8226-4145-ab92-a37542f697dd +# ╠═8bbd32f8-56f7-4f29-aea8-6906416f6cfd +# ╠═8782cc14-eb1a-48a8-a114-2f71f77be275 +# ╠═e5df2451-f4b9-4511-b25f-1a5e463f3eb2 +# ╠═3c5c1325-ad3e-4c54-8d29-c17939bb8529 +# ╠═6c5f79b9-598d-41ad-800d-0a9ff63d6f6c diff --git a/test/frontend/helpers/common.js b/test/frontend/helpers/common.js index c0578974b4..c2739c6b28 100644 --- a/test/frontend/helpers/common.js +++ b/test/frontend/helpers/common.js @@ -66,7 +66,7 @@ const with_connections_debug = (page, action) => { export const getTextContent = (page, selector) => { // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext return page.evaluate( - (selector) => document.querySelector(selector).innerText, + (selector) => document.querySelector(selector)?.textContent, selector ); }; @@ -127,18 +127,23 @@ export const waitForContentToChange = async ( return getTextContent(page, selector); }; -export const waitForContentToBecome = async (page, selector, targetContent) => { +export const waitForContentToBecome = async (/** @type {puppeteer.Page} */ page, /** @type {string} */ selector, /** @type {string} */ targetContent) => { await page.waitForSelector(selector, { visible: true }); - await page.waitForFunction( + try{ + await page.waitForFunction( (selector, targetContent) => { const element = document.querySelector(selector); // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext - return element !== null && element.innerText === targetContent; + return element !== null && element.textContent === targetContent; }, { polling: 100 }, selector, targetContent ); + } catch(e) { + console.error("Failed! Current content: ", JSON.stringify(await getTextContent(page, selector)), "Expected content: ", JSON.stringify(targetContent)) + throw(e) + } return getTextContent(page, selector); }; @@ -211,7 +216,7 @@ export const createPage = async (browser) => { return page }; -let testname = () => expect.getState()?.currentTestName?.replace(/ /g, "_") ?? "unnkown"; +let testname = () => expect.getState()?.currentTestName?.replace(/[ \:]/g, "_") ?? "unnkown"; export const lastElement = (arr) => arr[arr.length - 1]; diff --git a/test/frontend/helpers/pluto.js b/test/frontend/helpers/pluto.js index ea6ce564d1..b0856babdb 100644 --- a/test/frontend/helpers/pluto.js +++ b/test/frontend/helpers/pluto.js @@ -134,23 +134,51 @@ export const restartProcess = async (page) => { await page.waitForSelector(`#process-status-tab-button.something_is_happening`) } +/** + * @param {Page} page + * @param {boolean} iWantBusiness + */ const waitForPlutoBusy = async (page, iWantBusiness, options) => { await page.waitForTimeout(1) - await page.waitForFunction( - (iWantBusiness) => { - let quiet = //@ts-ignore - (document?.body?._update_is_ongoing ?? false) === false && - //@ts-ignore - (document?.body?._js_init_set?.size ?? 0) === 0 && - document?.body?.classList?.contains("loading") === false && - document?.querySelector(`#process-status-tab-button.something_is_happening`) == null && - document?.querySelector(`pluto-cell.running, pluto-cell.queued, pluto-cell.internal_test_queued`) == null - - return iWantBusiness ? !quiet : quiet - }, - options, - iWantBusiness - ) + try { + await page.waitForFunction( + (iWantBusiness) => { + const quiet_vals = [ + // @ts-ignore + document?.body?._update_is_ongoing, + // @ts-ignore + document?.body?._js_init_set?.size, + document?.body?.classList?.contains("loading"), + document?.querySelector(`#process-status-tab-button.something_is_happening`)?.id, + document?.querySelector(`pluto-cell.running, pluto-cell.queued, pluto-cell.internal_test_queued`)?.id, + ] + + let quiet = + (quiet_vals[0] ?? false) === false && + (quiet_vals[1] ?? 0) === 0 && + quiet_vals[2] === false && + quiet_vals[3] == null && + quiet_vals[4] == null + + window["quiet_vals"] = quiet_vals + + return iWantBusiness ? !quiet : quiet + }, + options, + iWantBusiness + ) + } catch (e) { + console.error( + "waitForPlutoBusy failed\n", + JSON.parse( + await page.evaluate(() => { + return JSON.stringify(window["quiet_vals"]) + }) + ) + ) + + throw e + } await page.waitForTimeout(1) } @@ -187,11 +215,29 @@ export const waitForNoUpdateOngoing = async (page, options = {}) => { return await page.waitForFunction( () => //@ts-ignore - document.body?._update_is_ongoing === false, + (document.body?._update_is_ongoing ?? false) === false, options ) } +export const getLogSelector = (cellId) => `pluto-cell[id="${cellId}"] pluto-logs` + +export const getLogs = async (page, cellid) => { + return await page.evaluate((sel) => { + const logs = document.querySelector(sel) + return Array.from(logs.children).map((el) => ({ + class: el.className.trim(), + description: el.querySelector("pluto-log-dot > pre").textContent, + kwargs: Object.fromEntries( + Array.from(el.querySelectorAll("pluto-log-dot-kwarg")).map((x) => [ + x.querySelector("pluto-key").textContent, + x.querySelector("pluto-value").textContent, + ]) + ), + })) + }, getLogSelector(cellid)) +} + /** * @param {Page} page */ diff --git a/test/frontend/jest.config.js b/test/frontend/jest.config.js index b4edfb11b6..93fb8f86da 100644 --- a/test/frontend/jest.config.js +++ b/test/frontend/jest.config.js @@ -1,4 +1,4 @@ module.exports = { - testTimeout: 100000, + testTimeout: 300000, slowTestThreshold: 30, } diff --git a/test/helpers.jl b/test/helpers.jl index a436f87fc7..34c333310e 100644 --- a/test/helpers.jl +++ b/test/helpers.jl @@ -154,3 +154,19 @@ const pluto_test_registry_spec = Pkg.RegistrySpec(; name="PlutoPkgTestRegistry", ) +const snapshots_dir = joinpath(@__DIR__, "snapshots") + +isdir(snapshots_dir) && rm(snapshots_dir; force=true, recursive=true) +mkdir(snapshots_dir) + +function cleanup(session, notebook) + testset_stack = get(task_local_storage(), :__BASETESTNEXT__, Test.AbstractTestSet[]) + name = replace(join((t.description for t in testset_stack), " – "), r"[\:\?\r\n<>\|\*]" => "-") + + path = Pluto.numbered_until_new(joinpath(snapshots_dir, name); suffix=".html", create_file=true) + + write(path, Pluto.generate_html(notebook)) + + WorkspaceManager.unmake_workspace((session, notebook)) +end + diff --git a/test/packages/Basic.jl b/test/packages/Basic.jl index c9b4f8c3a0..de74516d7f 100644 --- a/test/packages/Basic.jl +++ b/test/packages/Basic.jl @@ -206,7 +206,7 @@ import Malt @test count("PlutoPkgTestD", ptoml_contents()) == 0 - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end simple_import_path = joinpath(@__DIR__, "simple_import.jl") @@ -233,7 +233,7 @@ import Malt @test notebook.cells[2].output.body == "0.2.2" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @testset "Package added by url" begin @@ -258,7 +258,7 @@ import Malt @test notebook.cells[2].output.body == "1.0.0" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end future_notebook = read(joinpath(@__DIR__, "future_nonexisting_version.jl"), String) @@ -283,7 +283,7 @@ import Malt @test notebook.cells[2].output.body == "0.3.1" - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @@ -337,7 +337,7 @@ import Malt @test notebook.nbpkg_restart_required_msg !== nothing @test has_embedded_pkgfiles(notebook) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end pkg_cell_notebook = read(joinpath(@__DIR__, "pkg_cell.jl"), String) @@ -392,7 +392,7 @@ import Malt @test notebook.nbpkg_restart_recommended_msg === nothing @test notebook.nbpkg_restart_required_msg === nothing - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @testset "DrWatson cell" begin @@ -443,7 +443,7 @@ import Malt @test notebook.nbpkg_restart_recommended_msg === nothing @test notebook.nbpkg_restart_required_msg === nothing - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @static if VERSION < v"1.10.0-0" # see https://github.com/fonsp/Pluto.jl/pull/2626#issuecomment-1671244510 @@ -596,7 +596,7 @@ import Malt @test has_embedded_pkgfiles(notebook) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end end @@ -649,7 +649,7 @@ import Malt wait.(running_tasks) empty!(running_tasks) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @testset "PlutoRunner Syntax Error" begin @@ -671,7 +671,7 @@ import Malt @test !Pluto.is_just_text(notebook.topology, notebook.cells[2]) # Not a syntax error form @test Pluto.is_just_text(notebook.topology, notebook.cells[3]) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end @testset "Precompilation" begin @@ -758,7 +758,7 @@ import Malt # Running the import should not have triggered additional precompilation, everything should have been precompiled during Pkg.precompile() (in sync_nbpkg). @test after_sync == after_run - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end end @@ -773,7 +773,7 @@ import Malt @test isnothing(notebook.nbpkg_ctx) @test notebook.cells[2].output.body == sprint(Base.show, LOAD_PATH[begin]) @test notebook.cells[3].output.body == sprint(Base.show, LOAD_PATH[end]) - WorkspaceManager.unmake_workspace((🍭, notebook)) + cleanup(🍭, notebook) end Pkg.Registry.rm(pluto_test_registry_spec) diff --git a/test/packages/pkg_cell.jl b/test/packages/pkg_cell.jl index 1ec868edaf..c374343978 100644 --- a/test/packages/pkg_cell.jl +++ b/test/packages/pkg_cell.jl @@ -8,6 +8,7 @@ using InteractiveUtils begin import Pkg Pkg.activate(joinpath(@__DIR__)) + Pkg.resolve() Pkg.instantiate() # Pkg.status() diff --git a/test/runtests.jl b/test/runtests.jl index 3653efc485..83b2bae1b1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,13 +12,13 @@ end verify_no_running_processes() @timeit_include("Configuration.jl") verify_no_running_processes() -@timeit_include("packages/Basic.jl") +@timeit_include("React.jl") verify_no_running_processes() @timeit_include("Bonds.jl") verify_no_running_processes() @timeit_include("RichOutput.jl") verify_no_running_processes() -@timeit_include("React.jl") +@timeit_include("packages/Basic.jl") verify_no_running_processes() @timeit_include("Dynamic.jl") verify_no_running_processes() @@ -51,12 +51,3 @@ verify_no_running_processes() print_timeroutput() @timeit_include("ExpressionExplorer.jl") -# TODO: test PlutoRunner functions like: -# - from_this_notebook - -# TODO: test include() inside notebooks - -# TODO: test async execution order -# TODO: test @bind - -# TODO: test if notebooks are saved correctly after edits